Java 序列化与反序列化 & Apache Commons Collections反序列化漏洞

java 序列化与反序列化

简单例子

首先我们来看下简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main( String[] args) throws Exception{
String s = "abcd是我啊";
int i = 5;
FileOutputStream sfos= new FileOutputStream("string.out");
FileOutputStream ifos= new FileOutputStream("int.out");
ObjectOutputStream soos = new ObjectOutputStream(sfos);
ObjectOutputStream ioos = new ObjectOutputStream(ifos);
soos.writeObject(s);
ioos.writeObject(i);
soos.close();
ioos.close();
FileInputStream fis = new FileInputStream("string.out");
ObjectInputStream ois = new ObjectInputStream(fis);
String obj = (String) ois.readObject();
System.out.println(obj);
}
simple

根据结果我们可以看到,序列化了2个,一个String一个int.

先看string:
0x ACED作为序列化的标识符
0x 0005作为序列化版本号,这些都是常量,定义在Constants中。
0x 74表示String类型
0x 000Dstring长度 为13字节
0x 61626364 E698AFE6 8891E595 8A后面对应于内容的UTF-8编码值。

而int序列化
0x 73表示TC_OBJECT
0x 74表示TC_CLASSDESC(class描述符)
0x 11表示描述符长度,对应java.lang.Integer
0x 12E2A0A4 F7818738SerialVersionUID,如果不指定,会随机生成8字节uid
0x 02为标记号. 该值声明该对象支持序列化
0x 0001 表示域个数
0x 49 表示域类型 对应I 表示Integer
0x 0005 域名字长度
0x 76616C75 65 对应域名字描述value
0x 78 对应于TC_ENDBLOCKDATA 表示对象块结束

0x 78 对应于TC_ENDBLOCKDATA 表示对象块结束
0x 70 对应于TC_NULL 表示没有超类了
0x 0000 0005 对应值5

其中的Constants字段值可以参考java.io.ObjectStreamConstants

stream_magic

复杂例子

之前的例子使用的都是java常见的内部类,如果复杂一点呢?如果是我们自己定义的class,只需要实现Serializable接口即可。Serializable接口中没有任何方法,可以理解为一个标记,即表明这个类可以序列化。例如:

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 Test implements Serializable {
private static final long serialVersionUID= 305419896L;
private String name;
private int age;
private long date;
public Test(String name, int age){
this.name = name;
this.age = age;
this.date = new Date().getTime();
}
@Override
public String toString() {
return "Test{" +
"name='" + name + '\'' +
", age=" + age +
", date=" + date +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}

序列化后的结果如下:

class

我们可以看到,我们指定了uid之后,其值为12345678.

关于字段的详细含义,可以看这篇文章Object Serialization Stream Protocol 极其推荐!极其推荐!极其推荐! 这里引用一些比较关键的点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
A basic structure is needed to represent objects in a stream.
Each attribute of the object needs to be represented: its classes, its fields, and data written and later read by class-specific methods.
The representation of objects in the stream can be described with a grammar.
There are special representations for null objects, new objects, classes, arrays, strings, and back references to any object already in the stream.
Each object written to the stream is assigned a handle that is used to refer back to the object.
Handles are assigned sequentially starting from 0x7E0000. The handles restart at 0x7E0000 when the stream is reset.
New objects in the stream are represented by the following:
The most derived class of the object.
Data for each serializable class of the object, with the highest superclass first. For each class the stream contains the following:
The serializable fields.See Section 1.5, "Defining Serializable Fields for a Class."
If the class has writeObject/readObject methods, there may be optional objects and/or block-data records of primitive types written by the writeObject method followed by an endBlockData code.

其中的Rules of the Grammar可以仔细看下。

serialVersionUID

随着项目的更新,不同的serialVersionUID被认为是不同的class版本,在反序列化中可能会报错。如果手动设置UID,可以遵守以下规则:

1
2
3
4
5
6
7
只修改了类的方法,无需改变serialVersionUID;
只修改了类的static变量和使用transient 修饰的实例变量,无需改变serialVersionUID;
如果修改了实例变量的类型,例如一个变量原来是int改成了String,则反序列化会失败,需要修改serialVersionUID;如果删除了类的一些实例变量,可以兼容无需修改;如果给类增加了一些实例变量,可以兼容无需修改,只是反序列化后这些多出来的变量的值都是默认值。
[注]
1. 如果不想把变量序列化 可以加 transient字段
2. 序列化对象 父类和其引用也必须可以序列化

序列化引用

在之前,提到每个序列化后的stream都会分配一个handler值,从0x 007E 0000开始,当同一类的不同对象再序列化时,其会首先查看原始是否存在,如果存在则直接引用,不会再去重新生成一个。

reference

自定义序列化

writeObject & readObject

在序列化和反序列化过程中,我们可以实现:

1
2
private void writeObject(ObjectOutputStream out)
private void readObject(ObjectInputStream in)

来完全控制序列化和反序列化的过程

ObjectOutputStream先通过反射在要被序列化的对象的类中查找有无自定义的writeObject方法,若有,则会优先调用自定义的writeObject方法。因为查找反射方法时使用的是getPrivateMethod,所以自定以的writeObject方法的作用域要被设置为private。通过自定义writeObject和readObject方法可以完全控制对象的序列化与反序列化。

wr

当然,我们需要对应write和read的顺序,不然会报错,如图所示,可以看到我们在所Test对象之后附带了一个字符串,且字符串和原始对象均被正常读入处理。类似这种操作一般用于session中,添加session处理时间而不将时间戳信息写入session对象当中。

writeReplace & readResolve

这两个方法分别在writeObjectreadObject的前、后进行处理,与PHP反序列化中的__sleep & __wakeup类似,都是用于序列化与反序列化前后对对象进行处理的函数

implement Externalizable接口

实现Externalizable则序列化反序列化流程完全由我们自己决定:

1
2
public void writeExternal(ObjectOutput out)
public void readExternal(ObjectInput in)

java反序列化漏洞

我们再看下面的例子,由于java反序列化中没有指定反序列化对象的类型,任意对象均可被反序列化,那么在反序列化过程中,如果对象本身有一些函数会自动调用,则会造成任意代码执行。

de

由于反序列化存在于一些公共库,例如Apache Commons Collections,所以危害影响非常广。

漏洞基本原理

基本原理

如图,如果我们在readObject代码之后还去执行了一些其他东西,这就是漏洞的基本原理,但可能会想,谁会这样写?但实际上,一些有潜在危险的地方通过精心构造,是可以实现的。

所以一般漏洞产生的条件实现了Serializable接口以及 重写了readObject方法

Apache Commons Collections

问题出现在commons-collections这个apache组件当中,其中有个InvokerTransformer存在问题。我们先看一下这个类的内容:

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
public class InvokerTransformer implements Transformer, Serializable {
static final long serialVersionUID = -8653385846894047688L;
private final String iMethodName;
private final Class[] iParamTypes;
private final Object[] iArgs;
public static Transformer getInstance(String methodName) {
...
}
public static Transformer getInstance(String methodName, Class[] paramTypes, Object[] args) {
...
}
private InvokerTransformer(String methodName) {
this.iMethodName = methodName;
this.iParamTypes = null;
this.iArgs = null;
}
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName;
this.iParamTypes = paramTypes;
this.iArgs = args;
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var7) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var7);
}
}
}
}

