0x01 Fastjson 简介

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换

提供两个主要接口来分别实现序列化和反序列化操作:

  • JSON.toJSONString:将 Java 对象转换为 json 对象,序列化的过程
  • JSON.parseObject/JSON.parse:将 json 对象重新变回 Java 对象;反序列化的过程

所以可以简单的把 json 理解成类似 php 的序列化字符串

0x02 Fastjson demo

这里通过 Demo 了解下如何使用 Fastjson 进行序列化和反序列化,以及其中的一些特性之间的区别等等

首先,pom.xml 里面导入 Fastjson 的依赖,这里先导入 1.2.24 的

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

序列化 demo

定义一个 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
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 类对象 :

1
2
3
4
5
6
7
8
9
10
11
12
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("G3ng4r");
student.setAge(20);
String jsonString = JSON._toJSONString_(student, SerializerFeature._WriteClassName_);
System._out_.println(jsonString);
}
}

我们可以调试稍微看一下序列化的逻辑,首先会进到 JSON 这个类,然后进到它的 toJSONString() 的函数里面,new 了一个 SerializeWriter 对象,我们的序列化这一步在这里就已经是完成了

进到 JSON 这个类里面的时候多出了个 static 的变量,写着 “members of JSON”,这里要特别注意一个值 DEFAULT_TYPE_KEY 为 “@type”,这个挺重要的

img

里面定义了一些初值,赋值给 out 变量,这个 out 变量后续作为 JSONSerializer 类构造的参数

img

继续往下面走,就是显示的部分了,toString() 方法,最后的运行结果

img

序列化操作的重心很明显:

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
String jsonString = JSON._toJSONString_(student,SerializerFeature._WriteClassName_);
构造函数
setName
setAge
getAge
getName
{"@type":"Student","age":20,"name":"G3ng4r"}

String jsonString = JSON._toJSONString_(student);
构造函数
setName
setAge
getAge
getName
{"age":20,"name":"G3ng4r"}

反序列化 demo

调用 JSON.parseObject() 进行反序列化:

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

public class StudentUnserialize {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"Student\",\"age\":20,\"name\":\"G3ng4r\"}";
Student student = JSON._parseObject_(jsonString, Student.class, Feature._SupportNonPublicField_);
System._out_.println(student);
System._out_.println(student.getClass().getName());
}
}

img

0x03 Fastjson tips

反序列化 Feature.SupportNonPublicField 参数

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

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

这里改下 Student 类,将私有属性 age 的 setAge()函数注释掉(一般没人会给私有属性加 setter 方法,加了就没必要声明为 private 了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Student student = JSON._parseObject_(jsonString, Student.class, Feature._SupportNonPublicField_)
构造函数
setName
Student@5b37e0d2
Student
getName
G3ng4r
getAge
0

Student student = JSON._parseObject_(jsonString, Student.class, Feature._SupportNonPublicField_)
构造函数
setName
Student@4459eb14
Student
getName
G3ng4r
getAge
20

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

JSON.parseObject(jsonString)

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

由于 Fastjson 反序列化漏洞的利用只和包含了 @type 的 JSON 数据有关,因此这里我们只对序列化时设置了 SerializerFeature.WriteClassName 即含有 @type 指定反序列化类型的 JSON 数据进行反序列化;对于未包含 @type 的情况这里不做探讨,可自行测试。

修改 Student 类,添加两个 private 成员变量,且所有的私有成员变量都不定义 setter 方法:

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
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;
}
}

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

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

public class StudentUnserialize {
public static void main(String[] args) {
String jsonString ="{\"@type\":\"Student\",\"age\":20," +
"\"name\":\"G3ng4r\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON._parseObject_(jsonString);
System._out_.println(obj);
System._out_.println(obj.getClass().getName());
}
}

输出看到,调用了 Student 类的构造函数、所有属性的 getter 方法、JSON 里面非私有属性的 setter 方法,其中 getProperties() 调用了两次;无论定义的对象是 Object 还是 JSONObject,最后反序列化得到的都是 JSONObject 类对象,可以看到是未反序列化成功的:

img

接着在 StudentUnserialize.java 中修改反序列化代码语句如下,加上指定反序列化得到的类型为 Object.class 或 Student.class,这样就是成功反序列化的回显:

img

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 类型的对象、还可能需要去设置第二个参数指定返回特定的类

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

public class StudentUnserialize {
public static void main(String[] args) {
String jsonString ="{\"@type\":\"Student\",\"age\":20," +
"\"name\":\"G3ng4r\",\"address\":\"china\",\"properties\":{}}";
Object obj = JSON._parse_(jsonString);
System._out_.println(obj);
System._out_.println(obj.getClass().getName());
}
}

img

@type 规定类与调用方法

fastjson 在反序列化的时候会去找我们在 @type 中规定的类是哪个类,然后在反序列化的时候会自动调用这些 setter 与 getter 方法的调用,注意!并不是所有的 setter 和 getter 方法

下面直接引用结论,Fastjson 会对满足下列要求的 setter/getter 方法进行调用:

满足条件的 setter:

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

满足条件的 getter:

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

fastjson 的利用攻击其实是蛮简单的,没有那么多复杂的链子,也不需要反射修改值,直接在 json 串里面赋值就好了

0x04 fastjson 反序列化漏洞原理

漏洞原理

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);
  • 若反序列化指定类型的类如 Student obj = JSON.parseObject(text, Student.class);,该类本身的构造函数、setter 方法、getter 方法存在危险操作,则存在 Fastjson 反序列化漏洞
  • 若反序列化未指定类型的类如 Object obj = JSON.parseObject(text, Object.class);,该若该类的子类的构造方法、setter 方法、getter 方法存在危险操作,则存在 Fastjson 反序列化漏洞

编写 POC

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

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

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

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

漏洞 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
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
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\":\"Student\",\"age\":20,\"name\":\"G3ng4r\",\"address\":\"china\",\"properties\":{}}";

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

img

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

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

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

0x05 思考总结

总体来说入门还是比较简单的,感觉流程和 PHP 反序列化很像

总结一下漏洞发生在反序列化的点,也就是 Obj.parseObj.parseObject 这里。必须的是传参要带入 class 的参数,最好带上 Feature.SupportNonPublicField 访问私有属性

POC 是通过 String 传进去的,要以 @type 打头。

漏洞的原因是反序列化的时候去调用了 gettersetter 的方法,理解起来不难