一、什么是反序列化利用链(Gadget Chain)
Java 原生反序列化通过 ObjectInputStream.readObject() 将字节流还原为对象图。在还原过程中,JDK 会自动调用某些类的特殊方法(如 readObject()、hashCode()、equals()、finalize() 等)。
如果 classpath 中恰好存在一些"危险类",它们的这些自动调用方法内部包含反射调用、动态类加载、命令执行等能力,攻击者就可以精心构造一个对象图,让这些方法在反序列化时按预定顺序串联触发,最终实现远程代码执行(RCE)。
这条从"反序列化入口"到"命令执行点"的调用路径,就叫 Gadget Chain(利用链)。
一条完整的利用链通常由三部分组成:
| 角色 | 职责 | 典型代表 |
| 入口(Source) | 反序列化时自动触发的方法 | HashMap.readObject()、PriorityQueue.readObject() |
| 桥梁(Gadget) | 把入口和执行点连接起来的中间类 | LazyMap、BeanComparator、ToStringBean |
| 执行点(Sink) | 最终实现代码执行的方法 | Runtime.exec()、TemplatesImpl 字节码加载 |
下面逐一剖析三条经典利用链。
二、CC 链:Commons Collections 的 Transformer 反射链
2.1 核心危险组件
Apache Commons Collections 3.x 提供了一组 Transformer 接口实现,本意是用于集合数据的惰性转换,但其中三个类组合起来足以致命:
- InvokerTransformer:transform() 内部通过反射调用任意对象的任意方法;
- ChainedTransformer:把多个 Transformer 串联,前一个的输出作为后一个的输入;
- ConstantTransformer:恒定返回一个预设对象,常用作链的起点。
三者串联后可以拼出经典的四步命令执行链:
ConstantTransformer(Runtime.class) // 提供起点 → InvokerTransformer("getMethod", ...) // 拿到 getRuntime 方法 → InvokerTransformer("invoke", ...) // 调用得到 Runtime 实例 → InvokerTransformer("exec", command) // 执行系统命令2.2 触发机制:LazyMap + hashCode
LazyMap 的特性是:get(key) 找不到 key 时,会调用装饰的 Transformer 生成值并放入 Map。攻击者把恶意 ChainedTransformer 注入 LazyMap,只要 get() 被触发,命令就会执行。
剩下的问题就是:如何让反序列化过程自动触发 get()?
本项目采用的是 CC5 变体(经典 CC1 依赖的 AnnotationInvocationHandler 在 JDK 9+ 中实现已变更,会抛出 IncompleteAnnotationException,故不再适用):
ObjectInputStream.readObject() └─ HashMap.readObject() // 入口:重建时对每个 key 调 hash() └─ TiedMapEntry.hashCode() // key 是 TiedMapEntry └─ TiedMapEntry.getValue() └─ LazyMap.get("foo") // key 不存在 → 触发 transform └─ ChainedTransformer.transform() └─ ... → Runtime.exec()TiedMapEntry 绑定了 LazyMap 和一个不存在的 key,它的 hashCode() 内部会调用 getValue() → LazyMap.get(),从而把 HashMap 的 hashCode 触发点和 LazyMap 连接起来。
2.3 构造关键:惰性"装弹"
构造 payload 时有一个极易踩坑的细节:序列化过程本身也会调用 hashCode()。如果恶意链在序列化时就已就位,writeObject() 阶段就会提前引爆命令。
因此代码采用了"先装空弹、后换实弹"的技巧(见 generateCC5Payload):
先用无害的 ChainedTransformer(ConstantTransformer(1)) 装饰 LazyMap;
完成 HashMap.put()、序列化结构搭建;
最后通过反射替换 ChainedTransformer.iTransformers 字段,注入真实恶意链;
此时再序列化,恶意链只存在于字节流中,构造阶段不会触发。
2.4 版本与限制
仅适用于 commons-collections 3.1 ~ 3.2.1(3.2.2 起对 InvokerTransformer 等类增加了反序列化白名单校验);
CC5 变体兼容 JDK 8+;
缺陷:依赖 CC 特征类,容易被黑名单/WAF 识别。
三、CB 链:Commons Beanutils 的属性比较链
3.1 与 CC 链的本质区别
CB 链完全不依赖 Commons Collections,它利用的是 commons-beanutils 中的 BeanComparator。这意味着:即使目标环境屏蔽了所有 CC 类,CB 链依然可用。
3.2 触发桥梁:BeanComparator
BeanComparator 是一个比较器:compare(o1, o2) 时通过 PropertyUtils.getProperty(obj, property) 读取两个对象的同名属性值再比较。
关键利用点:如果把属性名设为 "outputProperties",把比较对象换成 TemplatesImpl,属性读取就会变成调用 getOutputProperties() —— 而该方法内部会触发恶意字节码加载。
3.3 入口:PriorityQueue 的堆化过程
PriorityQueue 反序列化时会调用 heapify() 重建堆,对元素两两调用 comparator.compare()。于是完整调用栈为:
ObjectInputStream.readObject() └─ PriorityQueue.readObject() └─ heapify() → siftDown() └─ BeanComparator.compare() └─ PropertyUtils.getProperty(obj, "outputProperties") └─ TemplatesImpl.getOutputProperties() └─ newTransformer() → defineTransletClasses() └─ ClassLoader.defineClass() // 加载恶意字节码 └─ 静态初始化块 → Runtime.exec()3.4 执行点:TemplatesImpl 字节码加载
TemplatesImpl 是 Xalan XSLT 编译器的模板实现类。当它的 _bytecodes 字段包含恶意字节码、且触发 getOutputProperties() / newTransformer() 时,会通过 defineClass() 把字节码定义成新类,类的静态初始化块在加载时自动执行。
恶意类的生成通常借助 Javassist(见 createEvilTemplatesImpl):
CtClass ctClass = pool.makeClass("EvilTranslet"); ctClass.setSuperclass(pool.get("...AbstractTranslet")); // 必须继承 ctClass.makeClassInitializer().insertBefore( "java.lang.Runtime.getRuntime().exec(\"calc.exe\");" // 静态块 = RCE );注意两个约束:
- 恶意类必须继承 AbstractTranslet,否则 TemplatesImpl 拒绝加载;
- 必须实现它的两个抽象 transform() 方法,否则无法实例化。
3.5 构造关键:安全属性占位
与 CC 链类似,构造阶段也要防止提前触发。CB 链的技巧是(见 generateCBPayload):
- 先用安全属性 "lowestSetBit"(BigInteger 的普通 int 属性)构造 BeanComparator;
- 放入两个 BigInteger,此时 add() 触发的比较完全无害;
- 反射把 property 改为 "outputProperties",并把内部数组替换为 TemplatesImpl;
- 序列化——一切恶意状态只存在于字节流中。
四、ROME 链:ToStringBean 的深度 toString 链
4.1 触发桥梁:ToStringBean 的"深度 toString"
ROME 是 RSS/Atom 订阅处理框架。其 ToStringBean 的设计目的是生成对象的完整字符串表示:通过 Java Beans 内省获取被包装对象的所有属性描述符,然后反射调用每一个无参 getter。
这个"遍历调用所有 getter"的行为正是危险所在:如果包装的是 TemplatesImpl,那么 getOutputProperties() 这个危险 getter 必然被调用,直接引爆字节码加载。
4.2 从 hashCode 到 toString:EqualsBean 桥接
HashMap 反序列化触发的是 hashCode(),而 ToStringBean 的触发点是 toString()。ROME 提供的 EqualsBean.beanHashCode() 恰好实现了这个转换:
// EqualsBean.beanHashCode() 的语义 return this._obj.toString().hashCode();ObjectBean 再把 hashCode() 委托给内部的 EqualsBean,最终形成完整链条:
HashMap.readObject() └─ hash(key) → ObjectBean.hashCode() └─ EqualsBean.beanHashCode() └─ ToStringBean.toString() └─ BeanIntrospector 内省 → 反射调用所有 getter └─ TemplatesImpl.getOutputProperties() └─ newTransformer() → defineClass() └─ 静态块 → Runtime.exec()4.3 构造关键:两次"偷梁换柱"
generateROMEPayload 的防提前触发策略更精细:
- 先把 TemplatesImpl._class 置为 null,防止构造阶段意外加载;
- 用 ToStringBean(Templates.class, templates) 包装,注意用 Templates 接口作为 beanClass,限定 getter 搜索范围(避免内省 TemplatesImpl 的其他危险方法导致异常);
- ObjectBean 先用无害的 String 构造并放入 HashMap,此时 put() 触发的 hashCode() 完全安全;
- 最后反射替换 ObjectBean 内部的 _equalsBean 和 _toStringBean,完成"装弹"。
4.4 ROME 链的独特价值
- 完全独立:不依赖 CC、CB 的任何类,可绕过针对它们的黑名单;
- 可复用组件:EqualsBean / ObjectBean 可以为任何需要 hashCode 触发点的链提供入口,是构造新链的"万能钥匙"。
五、三链横向对比
| 维度 | CC 链(CC5) | CB 链 | ROME 链 |
| 核心依赖 | commons-collections 3.x | commons-beanutils 1.x | rome 1.x |
| 触发组件 | InvokerTransformer + LazyMap | BeanComparator | ToStringBean + EqualsBean |
| 序列化入口 | HashMap + TiedMapEntry | PriorityQueue | HashMap + ObjectBean |
| 触发机制 | get 缺失时链式反射调用 | 堆化比较时读取属性 | toString 时遍历所有 getter |
| 执行方式 | 直接 Runtime.exec() | TemplatesImpl 字节码加载 | TemplatesImpl 字节码加载 |
| JDK 兼容 | JDK 8+(CC1 限 JDK 8) | 全版本 | 全版本 |
| 黑名单规避 | 特征明显,易被检测 | 可绕过 CC 黑名单 | 独立于 CC/CB |
| 构造难点 | 反射替换 Transformer 数组 | 安全属性占位 + 数组替换 | 双层偷换 + _class 置空 |
共性总结
三条链的构造都遵循同一套方法论:
- 找到自动触发点:利用集合类反序列化时必然调用的 hashCode() / compare();
- 寻找危险中转类:反射调用(CC)、属性读取(CB)、getter 遍历(ROME);
- 落到执行 Sink:直接命令执行或字节码加载;
- 惰性装弹:构造阶段保持对象图无害,序列化前最后一刻通过反射注入恶意状态——这是所有 payload 生成器的通用防御"自爆"的模式。
六、防御建议
- 不反序列化不可信数据:这是根本解法。优先改用 JSON 等不含类型信息的数据格式;
- JEP 290 / ObjectInputFilter:JDK 9+ 提供的反序列化过滤器,配置类白名单/黑名单;
- 升级组件:commons-collections ≥ 3.2.2 / 4.1、commons-beanutils ≥ 1.9.4、关注 ROME 安全公告;
- 移除冗余依赖:ROME、Xalan 等库常作为传递依赖引入,定期审计 mvn dependency:tree;
- 纵深检测:黑名单不能只盯 CC 类——CB、ROME 链完全独立,BeanComparator、ToStringBean、TemplatesImpl、PriorityQueue 等都应纳入监控;
- 运行时防护:RASP 对 Runtime.exec()、defineClass() 等敏感调用的来源堆栈做校验。
七、结语
CC、CB、ROME 三条链展示了一个深刻的安全教训:漏洞往往不在某一个类,而在"类的组合"。每一个单独的组件(LazyMap 的惰性、BeanComparator 的灵活、ToStringBean 的便利)在其设计语境下都合理且有用,但当它们与反序列化的自动调用机制相遇时,就拼成了致命的执行路径。
这也是 Java 反序列化防御如此困难的根源——攻击面是整个 classpath 上所有 Serializable 类的组合空间。理解这些经典链条的构造思路,是做好检测与防御的第一步。