java安全-CC5&CC7
java安全-CC5&CC7
- 这俩条链都是在 Hashmap#get 方法之上,变换了与 CC1 不同的入口类即 readObject 方法调用类
一、CC5
1.主要逻辑
BadAttributeValueExpException#readObject

readObject方法调用toString方法
TiedMapEntry#toString

TiedMapEntry#toString这个方法属于CC1中的一部分,接着看getValue方法
TiedMapEntry#getValue

这里会调用get方法,而LazyMap的get方法会调用链路transform方法

2.poc
编写poc,尝试满足条件

BadAttributeValueExpException这个类的构造方法会毁掉我们的恶意类tiedMapEntry,于是要通过反射最后修改
1 | setFieldValue(obj2,"val",tiedMapEntry); |
总poc
1 | Class r = Runtime.class; |
成功

二、CC7
1.关键类
Hashtable#readObject

这里调用了Hashtable#reconstitutionPut方法
Hashtable#reconstitutionPut

这里调用了equals方法
AbstractMap#equals

AbstractMap#equals方法会调用 get 方法,刚好接上链条后半段
2.主要逻辑
A、reconstitutionPut方法

这个方法调用equals是有条件的
Hashtable 里需要有两个 Map(lazyMap1 和 lazyMap2),它们的 HashCode 是一样的,这意味着它们都要存进同一个编号的柜子(比如第 5 号柜子)。
第一轮:存入 lazyMap1
- 检查柜子:程序走到
for循环,去看tab[5](第 5 号柜子)。 - 发现为空:此时柜子是空的(
null)。 - 跳过循环:因为
e == null,for 循环一次都不会执行。当然也就不会执行里面的e.key.equals(key)。 - 直接存入:程序跳过循环,执行下面的代码,把
decorateMap1放进了tab[5]。
第二轮:存入 lazyMap2
- 检查柜子:程序再次走到
for循环,去看tab[5]。 - 发现有人:此时
tab[5]里已经坐着decorateMap1了(e != null)。 - 进入循环:因为柜子不为空,程序被迫进入循环。
- 进行盘问 (触发漏洞):
- 程序心想:“这个新来的(
lazyMap2)和已经在里面的(lazyMap1)是不是同一个东西?” - 它执行了
e.key.equals(key),也就是decorateMap1.equals(decorateMap2)。 - 💥 轰! 这一比较,触发了
LazyMap的get,弹出了计算器。
- 程序心想:“这个新来的(
1 | // 遍历哈希桶(链表),检查里面是不是已经有这个 Key 了 |
同时,为了构造 e.hash == hash ,找到java中的一个小 bug
1 | "yy".hashCode() == "zZ".hashCode() |
最后便是
1 | Map map1 = new HashMap<>(); |
问题1:equals 的“懒惰”优化(直接返回)
如果你传的是同一个对象(比如 put(map1, val); put(map1, val);):
1 | // AbstractMap.equals 源码第一行 |
- 后果:代码在第一行就检测到是同一个对象引用,直接返回
true。 - 结局:后面的逻辑全部跳过,根本不会执行
m.get(),计算器弹不出来。
问题2:LazyMap 的“缺货”机制(这是最关键的!)
假设你传了两个不同的对象,但它们的内容是一样的(比如 map1 里有 key “yy”,map2 里也有 key “yy”)。
当 Hashtable 强迫它们比较时,AbstractMap.equals 会执行以下逻辑:
map1说:“我有 key"yy",我要去检查map2有没有这个 key。”- 代码执行:
map2.get("yy")。 - 关键时刻:
LazyMap的逻辑是:“只有当你找我要一个我不存在的 key 时,我才会去调用 Transformer(执行命令)。”- 因为
map2里面也有"yy",它会直接把值返回给你。
- 结局:
get方法正常返回,不触发 Transformer,计算器弹不出来。
“yy” 和 “zZ” 这种组合
- 既骗过 Hashtable:让它以为这两个 Map 是一样的(HashCode 必须相同),这样才能强迫它们走进同一个柜子,触发
equals比较。- 这就是为什么选
"yy"和"zZ",因为它俩 HashCode 一样,导致包裹它们的 Map HashCode 也一样。
- 这就是为什么选
- 又骗过 LazyMap:让它发现这两个 Map 内容其实不一样(Key 不同),这样才能触发“找不到 Key”的逻辑。
map1有"yy"。map2有"zZ"(没有"yy")。- 当
map1拿"yy"去问map2时,map2发现自己没货,于是触发 Transformer,BOOM!
B、AbstractMap#equals方法逻辑
- Hashtable#reconstitutionPut
- 你的理解:确实是起点。因为 Hash 碰撞,触发了
e.key.equals(key)。 - 实际动作:
lazyMap1.equals(lazyMap2)。
- 你的理解:确实是起点。因为 Hash 碰撞,触发了
- LazyMap#equals
- 你的理解:它没有这个方法。
- 实际动作:JVM 自动去查它的父类
AbstractMapDecorator。
- AbstractMapDecorator#equals
- 你的理解:调用
decorated().equals(object)。 - 实际动作:这就是**“甩锅”**。它把判断任务转交给了它包裹的
map(也就是hashMap1)。此时变成了hashMap1.equals(lazyMap2)。
- 你的理解:调用
- HashMap#equals
- 你的理解:HashMap 也没有这个方法。
- 实际动作:JVM 继续向上找父类,找到了
AbstractMap。
- AbstractMap#equals
- 你的理解:在调用它的父类,最终调用
get方法。 - 实际动作:这是**“凶手”**。它在对比过程中,拿
hashMap1里的 key (“yy”),去调用传入参数m(即lazyMap2) 的get("yy")。
- 你的理解:在调用它的父类,最终调用
最后一步 AbstractMap#equals 里,是谁调用的 get
在 AbstractMap.equals(Object o) 源码中:
1 | public boolean equals(Object o) { |
C、remove方法
当在本地执行 hashtable.put(lazyMap2, 1) 时,由于哈希碰撞触发了 equals,导致 AbstractMap 调用了 lazyMap2.get("yy")。
这时候,LazyMap 内部发生了如下反应:
if (map.containsKey("yy") == false):- 此时
lazyMap2只有"zZ",没有"yy"。 - 条件成立,进入
if内部。
- 此时
Object value = factory.transform("yy"):- 这里的
factory就是你构造的ChainedTransformer。 - 如果是本地调试(没把 Transformer 设为空),这里就已经弹出计算器了。
value变成了 Transformer 执行后的结果(通常是一个对象或者 Process 结果)。
- 这里的
map.put("yy", value)(这就是你要找的代码!):- 注意!
LazyMap并没有把这个值用完即弃,而是永久性地存入了自己的 HashMap 中。 - 从这一刻起,
lazyMap2的肚子里就多了一个键值对:"yy" -> value。
- 注意!
return value:- 返回结果,方法结束。
对应get方法源码
1 | public Object get(Object key) { |
map.put(key, value) 会改变 Map 的结构。在本地构造 Payload 的过程中,这个副作用会导致 Payload 被“污染”(多出了不该有的 Key)。如果不手动 remove 掉,发给受害者时,受害者一查发现“咦,你有 ‘yy’ 啊”,就不会走进这个 if 语句,也就永远走不到 factory.transform 这一行了。
状态图
| 步骤 | LazyMap2 的状态 | 说明 |
|---|---|---|
| 1. 初始化 | {"zZ": 1} |
初始纯净状态 |
2. hashtable.put() |
{"zZ": 1, "yy": obj} |
被污染! 本地触发了一次 get,自动生成了 yy |
| 3. 如果直接序列化 | {"zZ": 1, "yy": obj} |
发送给对方的是脏数据 |
4. 对方反序列化执行 get("yy") |
直接返回 obj | 因为有了 yy,就不触发漏洞逻辑了 |
| 正确的做法 | ||
3. lazyMap2.remove("yy") |
{"zZ": 1} |
恢复纯净,重置陷阱 |
| 4. 序列化发送 | {"zZ": 1} |
发送干净数据 |
5. 对方反序列化执行 get("yy") |
触发 Transformer | 因为没有 yy,LazyMap 才会去执行命令 |
D、调用的get方法
lazyMap2
第一幕:还原 Hashtable(开箱)
受害者服务器开始读取 ser.bin。
- 读取第一个对象:它读到了
lazyMap1(那个持有"yy"钥匙的警察)。- 服务器把它放进新建的
Hashtable的 5 号柜子(假设 Hash 是 5)。 - 此时柜子是空的,直接放入。平安无事。
- 服务器把它放进新建的
- 读取第二个对象:它读到了
lazyMap2(那个被清空了"yy"且身怀炸弹的嫌疑人)。- 服务器计算它的 Hash,发现也是 5。
- 服务器准备把它也放进 5 号柜子。
第二幕:碰撞与盘查(触发点)
因为 5 号柜子里已经有人了(lazyMap1),Hashtable 内部的代码(reconstitutionPut)立刻警觉起来:
- Hashtable:“同一个柜子不能放重复的 Key,我得检查一下这俩是不是同一个东西。”
- 动作:调用
e.key.equals(key)。e.key是柜子里坐着的lazyMap1。key是新来的lazyMap2。
- 执行代码:
lazyMap1.equals(lazyMap2)。
第三幕:借刀杀人(AbstractMap 逻辑)
代码进入 AbstractMap.equals()。这是 lazyMap1 的主场。
- lazyMap1:“既然要比,那就拿我的清单来比。”
- 遍历:
lazyMap1拿出了它唯一的一个 Key ——"yy"。- 注:这个 Key 是我们在 Payload 构造阶段保留下来的,没有被 remove。
- 质问:
lazyMap1指着lazyMap2问:“你有没有"yy"对应的值?拿出来!” - 执行代码:
lazyMap2.get("yy")。
第四幕:引爆(LazyMap 逻辑)
这是最关键的一刻。代码跳转到了 lazyMap2 的 get 方法中。
- 检查库存:
lazyMap2翻了一下自己的口袋。- 回想一下:我们在发送前专门执行了
lazyMap2.remove("yy")。 - 结果:找不到
"yy"!
- 回想一下:我们在发送前专门执行了
- 触发后门:
LazyMap的逻辑是:“没有?那我就造一个。”- 它调用
factory.transform("yy")。
- 核爆:
- 这里的
factory是什么? - 回想一下:我们在发送前通过反射,把
lazyMap2的factory替换成了恶意的finalChainedTransformer。 - 结果:
Runtime.exec("calc")被执行。
- 这里的
E、意外点
1 | Map map1 = new HashMap<>(); |
这里
obj2.put(lazyMap1, 1):成功放入。
obj2.put(lazyMap2, 1):发生 Hash 碰撞,触发 lazyMap1.equals(lazyMap2)。
比较过程:
lazyMap1拿出"yy"(值为 1)。- 去问
lazyMap2:lazyMap2.get("yy")。 lazyMap2没货,调用 Transformer,返回 1。AbstractMap比较:lazyMap1的 1 等于lazyMap2返回的 1。- 结论:
equals返回true。
Hashtable 的决定:
- “这俩是一样的嘛!”
- 结果:Hashtable 并没有存入两个 Map,而是合并成了一个。
3.最终poc
1 |
|
成功

到这里 CC 链也就结束啦,下图是总结
