Dubbo漏洞 - 17 漏洞演进与攻防对抗史

Dubbo 漏洞演进与攻防对抗史

为什么需要这篇笔记

单独看每个 CVE 只能看到一个点

把所有 CVE 串起来看,才能理解 Dubbo 安全攻防的完整脉络

这是安全研究的核心能力:看到漏洞之间的因果关系和演进趋势

时间线总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
2019 ─── CVE-2019-17564 (HTTP 反序列化)
│ 修复:移除 HttpInvokerServiceExporter

2020 ─── CVE-2020-1948 (Hessian2 反序列化) ← 第一次攻击默认配置
│ 修复:引入 Hessian2 黑白名单

├── CVE-2020-11995 (HashMap 嵌套绕过)
│ 修复:深度类型检查

2021 ─── CVE-2021-25641 (SerID 篡改) ← 协议层攻击
│ 修复:服务端决定序列化方式

├── CVE-2021-30179 (泛化调用 nativejava) ← 应用层绕过
│ 修复:禁用 nativejava

├── CVE-2021-30180 (YAML 注入) ← 配置中心攻击
├── CVE-2021-30181 (Nashorn 注入) ← 配置中心攻击
│ 修复:SafeConstructor + 移除脚本路由

├── CVE-2021-32824 (Telnet PojoUtils) ← 运维接口攻击
│ 修复:PojoUtils 白名单

└── CVE-2021-43297 (异常 toString) ← 异常路径攻击
修复:安全的日志记录

2023 ─── CVE-2023-23638 (泛化调用绕过) ← 再次绕过
│ 修复:全路径安全检查 + STRICT 模式

└── CVE-2023-29234 (恶意包解码) ← Dubbo 3.x
修复:异常路径安全检查

五条攻击主线

主线一:序列化方式的对抗

1
2
3
4
5
6
7
8
9
10
11
时间线:

[2019] HTTP 协议用 Java 原生序列化 → 全开放 → 直接打

[2020] Hessian2 默认序列化 → 无检查 → hashCode 触发链
↓ (修复:Hessian2 黑名单)
[2021] 攻击者篡改 SerID → 切换到 Java/Kryo → 绕过 Hessian2 保护
↓ (修复:服务端决定 SerID)
[2021] 通过泛化调用 attachment → generic=nativejava → 强制 Java 反序列化
↓ (修复:禁用 nativejava)
[2023] 通过 PojoUtils/SPI → 间接触发不安全反序列化

攻防本质:攻击者始终在寻找”让服务端使用不安全的反序列化方式”的途径

防御者始终在堵:每堵一个,攻击者就找新的路径

主线二:安全检查位置的对抗

