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 时:
创建空 HashMap
逐个读取 key-value 对
调用
HashMap.put(key, value)→ 触发key.hashCode()
安全检查发生在最外层对象类型上
但 HashMap 内部的 key 对象在 put() 时已经被反序列化并触发了 hashCode()
调用链
1 | DubboCodec.decodeBody() |
关键点
安全检查检的是”参数应该是什么类型”
但 HashMap 可以包含任意类型的 key
HashMap.put() 时 hashCode() 的调用是 Java 标准行为,无法拦截
复现步骤
环境搭建
1 | cd environments/cve-2020-11995 |
攻击
与 CVE-2020-1948 类似,但 payload 结构有所不同
恶意对象需要被包装在 HashMap 的 key 中
1 | 使用 dubbo-exp(自动处理 payload 包装) |
补丁分析
Dubbo 2.7.8 修复
将 Hessian2 类型过滤从”顶层检查”改为”深度检查”
在 MapDeserializer 内部也加入类型过滤
引入 SerializerFactory.setAllowNonSerializable(false) 配置
配置方式
1 | # dubbo.properties |
Payload 构造差异(与 CVE-2020-1948 对比)
CVE-2020-1948 的 payload 结构
1 | Dubbo Body: |
恶意对象直接作为 RPC 参数,被 Hessian2ObjectInput.readObject() 读取
CVE-2020-11995 的 payload 结构
1 | Dubbo Body: |
恶意对象包装在 HashMap 的 key 里
MapDeserializer.readMap() → HashMap.put(key, value) → key.hashCode() 触发
为什么嵌套就能绕过?
Dubbo 2.7.7 的类型检查逻辑:
1 | // 伪代码 |
时序问题:检查发生在反序列化之后,但 HashMap.put() 触发 hashCode() 是在反序列化过程中
其他嵌套容器也能利用
TreeMap:put() 时调用 key.compareTo() → 可触发 compareTo 入口的链
HashSet:内部使用 HashMap → 同样触发 hashCode()
PriorityQueue:heapify 时调用 comparator.compare()
只要是包含”反序列化时自动调用方法”的容器,都可能被利用
思考与延伸
这个漏洞说明安全检查的位置和时序非常重要
只在入口处做检查是不够的,HashMap/TreeMap 等容器内部的对象也可能触发危险操作
Hessian2 的 MapDeserializer 是一个反复被利用的攻击面
学到的攻击技巧:当直接攻击被拦截时,把恶意对象包装在合法容器中
这种”嵌套包装”绕过思路在很多安全场景中都适用(如 WAF 绕过、沙箱逃逸等)
| 上一章 | 目录 | 下一章 |
|---|---|---|
| 06-CVE-2020-1948 | Dubbo漏洞 | 08-CVE-2021-25641 |