FastJson简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,以其特有的算法,号称最快的json库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。
JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程

通过代码简单了解

首先导入依赖

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

然后定义个Student类

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

public class Student {
private String name;
private int age;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public void setAge(int age) {
System.out.println("setAge");
this.age = age;
}
}

然后是序列化的代码,调用 JSON.toJsonString() 来序列化 Student 类的对象 :
StudentSerialize.java

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

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class StudentSerialize {
public static void main(String[] args){
Student student = new Student();
student.setName("y0n3er");
student.setAge(6);
String jsonstring = JSON.toJSONString(student, SerializerFeature.WriteClassName);
System.out.println(jsonstring);
}
}

这里跟一下序列化的过程,在toJSONString()方法那里下个断点,跟进来到JSON类下的toJSONString()方法下
image-20230808223154139
在这里多出了个 static 的变量,写着 “members of JSON”,这里要特别注意一个值 DEFAULT_TYPE_KEY 为 “@type”,这个挺重要的,然后就是一些初始值赋值给out,这个out作为后续JSONSerializer构造函数的参数,看大佬说这一步走完其实就已经完成了序列化,但是我感觉是走完了serializer.write(object);这里完成序列化,也不太清楚
image-20230808223233965
然后就是到toString()这里序列化的数据进行显示
这里分析一下下面这条语句参数

1
String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);

第一个参数是 student,是一个对象,就不多说了;
第二个参数是 SerializerFeature.WriteClassName,是 JSON.toJSONString() 中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter/setter/is 方法。

  • Fastjson 接受的 JSON 可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。
    输出如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 设置了SerializerFeature.WriteClassName
    构造函数
    setName
    setAge
    getAge
    getName
    {"@type":"Dome.Student","age":6,"name":"y0n3er"}

    // 未设置SerializerFeature.WriteClassName
    构造函数
    setName
    setAge
    getAge
    getName
    {"age":6,"name":"y0n3er"}
    反序列化代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package Dome;  

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

    public class StudentUnserialize {
    public static void main(String[] args) {
    String jsonString = "{\"@type\":\"Dome.Student\",\"age\":6,\"name\":\"y0n3er\"}";
    Student student = JSON.parseObject(jsonString, Student.class, Feature.SupportNonPublicField);
    System.out.println(student);
    System.out.println(student.getClass().getName());
    }
    }
    image-20230808223321061
    简单的代码分析结束,稍微了解下过程

另外一些基础知识

感觉这里就是对后续漏洞理解做个基础铺垫

1. 反序列化时的 Feature.SupportNonPublicField 参数

前文我们在反序列化代码运行的时候,发现我们并不能获取到 “age” 这个值,因为它是私有属性的。

  • 如果要还原出 private 的属性的话,还需要在JSON.parseObject/JSON.parse中加上Feature.SupportNonPublicField参数。

这里改下Student类,将私有属性age的setAge()函数注释掉(一般没人会给私有属性加setter方法,加了就没必要声明为private了)然后修改StudentUnserialize.java去掉Feature.SupportNonPublicField,添加输出两个属性getter方法的返回值:

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

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

public class StudentUnserialize {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"Dome.Student\",\"age\":6,\"name\":\"y0n3er\"}";
Student student = JSON.parseObject(jsonString, Student.class);
System.out.println(student);
System.out.println(student.getClass().getName());
System.out.println(student.getName() + " " + student.getAge());
}
}

重新运行,会看到获取不到私有变量age的值而是被设置为0:

1
2
3
4
5
6
setName
Dome.Student@28c97a5
Dome.Student
getName
getAge
y0n3er 0

接着添加Feature.SupportNonPublicField:

1
Student student = JSON.parseObject(jsonString, Student.class,Feature.SupportNonPublicField);

再输出就能成功还原出age这个私有变量的值了:

1
2
3
4
5
6
setName
Dome.Student@28c97a5
Dome.Student
getName
getAge
y0n3er 6

也就是说,若想让传给JSON.parseObject()进行反序列化的JSON内容指向的对象类中的私有变量成功还原出来,则需要在调用JSON.parseObject()时加上Feature.SupportNonPublicField这个属性设置才行。

2. 只进行 JSON.parseObject(jsonString)

再来看下parseObject()的指定或不指定反序列化类型之间的差异。

由于Fastjson反序列化漏洞的利用只和包含了@type的JSON数据有关,因此这里我们只对序列化时设置了SerializerFeature.WriteClassName即含有@type指定反序列化类型的JSON数据进行反序列化;对于未包含@type的情况这里不做探讨,可自行测试。
关于这个@type的作用在白日梦组长视频下面看到个评论感觉挺形象的