我们看下这个类,在transform方法中,我们看到了和基本漏洞相似的写法:

1
2
3
Class cls = input.getClass();
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
return method.invoke(input, this.iArgs);

是不是很熟悉,如果inputiMethodNameiParamTypesiArgs都是由我们控制传入的话,是不是就可以像之前一样弹出计算器,很巧的是,在InvokerTransformer的构造方法中,可以传入其中的三个参数,transform的调用也可以传入1个,完美达成条件。我们来测试一下:

简单测试

我们可以看到,是可以的,于是我们尝试去找谁调用了transform这个方法。在org.apache.commons.collections.map ==> TransformedMap中找到3处:

1
2
3
4
5
6
7
8
9
10
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
TransformedMap

我们来仔细看下这个类。TransformedMap类实现了Map类的扩展,可以将两个实现了Transformer对象进行转换。首先是调用decorate方法初始化,decorate会调用构造方法进行实例化,对外暴露2个方法,putputall分别用于传入一个或多个。如果原始不为null,则会调用this.[]Transformer.transform(object).

再来看下其他的两个类:
ConstantTransformer

ConstantTransformer的构造方法返回传入的值到iConstant

ChainedTransformer

ChainedTransformer这个类的构造方法传入多个transformer且其transform可以触发每个元素的transform方法

transform-chain

我们来看这个transform-chain,我们构造了一个Transformer[],其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[0]
iConstant = Runtime.class
[1]
iMethodName = "getMethod"
iParamTypes = String.class
iArgs = "getRuntime"
[2]
iMethodName = "invoke"
iParamTypes = Object.class
iArgs = "getRuntime"
[3]
iMethodName = "exec"
iParamTypes = String.class
iArgs = "open /Applications/Calculator.app/"

现在我们构造了一个chain,但是缺少一个触发chain.transform的途径从而往下传递触发。而我们在之前看到TransformedMapcheckSetValue可以触发transform方法,但checkSetValue无法直接调用,我们进一步看:

checksetvalue

