
作者:兰陵笑笑僧
继 Fastjson2 0day 之后,我又把目光投向了阿里巴巴的另一张王牌——Dubbo。
结果你猜怎么着?确实还有货。
写在前面
Dubbo 不是什么小众玩具。国内但凡上点规模的 Java 项目,十有八九都跑着它。工商银行、携程、海尔、金蝶,底层全是 Dubbo。
GitHub 上 42k+ stars,这不是闹着玩的。
我之前写了 Fastjson2 的 AutoType 绕过分析,这次干脆把 Dubbo 3.3 的源码拉下来过了一遍——从协议解码层一路追到管理端口,最终在 Docker 环境里把三条攻击路径都验证了一遍。
废话不多说,直接上干货。
一张表说清楚:Dubbo 有哪些攻击面
| 攻击面 | 级别 | 类型 | 要认证吗 |
|---|---|---|---|
| QoS 远程调用任意服务方法 | 🔴 致命 | 命令执行 | 需要端口可达 |
| 协议层反序列化 RCE(Hessian2) | 🔴 致命 | 反序列化 | 不需要 |
| 参数反序列化(DecodeableRpcInvocation) | 🔴 致命 | 反序列化 | 不需要 |
| Fastjson2 WARN 模式绕过 | 🟠 高危 | AutoType RCE | 需先降级 |
| 泛化调用 $invoke | 🟠 高危 | 反序列化 | 不需要 |
| QoS 降级序列化检查 | 🟠 高危 | 绕过机制 | 需端口可达 |
| Telnet 命令注入 | 🟡 中危 | 信息泄露 | 不需要 |

最妙的是什么?上面这些攻击面,有些默认配置下就能打。
攻击面一:QoS 端口——最短的攻击路径
Dubbo 默认开了一个管理端口 22222,叫 QoS。你拿 telnet 或者浏览器访问这个端口,就能执行 Dubbo 的管理命令。
权限控制长什么样?我把源码里的完整调用链画出来了:
你从远程连上来
↓
ForeignHostPermitHandler 检查你 IP
├─ acceptForeignIp=false(默认)
├─ isAllowAnonymousAccess()=true ← 关键在这里
↓ 跳过 IP 白名单检查,连上了
然后执行命令
↓
DefaultCommandExecutor 做权限检查
├─ 你的当前级别 = PUBLIC(匿名默认,级别 1)
├─ invoke 命令要求 = PROTECTED(级别 2)
↓ 1 >= 2?→ 拒绝