为什么要引入type属性?比如,张三工资3000,另一个叫张三的工资20000,他们都有一个类,叫人类,现在老板要招工人了,你是猎头,你把两个人信息都报上去了,也就是序列化传过去了,但是你反序列化的时候,发现两个人都叫张三,那么哪个是3000工资的,哪个是20000工资的呢,这时候就会乱,所以引入了原始属性,也就是@type,这样就能找到归属,原来1米5的张三工资是3000,1米9的张三工资是20000,但是呢,这个@type属性可以随意填写,没想到工资3000的张三上面有人,给他写了个@type:我爹是李刚,故此1米5的张三拿着10000000000000的工资入职了。

总结就是用@type给你指定调用哪个类下的 setter 方法和 getter 方法
修改Student类,添加两个private成员变量,且所有的私有成员变量都不定义setter方法(既然是private也没必要定义):

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

import java.util.Properties;

public class Student {
private String name;
private int age;
private String address;
private Properties properties;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public String getAddress() {
System.out.println("getAddress");
return address;
}

public Properties getProperties() {
System.out.println("getProperties");
return properties;
}
}

修改反序列化StudentUnserialize文件,先是默认调用parseObject()不带指定类型的参数:也就是只有 JSON.parseObject(jsonstring)

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

import com.alibaba.fastjson.JSON;

public class StudentUnserialize2 {
public static void main(String[] args) {
String jsonString ="{\"@type\":\"Dome.Student\",\"age\":6," +
"\"name\":\"Drunkbaby\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parseObject(jsonString);
// 或以下语句,输出结果一致
//JSONObject obj = JSON.parseObject(jsonString);
System.out.println(obj);
System.out.println(obj.getClass().getName());
}
}

输出看到,调用了Student类的构造函数、所有属性的getter方法、JSON里面非私有属性的setter方法,其中getProperties()调用了两次;无论定义的对象是Object还是JSONObject,最后反序列化得到的都是JSONObject类对象,可以看到是未反序列化成功的:
image-20230808223413064
接着在StudentUnserialize2.java中修改反序列化代码语句如下,加上指定反序列化得到的类型为Object.class或Student.class,这样就是成功反序列化的回显。
image-20230809112543959

3. parse与parseObject区别

前面的demo都是用parseObject()演示的,还没说到parse()。两者主要的区别就是parseObject()返回的是JSONObject而parse()返回的是实际类型的对象,当在没有对应类的定义的情况下,一般情况下都会使用JSON.parseObject()来获取数据。

FastJson中的 parse()parseObject() 方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 settergetter 方法。

也就是说,我们用parse()反序列化会直接得到特定的类,而无需像parseObject()一样返回的是JSONObject类型的对象、还可能需要去设置第二个参数指定返回特定的类。

修改反序列化语句中的parseObject()parse()

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

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

public class StudentUnserialize3 {
public static void main(String[] args) {
String jsonString ="{\"@type\":\"Dome.Student\",\"age\":6," +
"\"name\":\"y0n3er\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parse(jsonString, Feature.SupportNonPublicField);
System.out.println(obj);
System.out.println(obj.getClass().getName());
}
}

image-20230809112621604

4. 简单总结

简单总结上面的就是反序列化的语句应该这么写

1
2
3
String jsonString ="{\"@type\":\"Student\",\"age\":6," +  
"\"name\":\"y0n3er\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON.parseObject(jsonString, Student.class);

要用 parseObject,里面的参数需要是 Object.class

fastjson 反序列化漏洞原理

这里还是上面的那个总结,我用@type给你指定你在反序列化的时候自动调用哪个类下的 settergetter 方法,当然这里并不是所有的 settergetter 方法。
下面直接引用结论,Fastjson会对满足下列要求的setter/getter方法进行调用:
满足条件的setter:

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

满足条件的getter:

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

前面的properties私有属性,其类型为Properties,而Properties是继承于Hashtable,Hashtable是实现Map接口类的类,因此properties私有属性的getter方法是继承自Map,从而能够成功被Fastjson调用。

1. 漏洞原理

由前面知道,Fastjson是自己实现的一套序列化和反序列化机制,不是用的Java原生的序列化和反序列化机制。无论是哪个版本,Fastjson反序列化漏洞的原理都是一样的,只不过不同版本是针对不同的黑名单或者利用不同利用链来进行绕过利用而已。

通过Fastjson反序列化漏洞,攻击者可以传入一个恶意构造的JSON内容,程序对其进行反序列化后得到恶意类并执行了恶意类中的恶意函数,进而导致代码执行。

