乐于分享
好东西不私藏

扒了 Dubbo 3.3 的源码,我发现三条能打死人的攻击路径

扒了 Dubbo 3.3 的源码,我发现三条能打死人的攻击路径

作者:兰陵笑笑僧

继 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 是否在白名单里。但:

  1. 手工关掉 serialization.security.check=false → 直接裸奔
  2. 白名单里有危险序列化器(比如 nativejava)→ 直接 Java 原生反序列化 RCE
  3. 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       │
                 └─────────────────────────┘

验证结果汇总

测试项STRICTWARN
QoS 端口可达✅✅
远程 invoke❌ 拒绝✅ Hello world!
服务枚举 ls❌ 拒绝✅ 成功
serializeCheckStatus❌ 拒绝✅ WARN 确认
标准 RPC 调用✅✅
泛化调用✅—
畸形参数(JNDI/命令注入等)✅ 全部安全处理—

防御建议

  1. QoS 端口别往外暴露。默认 acceptForeignIp=false 就是对的,别改了。
  2. 序列化检查保持 STRICT。生产环境别开 WARN 模式,serialization.security.check 别关。
  3. 定期更新 Dubbo 和 Fastjson2 版本。CVE 不会被挖完,但补丁会跟上。
  4. 最小权限原则。QoS 的 anonymousAccessPermissionLevel 改成 NONE 最安全。

最后

Dubbo 作为国内 Java 微服务的基石,安全性确实在逐步提升——3.3 版本比早期版本多了序列化安全检查、权限分级这些机制。但从这次分析来看,三条攻击路径分别对应着配置缺陷、历史漏洞残留、以及不同安全组件之间的连锁反应,攻防双方都还有得玩。

专注 Java 安全,我是兰陵笑笑僧。如果你觉得有用,点个在看,转给你的安全同事,下次接着聊 Nacos 的攻击面分析。


本文基于 Apache Dubbo 3.3.6 / 3.3.7-SNAPSHOT。

相关学习资料