发现MapEntry可以直接调用setValue方法触发checkSetValue 所以整个的POC如下:

poc

即构造了个TransformedMap然后对其第一个元素进行赋值。调用了setValue方法,触发了checkSetValue从而调用了ChainedTransformertransform方法,从而对内部的4个Transform分别调用transform,从而完成了命令执行,但我们对细节还是一知半解,我们通过debug来一步步看下他执行的流程.

  1. 首先是transformers 的定义,为4个Transformer的数组,然后新建hashmap对象,并写入一个kv值
  2. 之后调用TransformedMap.decorate生成一个outerMap对象,outerMap.map是之前的hashmap, outerMap.keyTransformernull,keyTransformer.valueTransformer是之前的chainedTransform
  3. elEntry 指向Map的第一个kv对
  4. 重点来了
workflow

之后我们看到,调用setValue会调用parent.checkSetValue,会返回valueTransformer.transform,而valueTransformer是之前的chainedTransform从而对每一个Transform对象都进行transform

注意!ChainedTransformertransform是链式的(这一点我到这里才注意到,也才想通整个过程),即前一个的结果会作为参数传到下一个的transform方法中object = iTransformers[i].transform(object);

  1. 第一个ConstantTransformertransform会返回java.lang.Runtime
  2. 第二个InvokerTransformer之后返回Runtime.getRuntime()对象
  3. 第三个之后返回Runtime()对象
  4. 第三个则返回Runtime.getRuntime().getMethod("exec", String.class).invoke(,"open.....")最终执行命令

以上就是核心的原理,那我我们如何利用呢?参考之前的的漏洞原理,我们需要找到一个接收Map类对象反序列化,且在readObject方法时调用Map对象的Entry的setValue方法.

所利用的类是AnnotationInvocationHandler,这个类无法直接访问(。。。一直以为是jdk的问题删掉了…哭晕),可以直接去github上看源码,这里提供一个镜像链接。我们把它download下来看。

AnnotationInvocationHandler

我们可以看到,当反序列化AnnotationInvocationHandler类时,其memberValues为我们传入的Map对象,在readObject会调用Map.EntrysetValue函数,从而触发漏洞:

payload

完整payload如图,但这里本地无法成功,具体原因往下看。到这里我发现运行失败,而且无法进入AnnotationInvocationHandler里面单步调试,一开始我以为是oracle jdkopenjdk区别,下载了很多版本在idea中可以用Class.forName()调用但是就是无法import,也无法进入调试。由于不知道为什么错误也看不见具体逻辑,一度陷入僵局,而且网站的分析基本到给出payload就结束了。

于是采用一个骚操作,由于我们在镜像链接上可以看到,/sun/reflect/annotation/中有11个类,而我们在idea import sun.reflect.annotation中只能看到9个,唯独少了AnnotationInvocationHandlerAnnotationTypeMismatchExceptionProxy,且后者在前者中使用了,于是我打算把这两个类下载下来,作为我们本地类,这样就可以单步调试了。

AnnotationInvocationHandler

注意,ExceptionProxy这个类只能在内部使用,我们把它注释掉就好,没有影响。一步步调试,一切如我们预期,在反序列化中,调用了AnnotationInvocationHandler类的readObject方法,一直到for循环中,如下图,在Class<?> memberType = memberTypes.get(name);中取得的值是value而我们之前HashMap的key是name导致不等没有进入setValue的过程。

AnnotationInvocationHandler

这下就明白了,所以我们只需要更改innerMap.put("value", "hello");,保证key是value即可成功执行命令:

success.png

当然代码可以改回sun.reflect.AnnotationInvocationHandler

我们再看下,为什么必须是valueannotationtype是注解类型,annotationType = AnnotationType.getInstance(type);的作用是返回@Target注解的内容,其memberTypes的key值为value,所以传入的内容也需要为value.

annotationtype

那么可以合理推测,我们将原始payload改成其他类型的注解,应该由于其memberTypes应该是表明其注解类型,应该也是value->...的Map形式,应该也可以成功执行,尝试后发现,符合我们预期,这样整个原理就算非常透彻的理解了。

try

漏洞修复

Apache Commons Collections在3.2.2版本中做了一定的安全处理,对这些不安全的Java类的序列化支持增加了开关,默认为关闭状态。涉及的类包括CloneTransformer,ForClosure, InstantiateFactory, InstantiateTransformer, InvokerTransformer, PrototypeCloneFactory,PrototypeSerializationFactory, WhileClosure。

我们可以看到,官方对于这种反序列化问题,往往采取的是黑名单,这也是之后为什么漏洞被频繁各种绕过,由于Apache Commons Collections当时存在于各种web server或容器中,导致当时各种组件被攻击。