Hessian2 Gadget Chain — Dubbo 默认序列化的利用链
为什么单独分析 Hessian2?
Hessian2 是 Dubbo 的默认序列化方式
绝大多数 Dubbo 漏洞的利用都需要构造 Hessian2 格式的 payload
Hessian2 的反序列化机制与 Java 原生完全不同,可用的 Gadget Chain 也不同
不理解 Hessian2,就无法理解 Dubbo 漏洞
Hessian2 反序列化机制
与 Java 原生的核心区别
| 对比项 | Java ObjectInputStream | Hessian2 |
|---|---|---|
| 格式 | Java 私有二进制格式 | Hessian2 协议(跨语言) |
| magic | aced 0005 |
无固定 magic |
| 入口方法 | readObject() |
不调用 readObject() |
| 对象重建 | 通过 readObject()/readResolve() |
通过 MapDeserializer/JavaDeserializer |
| 触发点 | readObject() → 任意逻辑 |
HashMap.put() → hashCode()/equals() |
| Gadget 入口 | readObject | hashCode / equals / compareTo / toString |
Hessian2 如何反序列化对象
1 | Hessian2Input.readObject() |
四个危险触发点
1. hashCode() — HashMap 反序列化时
1 | // Hessian2 的 MapDeserializer |
2. equals() — HashMap 键冲突时
当两个 key 的 hashCode 相同但不是同一对象时,调用 equals() 比较
3. compareTo() — TreeMap 反序列化时
1 | // TreeMap.put() 内部: |
4. toString() — 异常处理时
当 Hessian2 反序列化过程中抛异常,日志记录可能调用对象的 toString()
CVE-2021-43297 就利用了这一点
Hessian2 可用的 Gadget Chain
1. ROME 链(最常用)
调用链
1 | Hessian2Input.readObject() |
关键点
HashMap.put() 调用 hashCode() — 这在 Hessian2 中也会发生
ROME 链的入口正好是 hashCode(),完美匹配
CVE-2020-1948 和 CVE-2020-11995 的主要利用链
marshalsec 生成
1 | java -cp marshalsec.jar marshalsec.Hessian2 Rome "touch /tmp/pwned" |
2. Spring AOP 链
SpringPartiallyComparableAdvisorHolder
1 | HashMap.put() |
触发路径:hashCode → equals → toString → JNDI
需要 spring-aop 在 classpath(Dubbo 项目基本都有)
SpringAbstractBeanFactoryPointcutAdvisor
1 | HashMap.put() |
marshalsec 生成
1 | java -cp marshalsec.jar marshalsec.Hessian2 SpringPartiallyComparableAdvisorHolder \ |
3. Resin/Quercus 链
前置条件
依赖:com.caucho:quercus(Caucho 的 PHP-on-Java 实现)
实际场景中较少见
调用链
1 | HashMap.put() |
4. XBean 链
前置条件
依赖:org.apache.xbean:xbean-naming
调用链
1 | Hessian2 JavaDeserializer |
特殊性
不通过 hashCode,而是通过 setter 方法调用
Hessian2 的 JavaDeserializer 在设置字段时会调用 setter
CVE-2021-32824(Telnet PojoUtils)利用了 XBean 链
5. Groovy 链
前置条件
依赖:groovy
调用链
1 | HashMap.put() |
Hessian2 Gadget Chain 总览
| 链名 | 入口 | 中间跳板 | Sink | 依赖 | 实际可用性 |
|---|---|---|---|---|---|
| ROME | hashCode | toString → getter | TemplatesImpl | rome | 高(常见依赖) |
| Spring AOP(1) | hashCode→equals | toString → getBean | JNDI | spring-aop | 极高(几乎必有) |
| Spring AOP(2) | hashCode | getAdvice → getBean | JNDI | spring-aop | 极高 |
| Resin | hashCode | QName | ClassLoader | quercus | 低 |
| XBean | setter | setAsText | JNDI | xbean-naming | 中 |
| Groovy | hashCode | Closure.call | Runtime.exec | groovy | 中 |
如何判断目标可用哪条链?
信息收集思路
- 探测目标 classpath 中有哪些 jar 包
如果能获取到 POM 文件或 lib 目录 → 直接看
通过报错信息推测
- 常见场景
只有 Spring → Spring AOP 链
有 rome → ROME 链(优先,不需要 JNDI 出网)
有 xbean-naming → XBean 链
纯 Dubbo 无其他依赖 → 较难利用
JNDI vs TemplatesImpl
| 方式 | 优点 | 缺点 |
|---|---|---|
| TemplatesImpl | 不需要出网 | 需要 ROME/CB 链 |
| JNDI | 依赖更常见(Spring) | 需要目标能访问攻击者的 LDAP 服务 |
高版本 JDK(8u191+)限制了 JNDI 远程类加载,需要配合 bypass 技术
实战:构造 Hessian2 ROME Payload
方法一:使用 marshalsec
1 | 生成 ROME 链的 Hessian2 payload |
方法二:使用 dubbo-exp
1 | 直接发送 Hessian2 payload 到 Dubbo 端口 |
方法三:Python 手工构造(后续 PoC 中使用)
需要理解 Hessian2 的二进制格式
在 exploits/utils/hessian2_payload.py 中封装
Hessian2 安全防护
Dubbo 的黑白名单机制
Dubbo 2.7.8+ 引入了 Hessian2 类型过滤
三种模式:
STRICT(严格模式,Dubbo 3.2+ 默认)— 只允许白名单中的类
WARN(警告模式,Dubbo 3.1 默认)— 拦截黑名单,记录日志
DISABLE(禁用)— 不检查,危险配置
绕过黑白名单的思路
CVE-2021-25641:篡改序列化 ID,绕过 Hessian2 的检查
CVE-2023-23638:通过泛化调用绕过检查逻辑
CVE-2023-46279:直接绕过黑名单匹配
这就是为什么 Dubbo 漏洞层出不穷 — 每次修补都可能被新方法绕过
动手练习
搭建一个 Dubbo 2.7.3 环境,分别测试 ROME 和 Spring AOP 链
对比 Java 原生反序列化和 Hessian2 反序列化的 payload 区别
用 ysoserial 生成的是 Java 原生格式
用 marshalsec 生成的是 Hessian2 格式
两者的二进制结构完全不同
思考:为什么 CC 链不能用于 Hessian2?因为 CC 链的入口是 readObject(),而 Hessian2 不调用 readObject()
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 02-经典Gadget-Chain详解 | Dubbo漏洞 | 04-Dubbo架构与协议分析 |