news 2026/9/3 7:36:57

Java 反序列化利用链深度剖析:CC / CB / ROME 三链对比与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java 反序列化利用链深度剖析:CC / CB / ROME 三链对比与实战

一、什么是反序列化利用链(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 );

注意两个约束:

  1. 恶意类必须继承 AbstractTranslet,否则 TemplatesImpl 拒绝加载;
  2. 必须实现它的两个抽象 transform() 方法,否则无法实例化。
3.5 构造关键:安全属性占位

与 CC 链类似,构造阶段也要防止提前触发。CB 链的技巧是(见 generateCBPayload):

  1. 先用安全属性 "lowestSetBit"(BigInteger 的普通 int 属性)构造 BeanComparator;
  2. 放入两个 BigInteger,此时 add() 触发的比较完全无害;
  3. 反射把 property 改为 "outputProperties",并把内部数组替换为 TemplatesImpl;
  4. 序列化——一切恶意状态只存在于字节流中。

四、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 的防提前触发策略更精细:

  1. 先把 TemplatesImpl._class 置为 null,防止构造阶段意外加载;
  2. 用 ToStringBean(Templates.class, templates) 包装,注意用 Templates 接口作为 beanClass,限定 getter 搜索范围(避免内省 TemplatesImpl 的其他危险方法导致异常);
  3. ObjectBean 先用无害的 String 构造并放入 HashMap,此时 put() 触发的 hashCode() 完全安全;
  4. 最后反射替换 ObjectBean 内部的 _equalsBean 和 _toStringBean,完成"装弹"。
4.4 ROME 链的独特价值
  • 完全独立:不依赖 CC、CB 的任何类,可绕过针对它们的黑名单;
  • 可复用组件:EqualsBean / ObjectBean 可以为任何需要 hashCode 触发点的链提供入口,是构造新链的"万能钥匙"。

五、三链横向对比

维度CC 链(CC5)CB 链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 8+(CC1 限 JDK 8)全版本全版本
黑名单规避特征明显,易被检测可绕过 CC 黑名单独立于 CC/CB
构造难点反射替换 Transformer 数组安全属性占位 + 数组替换双层偷换 + _class 置空

共性总结
三条链的构造都遵循同一套方法论:

  1. 找到自动触发点:利用集合类反序列化时必然调用的 hashCode() / compare();
  2. 寻找危险中转类:反射调用(CC)、属性读取(CB)、getter 遍历(ROME);
  3. 落到执行 Sink:直接命令执行或字节码加载;
  4. 惰性装弹:构造阶段保持对象图无害,序列化前最后一刻通过反射注入恶意状态——这是所有 payload 生成器的通用防御"自爆"的模式。

六、防御建议

  1. 不反序列化不可信数据:这是根本解法。优先改用 JSON 等不含类型信息的数据格式;
  2. JEP 290 / ObjectInputFilter:JDK 9+ 提供的反序列化过滤器,配置类白名单/黑名单;
  3. 升级组件:commons-collections ≥ 3.2.2 / 4.1、commons-beanutils ≥ 1.9.4、关注 ROME 安全公告;
  4. 移除冗余依赖:ROME、Xalan 等库常作为传递依赖引入,定期审计 mvn dependency:tree;
  5. 纵深检测:黑名单不能只盯 CC 类——CB、ROME 链完全独立,BeanComparator、ToStringBean、TemplatesImpl、PriorityQueue 等都应纳入监控;
  6. 运行时防护:RASP 对 Runtime.exec()、defineClass() 等敏感调用的来源堆栈做校验。

七、结语

CC、CB、ROME 三条链展示了一个深刻的安全教训:漏洞往往不在某一个类,而在"类的组合"。每一个单独的组件(LazyMap 的惰性、BeanComparator 的灵活、ToStringBean 的便利)在其设计语境下都合理且有用,但当它们与反序列化的自动调用机制相遇时,就拼成了致命的执行路径。
这也是 Java 反序列化防御如此困难的根源——攻击面是整个 classpath 上所有 Serializable 类的组合空间。理解这些经典链条的构造思路,是做好检测与防御的第一步。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 7:36:01

白噪音睡眠音箱拆解:低压低功耗霍尔开关的设计必要性

白噪音睡眠音箱实现“放回底座即休眠、拿起即播放”的无感操作,核心硬件就是一颗霍尔开关。它在主板贴装霍尔传感器,配合底座或机身内的磁铁,通过磁场变化输出电平信号给MCU,从而控制整机休眠与唤醒,全程无物理接触、无…

作者头像 李华
网站建设 2026/9/3 7:35:01

昇腾AI芯片高性能算子开发实战:CSR-Block稀疏张量压缩检索优化

简介:本资源是昇腾AI原生平台创新算子挑战赛(S1赛季)‘三人行队’的完整参赛作品源码包,面向AI底层开发工程师、高校算法加速研究者及昇腾生态开发者,聚焦自定义算子设计与性能优化实践。包内共729个文件,总…

作者头像 李华
网站建设 2026/9/3 7:34:38

STM32驱动TM1628/TM1640:从时序解析到健壮驱动设计实践

简介:本资源是一套面向嵌入式初学者与STM32开发者的TM1628/TM1640显示与按键驱动工程,专为解决LED点阵动态显示及多键实时响应这一典型人机交互需求而设计。压缩包共2个文件(1个.h头文件、1个.c源文件),总大小仅4KB&am…

作者头像 李华
网站建设 2026/9/3 7:34:21

ARM设备运行Steam游戏实战:Bionic NM与兼容层配置指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 7:33:17

STM32F407 DCMI图像链路实战:从-8005错误到稳定显示

简介:本资源是面向STM32F407嵌入式开发者的DCMI数字相机接口实战代码包,聚焦图像采集系统中DCMI与DMA协同工作的核心难点,尤其适用于需实现高速、低CPU占用图像捕获的视觉类项目(如智能摄像头、工业检测终端)。压缩包仅…

作者头像 李华
网站建设 2026/9/3 7:32:08

大模型时代人才价值重估:从“林俊旸现象”看AI创投信号

最近AI创投圈有一个讨论度挺高的话题,媒体和分析师把它称为“林俊旸现象”。多数人第一次听到这个名字,可能来自DeepSeek-V3、DeepSeek-R1等模型发布时的技术报告。真正让我觉得值得研究的,不是某位研究者的个人动向,而是整个创投…

作者头像 李华