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漏洞