Dubbo漏洞 - 04 Dubbo架构与协议分析

Dubbo 架构与协议分析 — 理解攻击面

Dubbo 是什么

Apache Dubbo 是一个高性能的 Java RPC 框架

核心功能:让一台机器上的 Java 程序可以像调用本地方法一样调用另一台机器上的方法

广泛用于微服务架构,阿里巴巴内部大规模使用

架构模型

三个核心角色

1
2
3
4
5
6
7
8
9
10
11
┌────────────┐     ② 订阅服务      ┌──────────────┐
│ Consumer │ ←─────────────────── │ Registry │
│ (调用方) │ │ (注册中心) │
└─────┬──────┘ ① 注册服务 └──────┬───────┘
│ ─────────────────→ │
│ ③ RPC 调用 │
▼ │
┌────────────┐ │
│ Provider │ ─────────────────────────────┘
│ (服务方) │ ① 注册服务地址
└────────────┘

Provider(服务方):暴露服务,监听端口(默认 20880),处理 RPC 请求

Consumer(调用方):调用远程服务,从 Registry 获取 Provider 地址

Registry(注册中心):服务注册与发现,常用 ZooKeeper、Nacos

RPC 调用流程

  1. Provider 启动,将服务地址注册到 Registry

  2. Consumer 启动,从 Registry 订阅服务列表

  3. Consumer 根据负载均衡选择一个 Provider

  4. Consumer 将方法名、参数序列化,通过网络发送到 Provider

  5. Provider 反序列化请求,执行业务方法 ← 漏洞发生在这里

  6. Provider 将结果序列化返回给 Consumer

安全启示

如果攻击者能直接连接 Provider 端口(20880),就可以发送恶意 RPC 请求

如果攻击者能访问 Registry(ZooKeeper:2181),可以投毒路由规则

Provider 端口通常没有认证机制(早期版本)

分层架构

Dubbo 内部分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
┌──────────────────────────────────┐
│ Service Layer (业务代码)
├──────────────────────────────────┤
│ Config Layer (配置)
├──────────────────────────────────┤
│ Proxy Layer (代理,透明RPC)
├──────────────────────────────────┤
│ Registry Layer (服务注册发现)
├──────────────────────────────────┤
│ Cluster Layer (路由、负载均衡) │ ← CVE-2021-30180/30181 YAML/脚本注入
├──────────────────────────────────┤
│ Monitor Layer (监控)
├──────────────────────────────────┤
│ Protocol Layer (协议) │ ← CVE-2021-30179 泛化调用
├──────────────────────────────────┤
│ Exchange Layer (信息交换) │ ← 反序列化发生在这里!
├──────────────────────────────────┤
│ Transport Layer (网络传输) │ ← TCP/HTTP 连接
├──────────────────────────────────┤
│ Serialize Layer (序列化) │ ← Hessian2/Java/Kryo/FST
└──────────────────────────────────┘

关键漏洞层

Exchange LayerExchangeCodec.decode() → 解码网络数据 → 调用序列化层反序列化

Protocol LayerGenericFilter 处理泛化调用 → 可能绕过类型检查

Cluster Layer:路由规则解析 → YAML/脚本注入

Dubbo 协议二进制格式(核心重点)

概览

Dubbo 协议是一个自定义的二进制协议

每个请求/响应包含:16 字节固定 Header + 变长 Body

默认端口:20880

Header 结构(16字节)

1
2
3
4
Byte:  0    1    2    3    4    5    6    7    8    9   10   11   12   13   14   15
┌────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┬────┐
│ DA │ BB │Flag│Stat│ Request ID (8 bytes) │ Data Length (4B) │
└────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┴────┘

逐字段解析

Byte 0-1:Magic Number(魔数)

固定值:0xDA 0xBB

用于识别 Dubbo 协议数据包

安全意义:抓包看到 dabb 就知道是 Dubbo 协议