看到问题了吗?
默认配置下远程确实调不了 invoke 命令,因为 PUBLIC(1) < PROTECTED(2)。
但换个场景就不一样了。
三种姿势打穿 QoS 端口
姿势一:管理员手滑了
只要 acceptForeignIp=true 或者 anonymousAccessPermissionLevel >= PROTECTED,远程直接飞。
我们在 Docker 里用 WARN 模式的 provider 测试了一下:
telnet 到 22223 → invoke com.dubbo.lab.api.DemoService.sayHello("world")
→ 返回: "Hello world, from Dubbo 3.3 Lab!"
啪,通了。
姿势二:SSRF 利用
Dubbo 主端口(20880)上的某个服务如果会往外发 HTTP 请求,攻击者就可以让它请求 localhost:22222——绕过 IP 检查,因为在本机看来你是 loopback 地址,直接获得 PRIVATE 级别。
姿势三:本地已经能连了
内网能直连 QoS 端口的攻击者,默认情况下虽然不能执行 invoke,但可以执行 live、ready、startup 这些 PUBLIC 命令——探活、发现服务存在,为后续攻击铺路。
利用原理
核心代码在 InvokeTelnet.java 第 157 行:
Object[] array = realize(list.toArray(),
invokeMethod.getParameterTypes(),
invokeMethod.getGenericParameterTypes());
Object o = invokeMethod.invoke(selectedProvider.getServiceInstance(), array);
PojoUtils.realize() 会根据 JSON 参数里的 class 字段实例化任意 Java 类,然后反射调用。只要你传的参数类型足够刁钻,就能让服务端加载你想要的类。
攻击面二:协议层的反序列化 RCE
这条路径比 QoS 更隐蔽,因为不需要任何认证,只要你的 Dubbo 端口(默认 20880)暴露在外就能打。
完整调用链
ExchangeCodec.decode()
→ 读魔数 0xdabb,序列化 ID 藏在 flag 的低 5 位
→ CodecSupport.deserialize(序列化ID)
DubboCodec.decodeBody()
→ DecodeableRpcInvocation.decode()
DecodeableRpcInvocation.decode()
→ L133: 拿到 ObjectInput
→ L151: 读参数类型描述符 desc
→ L167: drawPts() → 查服务注册信息,拿到参数类型
→ L178: drawArgs() → 逐个反序列化参数
→ L277: args[i] = in.readObject(pts[i]) ★ 攻击点
→ L182: 反序列化 attachments(Map 类型)
关键代码
// 第 273-279 行:参数反序列化核心
protected Object[] drawArgs(ObjectInput in, Class<?>[] pts) throws ... {
args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
args[i] = in.readObject(pts[i]);
}
return args;
}
什么情况下能打
Dubbo 3.3 默认开了序列化安全检查(serialization.security.check=true),会校验序列化 ID 是否在白名单里。但:
- 手工关掉
serialization.security.check=false→ 直接裸奔 - 白名单里有危险序列化器(比如 nativejava)→ 直接 Java 原生反序列化 RCE
- Hessian2 白名单有漏洞 → CVE-2023-23638 就是这么来的
我们在 Docker 里用 Java consumer 验证了这条路径是通的——标准 RPC 调用、泛化调用 $invoke、畸形参数注入,全部成功返回。
攻击面三:Fastjson2 安全过滤器的 WARN 模式绕过
这条线和之前 Fastjson2 的 0day 研究是连着的。
Dubbo 的 fastjson2 序列化适配用了一个叫 Fastjson2SecurityManager.Handler 的过滤器,它的安全逻辑长这样:
apply(typeName, expectClass, features)
│
├─ 在白名单里 → 通过 ✅
│
├─ STRICT 模式 + 不在白名单 → 抛异常 ❌
│
└─ WARN 模式 → loadClassDirectly(typeName)
├─ 在黑名单里 → 拒绝
├─ 不在黑名单 → TypeUtils.loadClass()
└─ 加载成功 → 返回 class ✅(只打了一行警告日志)
看到 WARN 模式那里了吗?不在白名单的类,只打警告日志,反序列化照常进行。
三种模式的安全级别对比如下:
| 模式 | 行为 | 安全吗 |
|---|---|---|
STRICT(默认) | 非白名单直接抛异常 | 安全 |
WARN | 非白名单只打日志,仍然加载 | 高危 |
DISABLE | 啥也不检查 | 致命 |
默认是 STRICT,看起来安全。但问题在于 QoS 端口上有一个 serializeCheckStatus 命令,它可以动态把检查级别降成 WARN。
降级链路:
QoS 22222 → serializeCheckStatus WARN → SerializeSecurityManager.setCheckStatus(WARN) → Fastjson2SecurityManager.notifyCheckStatus(WARN) → 重建 Handler → status = WARN → 之后的 fastjson2 反序列化全部放行
所以如果攻击者先通过 QoS 降级了序列化检查,再往主端口发一个精心构造的 fastjson2 请求——刚好你在 Fastjson2 上的 FNV 哈希碰撞研究就能平移过来用了。
验证环境一览
我们用 Docker Compose 搭了一套完整的测试环境:
┌─ Zookeeper ─┬─ dubbo-provider (STRICT) ─┐
│ 2181 │ 20880 / 22222 │
└─────────────┘ strict 模式 │
├─ dubbo-provider-warn ───┤
│ 20881 / 22223 │
│ WARN + 远程 QoS │
└─────────────────────────┘
验证结果汇总
| 测试项 | STRICT | WARN |
|---|---|---|
| QoS 端口可达 | ✅ | ✅ |
| 远程 invoke | ❌ 拒绝 | ✅ Hello world! |
| 服务枚举 ls | ❌ 拒绝 | ✅ 成功 |
| serializeCheckStatus | ❌ 拒绝 | ✅ WARN 确认 |
| 标准 RPC 调用 | ✅ | ✅ |
| 泛化调用 | ✅ | — |
| 畸形参数(JNDI/命令注入等) | ✅ 全部安全处理 | — |
防御建议
- QoS 端口别往外暴露。默认
acceptForeignIp=false就是对的,别改了。 - 序列化检查保持 STRICT。生产环境别开 WARN 模式,
serialization.security.check别关。 - 定期更新 Dubbo 和 Fastjson2 版本。CVE 不会被挖完,但补丁会跟上。
- 最小权限原则。QoS 的
anonymousAccessPermissionLevel改成 NONE 最安全。
最后
Dubbo 作为国内 Java 微服务的基石,安全性确实在逐步提升——3.3 版本比早期版本多了序列化安全检查、权限分级这些机制。但从这次分析来看,三条攻击路径分别对应着配置缺陷、历史漏洞残留、以及不同安全组件之间的连锁反应,攻防双方都还有得玩。
专注 Java 安全,我是兰陵笑笑僧。如果你觉得有用,点个在看,转给你的安全同事,下次接着聊 Nacos 的攻击面分析。
本文基于 Apache Dubbo 3.3.6 / 3.3.7-SNAPSHOT。
夜雨聆风