Dubbo 架构与协议分析 — 理解攻击面
Dubbo 是什么
Apache Dubbo 是一个高性能的 Java RPC 框架
核心功能:让一台机器上的 Java 程序可以像调用本地方法一样调用另一台机器上的方法
广泛用于微服务架构,阿里巴巴内部大规模使用
架构模型
三个核心角色
1 | ┌────────────┐ ② 订阅服务 ┌──────────────┐ |
Provider(服务方):暴露服务,监听端口(默认 20880),处理 RPC 请求
Consumer(调用方):调用远程服务,从 Registry 获取 Provider 地址
Registry(注册中心):服务注册与发现,常用 ZooKeeper、Nacos
RPC 调用流程
Provider 启动,将服务地址注册到 Registry
Consumer 启动,从 Registry 订阅服务列表
Consumer 根据负载均衡选择一个 Provider
Consumer 将方法名、参数序列化,通过网络发送到 Provider
Provider 反序列化请求,执行业务方法 ← 漏洞发生在这里
Provider 将结果序列化返回给 Consumer
安全启示
如果攻击者能直接连接 Provider 端口(20880),就可以发送恶意 RPC 请求
如果攻击者能访问 Registry(ZooKeeper:2181),可以投毒路由规则
Provider 端口通常没有认证机制(早期版本)
分层架构
Dubbo 内部分层
1 | ┌──────────────────────────────────┐ |
关键漏洞层
Exchange Layer:ExchangeCodec.decode() → 解码网络数据 → 调用序列化层反序列化
Protocol Layer:GenericFilter 处理泛化调用 → 可能绕过类型检查
Cluster Layer:路由规则解析 → YAML/脚本注入
Dubbo 协议二进制格式(核心重点)
概览
Dubbo 协议是一个自定义的二进制协议
每个请求/响应包含:16 字节固定 Header + 变长 Body
默认端口:20880
Header 结构(16字节)
1 | Byte: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
逐字段解析
Byte 0-1:Magic Number(魔数)
固定值:0xDA 0xBB
用于识别 Dubbo 协议数据包
安全意义:抓包看到 dabb 就知道是 Dubbo 协议
Byte 2:Flags(标志字节) ← CVE-2021-25641 攻击的关键字节
1 | Bit: 7 6 5 4 3 2 1 0 |
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 | import struct |
如果要篡改序列化方式为 Java 原生:
1 | # 将 Hessian2(0xC2) 改为 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 | 收到 RPC 请求 |
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 | 攻击面总览: |
| 攻击入口 | 端口 | 所需条件 | 相关 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 | import socket, struct |
连接到 Dubbo 的 telnet 接口
1 | telnet 127.0.0.1 20880 |
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 03-Hessian2-Gadget-Chain | Dubbo漏洞 | 05-CVE-2019-17564 |