Byte 2:Flags(标志字节)CVE-2021-25641 攻击的关键字节

1
2
3
4
Bit:  7    6    5    4    3    2    1    0
┌────┬────┬────┬────┬────┬────┬────┬────┐
│Req │2Way│Evnt│ Serialization ID │
└────┴────┴────┴────┴────┴────┴────┴────┘

Bit 7(最高位):Request/Response 标志

1 = 请求(Request)

0 = 响应(Response)

Bit 6:Two-way 标志

1 = 需要响应

0 = 不需要响应(OneWay)

Bit 5:Event 标志

1 = 心跳/事件消息

0 = 正常 RPC 消息

Bit 0-4:Serialization ID(5位,0-31)关键!

序列化方式 安全性
2 Hessian2 中(默认,有黑白名单)
3 Java 原生 极低(完整 gadget 支持)
4 Kryo 低(无安全过滤)
5 FST 低(无安全过滤)
6 Fastjson
7 Protobuf

CVE-2021-25641 攻击方式:篡改这 5 bit,将 Hessian2(00010) 改为 Java(00011)

篡改后,服务端用 Java 原生反序列化处理数据 → 所有 ysoserial 链均可利用

Byte 3:Status(状态码)

仅在 Response 中有效

20 = OK, 30 = CLIENT_TIMEOUT, 40 = BAD_REQUEST, 50 = BAD_RESPONSE

Byte 4-11:Request ID(8字节 long)

请求的唯一标识,用于匹配请求和响应

Byte 12-15:Data Length(4字节 int,大端序)

Body 部分的字节长度

构造一个请求包的 Python 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import struct

# Header
magic = b'\xda\xbb'
# Flags: Request(1) + TwoWay(1) + NotEvent(0) + Hessian2(00010) = 11000010 = 0xC2
flags = 0xC2
status = 0x00 # 请求中不使用
request_id = 1 # 8字节

body = b'...' # 序列化后的 RPC 数据
data_length = len(body)

header = magic + struct.pack('!BBqI', flags, status, request_id, data_length)
# ! = 大端序
# B = unsigned byte (flags)
# B = unsigned byte (status)
# q = signed long long (request_id, 8字节)
# I = unsigned int (data_length, 4字节)

packet = header + body

如果要篡改序列化方式为 Java 原生

1
2
3
# 将 Hessian2(0xC2) 改为 Java 原生
# 0xC2 = 1100 0010 → 改低5位为 00011 → 1100 0011 = 0xC3
flags = 0xC3 # Java 原生序列化

Body 格式(Hessian2 序列化)

Request Body 内容

Body 按以下顺序包含多个字段,每个字段用 Hessian2 编码:

顺序 内容 类型 示例
1 Dubbo 版本 String “2.7.3”
2 服务接口名 String “com.vuln.api.DemoService”
3 服务版本 String “0.0.0”
4 方法名 String “sayHello”
5 参数类型描述符 String “Ljava/lang/String;”
6 参数值 Object “world”
7 Attachments Map {“path”: “com.vuln.api.DemoService”}

攻击者的利用点

参数值:这是最直接的注入点,恶意对象作为参数被反序列化

服务名/方法名:可以是任意值,不需要真实存在

CVE-2020-1948:服务名和方法名都可以是垃圾值,只要参数包含恶意对象

Attachments:可以注入 generic=nativejava 等特殊参数

CVE-2021-30179:通过 attachment 指定泛化调用类型

泛化调用(GenericService)

什么是泛化调用

Dubbo 的泛化调用允许 Consumer 不依赖 Provider 的接口 JAR 就能调用服务

Consumer 使用 GenericService.$invoke(方法名, 参数类型, 参数值) 调用

Provider 默认启用泛化调用支持,无法关闭

安全隐患

攻击者不需要知道 Provider 暴露了哪些服务和方法

可以调用任意方法名