1
2
3
4
[2020] 顶层类型检查 → HashMap key 绕过(CVE-2020-11995
[2021] 正常路径检查 → 异常路径绕过(CVE-2021-43297
[2023] 入口检查 → 中间处理绕过(CVE-2023-23638
[2023] 正常解码检查 → fallback 解码绕过(CVE-2023-29234

核心教训:安全检查必须覆盖所有代码路径,包括:

正常路径 + 异常路径

入口 + 中间处理 + 返回值处理

顶层对象 + 嵌套对象

主线三:配置中心作为跳板

1
2
[2021] ZooKeeper 无认证 → 写入恶意 YAML → SnakeYAML RCE
[2021] ZooKeeper 无认证 → 写入恶意 JS → Nashorn RCE

本质:从配置数据到代码执行的跨越

YAML 的 !! 标签 = 任意类实例化

Script 路由 = 直接执行代码

攻击面转移:不直接攻击 Dubbo Provider,而是攻击它信任的数据源

主线四:运维/调试接口的攻击

1
[2021] Telnet invoke → PojoUtils.realize() → JNDI 注入

Telnet 本来是运维调试用的

但它与 Dubbo 协议共用端口(20880)

无认证 + 可实例化任意类 = 完美攻击入口

主线五:Gadget Chain 的演进

1
2
3
4
5
6
7
8
9
10
11
Java 原生序列化 → CC/CB 全系列均可(CVE-2019-17564

Hessian2 序列化 → 只有 hashCode/toString 入口的链可用

ROME 链 (hashCode→toString→getter) → CVE-2020-1948 主力
Spring AOP 链 (hashCode→JNDI) → 几乎所有 Dubbo 项目可用
XBean 链 (setter→JNDI) → Telnet PojoUtils 场景

黑名单拦截已知链 → 攻击者寻找新的 gadget chain

STRICT 白名单 → 基本阻断(但可能有实现缺陷)

防御演进

时期 防御手段 被绕过方式
2020 前 无防御
2020 Hessian2 黑名单 HashMap 嵌套
2021 Q1 深度类型检查 SerID 篡改
2021 Q2 服务端决定 SerID 泛化调用 nativejava
2021 Q3 禁用 nativejava Telnet PojoUtils
2021 Q4 PojoUtils 白名单 异常路径 toString
2022 修复异常处理
2023 GenericFilter 加强 PojoUtils/SPI 变体
2023+ STRICT 白名单模式 尚未发现公开绕过

趋势:从黑名单 → 白名单,从局部检查 → 全路径检查

STRICT 模式是目前最有效的防御,但需要正确配置

漏洞发现者的思维模式

安全研究者是如何找到这些绕过的?

思维一:检查遗漏

“修复加在了 Hessian2 上,那 Java/Kryo/FST 呢?” → CVE-2021-25641

“检查了正常路径,那异常路径呢?” → CVE-2021-43297

“检查了入口,那中间处理呢?” → CVE-2023-23638

思维二:控制流劫持

“客户端能控制哪些参数?”

SerID 字段 → CVE-2021-25641

generic attachment → CVE-2021-30179

serialization attachment → CVE-2023-23638

思维三:攻击面转移

“直接打 RPC 被拦了,那配置中心呢?” → CVE-2021-30180/30181

“RPC 被拦了,那 Telnet 呢?” → CVE-2021-32824

思维四:触发点多样性

readObject → hashCode → toString → equals → compareTo → setter

每种触发点对应不同的 gadget chain 和攻击场景

对安全学习者的启示

学习漏洞分析的方法论

  1. 不要只看单个 CVE:要理解它在整个攻防链中的位置

  2. 关注修复方式:修复方式决定了下一个漏洞可能出现在哪里

  3. 画攻击面地图:列出所有输入点、处理路径、序列化/反序列化位置

  4. **思考”修了什么,没修什么”**:每次修复都可能留下新的攻击面

  5. 自动化 + 手工:用 GadgetInspector 发现链,手工分析业务逻辑绕过

Dubbo 安全审计检查思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. 找到所有反序列化入口
- DubboCodec.decodeBody()
- GenericFilter.invoke()
- PojoUtils.realize()
- Telnet Handler
- YAML/Script 解析

2. 对每个入口检查
- 是否有类型过滤?
- 过滤是在入口还是深层?
- 异常路径是否也有过滤?
- 客户端能否控制使用哪个反序列化器?

3. 检查 classpath 依赖
- 有哪些 gadget chain 可用?
- 是否有不必要的危险依赖?

4. 检查基础设施
- 配置中心是否有认证?
- Dubbo 端口是否暴露?
- Telnet 是否禁用?

推荐阅读

GitHub Blog: “Apache Dubbo: All roads lead to RCE” — 最佳的 Dubbo 漏洞综合分析

GitHub Security Lab: GHSL-2021-034~043 — 2021 年多个 CVE 的原始发现报告

Checkmarx: “The 0xDABB of Doom” — CVE-2021-25641 的精彩分析

SonarSource: “Apache Dubbo Consumer Risks” — 从 Consumer 视角分析风险


上一章 目录 下一章
16-安全防御与加固 Dubbo漏洞