
一、什么是反序列化利用链Gadget ChainJava 原生反序列化通过 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 接口实现本意是用于集合数据的惰性转换但其中三个类组合起来足以致命InvokerTransformertransform() 内部通过反射调用任意对象的任意方法ChainedTransformer把多个 Transformer 串联前一个的输出作为后一个的输入ConstantTransformer恒定返回一个预设对象常用作链的起点。三者串联后可以拼出经典的四步命令执行链ConstantTransformer(Runtime.class) // 提供起点 → InvokerTransformer(getMethod, ...) // 拿到 getRuntime 方法 → InvokerTransformer(invoke, ...) // 调用得到 Runtime 实例 → InvokerTransformer(exec, command) // 执行系统命令2.2 触发机制LazyMap hashCodeLazyMap 的特性是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.13.2.2 起对 InvokerTransformer 等类增加了反序列化白名单校验CC5 变体兼容 JDK 8缺陷依赖 CC 特征类容易被黑名单/WAF 识别。三、CB 链Commons Beanutils 的属性比较链3.1 与 CC 链的本质区别CB 链完全不依赖 Commons Collections它利用的是 commons-beanutils 中的 BeanComparator。这意味着即使目标环境屏蔽了所有 CC 类CB 链依然可用。3.2 触发桥梁BeanComparatorBeanComparator 是一个比较器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见 createEvilTemplatesImplCtClass 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先用安全属性 lowestSetBitBigInteger 的普通 int 属性构造 BeanComparator放入两个 BigInteger此时 add() 触发的比较完全无害反射把 property 改为 outputProperties并把内部数组替换为 TemplatesImpl序列化——一切恶意状态只存在于字节流中。四、ROME 链ToStringBean 的深度 toString 链4.1 触发桥梁ToStringBean 的深度 toStringROME 是 RSS/Atom 订阅处理框架。其 ToStringBean 的设计目的是生成对象的完整字符串表示通过 Java Beans 内省获取被包装对象的所有属性描述符然后反射调用每一个无参 getter。这个遍历调用所有 getter的行为正是危险所在如果包装的是 TemplatesImpl那么 getOutputProperties() 这个危险 getter 必然被调用直接引爆字节码加载。4.2 从 hashCode 到 toStringEqualsBean 桥接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 链CC5CB 链ROME 链核心依赖commons-collections 3.xcommons-beanutils 1.xrome 1.x触发组件InvokerTransformer LazyMapBeanComparatorToStringBean EqualsBean序列化入口HashMap TiedMapEntryPriorityQueueHashMap ObjectBean触发机制get 缺失时链式反射调用堆化比较时读取属性toString 时遍历所有 getter执行方式直接 Runtime.exec()TemplatesImpl 字节码加载TemplatesImpl 字节码加载JDK 兼容JDK 8CC1 限 JDK 8全版本全版本黑名单规避特征明显易被检测可绕过 CC 黑名单独立于 CC/CB构造难点反射替换 Transformer 数组安全属性占位 数组替换双层偷换 _class 置空共性总结三条链的构造都遵循同一套方法论找到自动触发点利用集合类反序列化时必然调用的 hashCode() / compare()寻找危险中转类反射调用CC、属性读取CB、getter 遍历ROME落到执行 Sink直接命令执行或字节码加载惰性装弹构造阶段保持对象图无害序列化前最后一刻通过反射注入恶意状态——这是所有 payload 生成器的通用防御自爆的模式。六、防御建议不反序列化不可信数据这是根本解法。优先改用 JSON 等不含类型信息的数据格式JEP 290 / ObjectInputFilterJDK 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 类的组合空间。理解这些经典链条的构造思路是做好检测与防御的第一步。