那么如何才能够反序列化出恶意类呢?

由前面demo知道,Fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。如果指定的类型太大,包含太多子类,就有利用空间了。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。例如代码写Object o = JSON.parseObject(poc,Object.class)就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。

接着,如何才能触发反序列化得到的恶意类中的恶意函数呢?

由前面知道,在某些情况下进行反序列化时会将反序列化得到的类的构造函数、getter方法、setter方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在。换句话说,就是攻击者传入要进行反序列化的类中的构造函数、getter方法、setter方法中要存在漏洞才能触发。

我们到DefaultJSONParser.parseObject(Map object, Object fieldName)中看下,JSON中以@type形式传入的类的时候,调用deserializer.deserialize()处理该类,并去调用这个类的settergetter方法:

1
2
3
4
5
6
7
public final Object parseObject(final Map object, Object fieldName) {
...
// JSON.DEFAULT_TYPE_KEY即@type
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
...
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

整个解析过程相当复杂,知道结论就ok了。

小结一下

若反序列化指定类型的类如Student obj = JSON.parseObject(text, Student.class);,该类本身的构造函数、setter方法、getter方法存在危险操作,则存在Fastjson反序列化漏洞;

若反序列化未指定类型的类如Object obj = JSON.parseObject(text, Object.class);,该若该类的子类的构造方法、setter方法、getter方法存在危险操作,则存在Fastjson反序列化漏洞;

2. PoC 写法

一般的,Fastjson反序列化漏洞的PoC写法如下,@type指定了反序列化的类

1
2
3
4
5
{
"@type":"xxx.xxx.xxx",
"xxx":"xxx",
...
}

关键是要找出一个特殊的在目标环境中已存在的类,满足如下两个条件:

  1. 该类的构造函数、setter方法、getter方法中的某一个存在危险操作,比如造成命令执行;
  2. 可以控制该漏洞函数的变量(一般就是该类的属性);

3. 漏洞Demo

由前面比较的案例知道,当反序列化指定的类型是Object.class,即代码为Object obj = JSON.parseObject(jsonstring, Object.class, Feature.SupportNonPublicField);时,反序列化得到的类的构造函数、所有属性的setter方法、properties私有属性的getter方法都会被调用,因此我们这里直接做最简单的修改,将Student类中会被调用的getter方法添加漏洞代码,这里修改getProperties()作为演示:

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

import java.io.IOException;
import java.util.Properties;

public class Student {
private String name;
private int age;
private String address;
private Properties properties;

public Student() {
System.out.println("构造函数");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}

public int getAge() {
System.out.println("getAge");
return age;
}

public String getAddress() {
System.out.println("getAddress");
return address;
}

public Properties getProperties() throws Exception {
System.out.println("getProperties");
Runtime.getRuntime().exec("calc");
return properties;
}
}

然后这里直接新建一个反序列的代码用来触发漏洞

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

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

public class FastjsonEasyPoC {
public static void main(String[] args){
String jsonString ="{\"@type\":\"Dome.Student\",\"age\":6,\"name\":\"y0n3er\",\"address\":\"china\",\"properties\":{}}";

Object obj = JSON.parseObject(jsonString, Object.class);
System.out.println(obj);
System.out.println(obj.getClass().getName());
}
}

image-20230809112658404
很明显,前面的Demo中反序列化的类是一个Object类,该类是任意类的父类,其子类Student存在Fastjson反序列化漏洞,当@type指向Student类是反序列化就会触发漏洞。

对于另一种反序列化指定类的情景,是该指定类本身就存在漏洞,比如我们将上述Demo中反序列化那行代码改成直接反序列化得到Student类而非Object类,这样就是另一个触发也是最直接的触发场景:

1
Student obj = JSON.parseObject(jsonstring, Student.class, Feature.SupportNonPublicField);

4. 调试分析漏洞

debug一下看看内部运行逻辑
image-20230809112714870
最终会在断点这个命令执行,当然如果想跟的更深也可以继续进入这个方法跟

小结

到这里感觉还不算难,就是利用@type指定类去自动执行构造函数和setter或者getter方法

参考

Java反序列化Fastjson篇01-FastJson基础 | Drunkbaby’s Blog (drun1baby.top)
Fastjson反序列化漏洞(1)——基本原理 – JohnFrod’s Blog
https://www.bilibili.com/video/BV1bD4y117Qh/?spm_id_from=333.999.0.0&vd_source=fdbccecc8d1a39a2449860e47c52b6e7