可以在 Attachment 中指定序列化方式(如 generic=nativejava

这是 CVE-2021-30179、CVE-2023-23638 的攻击入口

GenericFilter 处理流程

1
2
3
4
5
6
7
8
收到 RPC 请求
→ 检查 Attachment 中是否有 generic 参数
generic = "nativejava" → 使用 Java 原生反序列化处理参数
generic = "true" → 使用默认方式
generic = "bean" → 使用 JavaBeanDescriptor
→ GenericFilter.invoke()
→ 反序列化参数
→ 通过反射调用实际方法

CVE-2021-30179:攻击者在 attachment 中设置 generic=nativejava,强制 Provider 用 Java 原生反序列化处理参数

关键源码文件(Dubbo 2.7.x)

协议编解码

dubbo-remoting-api/src/main/java/org/apache/dubbo/remoting/exchange/codec/ExchangeCodec.java

decode() — 读取 16 字节 header

decodeBody() — 反序列化 body

dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/DubboCodec.java

继承 ExchangeCodec,重写 decodeBody()

根据 header 中的 Serialization ID 选择反序列化器

序列化

dubbo-serialization-hessian2/src/main/java/org/apache/dubbo/common/serialize/hessian2/Hessian2ObjectInput.java

Hessian2 反序列化入口

dubbo-common/src/main/java/org/apache/dubbo/common/serialize/java/JavaObjectInput.java

Java 原生反序列化入口

泛化调用

dubbo-rpc-api/src/main/java/org/apache/dubbo/rpc/filter/GenericFilter.java

处理泛化调用请求

决定使用哪种反序列化方式

Telnet 处理

dubbo-rpc-dubbo/src/main/java/org/apache/dubbo/rpc/protocol/dubbo/telnet/InvokeTelnetHandler.java

处理 telnet invoke 命令

使用 PojoUtils.realize() 实例化对象

Dubbo 的安全边界总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
攻击面总览:

┌─────────────────────────────────────────────────────┐
│ 攻击者 │
└─────────────┬──────────┬──────────┬─────────────────┘
│ │ │
① TCP:20880 ② HTTP:8080 ③ ZooKeeper:2181
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────┐
│ ① Dubbo 协议 ② HTTP 协议 ③ 配置中心投毒 │
│ - Hessian2 反序列化 - Java 反序列化 - YAML 注入 │
│ - 泛化调用 - JSON-RPC - 脚本注入 │
│ - SerID 篡改 - POST body - 路由规则投毒 │
│ - Telnet invoke
└─────────────────────────────────────────────────────┘
攻击入口 端口 所需条件 相关 CVE
Dubbo 协议 20880 网络可达 大部分 CVE
HTTP 协议 8080 Provider 开启 HTTP CVE-2019-17564
Telnet 20880 网络可达 CVE-2021-32824
配置中心 2181 能写入 ZK/Nacos CVE-2021-30180/30181

动手练习

用 Wireshark 抓包分析 Dubbo 协议数据包

过滤器:tcp.port == 20880

找到 dabb magic number

解析 flags 字节,确认序列化方式

用 Python socket 向 Dubbo 端口发送心跳包

1
2
3
4
5
6
7
8
import socket, struct
s = socket.socket()
s.connect(("127.0.0.1", 20880))
# 心跳包: magic + flags(Event=1) + 空body
# flags = 1110 0010 = 0xE2 (Request + TwoWay + Event + Hessian2)
header = b'\xda\xbb\xe2\x00' + struct.pack('!qI', 0, 1) + b'N'
s.send(header)
print(s.recv(1024))

连接到 Dubbo 的 telnet 接口

1
2
3
telnet 127.0.0.1 20880
# 输入 ls 查看暴露的服务
# 输入 invoke DemoService.sayHello("test")

上一章 目录 下一章
03-Hessian2-Gadget-Chain Dubbo漏洞 05-CVE-2019-17564