Fastjson反序列化(1)-初探利用方式
# 前言
Fastjson是Alibaba所维护了一套开源的JSON解析库,它可以把Java对象转换成JSON文本进行广解析。
近几年来Fastjson爆出了许多反序列化漏洞,本着学习的目的,我打算写一个系列就Fastjson历史上爆出的一些漏洞以及绕过patch的方式进行分析,本意是想总结自己学习以及调试的过程,当然,如果读者能有一定的收获,那是我莫大的荣幸了。
# Fastjson初次使用
# 环境配置
环境配置
- Fastjson 1.2.24
- JDK 8u101
maven依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>x.x.x</version>
</dependency>
2
3
4
5
# 序列化
我下面用一个Demo来展示Fastjson进行序列化和反序列化的基本过程。
首先定义一个学生类,一个标准的JavaBean:
public class Student {
private int age;
private String name;
public Student() {
System.out.println("调用无参构造函数");
}
public Student(int age, String name) {
System.out.println("调用有参构造函数");
this.age = age;
this.name = name;
}
public int getAge() {
System.out.println("调用age的getter方法");
return age;
}
public void setAge(int age) {
System.out.println("调用age的setter方法");
this.age = age;
}
public String getName() {
System.out.println("调用name的getter方法");
return name;
}
private void setName(String name) {
System.out.println("调用name的setter方法");
this.name = name;
}
}
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
Fastjson中使用的序列化方法是JSON#toJSONString
,尝试用Fastjson将学生类的对象进行序列化:
public class FJTest {
public static void main(String[] args) {
Student student = new Student(20, "f4de");
String jsonString = JSON.toJSONString(student);
System.out.println(jsonString);
}
}
2
3
4
5
6
7
可以看到,在进行序列化的时候调用了两个private
属性的getter
方法。
但是如果一个private
属性没有对应的getter
方法,则无法进行序列化(我把age
的getter
方法注释掉了):
那么如果一个修饰的成员变量性是public、protected、static
呢?我们可以再进行下面的测试:
public class TT {
public String name = "F4de";
protected int age = 20;
static String address = "xxx";
}
2
3
4
5
public class FJTest {
public static void main(String[] args) {
TT tt = new TT();
System.out.println(JSON.toJSONString(tt));
}
}
2
3
4
5
6
在没有对应的getter
方法的时候,只有被public
修饰的成员变量序列化成功了。如果给每一个成员变量添加对应的getter
方法(当然实际环境中没有人这么写,这里只是为了演示):
经过测试我们就可以得到如下的结论:
- 序列化的时候会优先调用成员变量的
getter
方法(不论成员变量的修饰符是什么) public
成员变量如果没有getter
方法仍是可以被序列化成功的private、protected
修饰符所修饰的成员变量如果没有getter
方法则无法被序列化成功static
的成员变量无法被序列化(static
修饰的成员变量是属于类的)
# @type
在序列化的时候可以指定SerializerFeature.WriteClassName
,它是JSON#toJSONString
的一个属性值,如果指定了这个属性,则在序列化的时候会JSON字符串前面加上一个"@type":"xxx.xxx.xxx"
的键值对,表示正在进行序列化的对象所属的类。我们还用一开始的例子进行演示:
public class FJTest {
public static void main(String[] args) {
Student student = new Student();
System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName));
}
}
2
3
4
5
6
7
它的主要用处就是给JSON文本添加自省的功能,也就是说,在反序列化的时候,反序列化引擎会根据@type
的值来得到当前进行反序列化的JSON文本的对象类型,而Fastjson的反序列化漏洞就是由它引起的。
# 反序列化
# JSON#parse
JSON#parse
是Fastjson反序列化的一个方法,我们对{"@type":"com.demo1.Student","age":20,"name":"F4de"}
进行一次反序列化:
public class FJTest {
public static void main(String[] args) {
// Student student = new Student(20, "F4de");
// String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
// System.out.println(jsonString);
/*
{"@type":"com.demo1.Student","age":20,"name":"F4de"}
*/
String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
Object student = JSON.parse(jsonString);
System.out.println(student.getClass().getName());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
所以目前我们得到了这样的一个结论:
反序列化的过程中调用了类的无参构造函数
反序列化的过程中调用了对应成员变量的
setter
方法来对各个成员变量进行赋值
但是如果把setter
方法改为private
的话,仍然是无法进行序列化的:
private void setName(String name) {
System.out.println("调用了name的setter方法");
this.name = name;
}
2
3
4
如果在序列化的时候没有设置SerializerFeature.WriteClassName
的话,我们可以看一下用parse
方法反序列化后得到的结果:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
Object student = JSON.parse(jsonString);
System.out.println(student.getClass().getName());
}
}
2
3
4
5
6
7
我们看不到成员变量setter
方法的调用,反序列化后得到的结果也不是Student
类,所以FastJson序列化后得到的文本只有指定了@type
之后才支持自省。
# JSON#parseObject
Fastjson中除了使用JSON#parse
方法来反序列化,还有另外一个方法JSON#parseObject
,与前者不同的是,后者可以在反序列化的时候指定类名:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
Student student = JSON.parseObject(jsonString, Student.class);
System.out.println(student.getClass().getName());
}
}
2
3
4
5
6
7
对于这个例子中的JSON字符串,我们没有指定@type
,但是在反序列化的时候往方法中传入了第二个参数Student.class
,这样就可以让Fastjson在进行反序列化的时候拥有自省的效果。同样,我们也可以试一下不指定Class的效果:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
JSONObject student = JSON.parseObject(jsonString);
System.out.println(student.getClass().getName());
}
}
2
3
4
5
6
7
如果在JSON文本中指定了@type
,在使用JSON.parseObject
方法的时候第二个参数是可以指定为Object.class
,仍然是可以得到函数的调用和正确的类名的:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
Object student = JSON.parseObject(jsonString, Object.class);
System.out.println(student.getClass().getSimpleName());
}
}
2
3
4
5
6
7
如果JSON文本存在@type
,但是在JSON#parseObject
进行反序列化的时候没有指定类型:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
Object student = JSON.parseObject(jsonString);
System.out.println(student);
System.out.println(student.getClass().getSimpleName());
}
}
2
3
4
5
6
7
8
我们可以看到还额外调用了对应成员变量的getter
方法,并且得到的结果是一个JSON字符串的形式,它是一个JSONObject
的对象。
为什么会得到这样的结果呢?我们可以跟进parseObject
的源码看一下:
可以看到parseObject
就是调用了一次pasrse
方法,如果得到的对象的类不是JSONObject
的话,就对它进行一次toJSON
的处理,再跟进查看(关键代码如下):
因为要把结果处理成JSON文本的形式,所以需要额外调用一次getter
方法。
那么结果就很明显了,我们的JSON文本指定了@type
,在经过parse
方法处理之后会得到一个Student
类的对象,所以还会对它用toJSON
处理之后再用JSONObject
进行包装,得到了我们最后看到的结果。
所以我们之前得到的结论可以补充如下(方法修饰符为public
):
- 当
JSON.parseObject
没有指定类型的时候,会调用无参构造函数,对应成员变量的setter
和getter
方法。 - 当
JSON.parseObject
指定了类型的时候,会调用无参构造函数,对应成员变量的setter
方法。
# properties修饰符
其实我们上面得到的结论并不是准确的,因为在Jackjson中对于properties
修饰符修饰的成员变量,在反序列化的过程中,如果这个成员变量没有setter
方法,则会把getter
方法当作setter
方法来使用,也就是说通过getter
方法来对成员变量进行赋值。我们这里先说明这个结论,具体原理会在之后的调试中具体说明。我们可以看下面这里例子:
public class Student {
private int age;
private String name;
private Properties properties;
public Student() {
System.out.println("调用无参构造函数");
}
public Student(int age, String name, Properties properties) {
System.out.println("调用有参构造函数");
this.age = age;
this.name = name;
this.properties = properties;
}
public int getAge() {
System.out.println("调用age的getter方法");
return age;
}
public void setAge(int age) {
System.out.println("调用age的setter方法");
this.age = age;
}
public String getName() {
System.out.println("调用name的getter方法");
return name;
}
public void setName(String name) {
System.out.println("调用了name的setter方法");
this.name = name;
}
public Properties getProperties() {
System.out.println("调用properties的getter方法");
return properties;
}
// public void setProperties(Properties properties) {
// System.out.println("调用properties的setter方法");
// this.properties = properties;
// }
}
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
这里的properties
只有getter
方法,我们进行下面的反序列化:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\",\"properties\":{}}";
Object student = JSON.parseObject(jsonString, Object.class);
System.out.println(student);
System.out.println(student.getClass().getSimpleName());
}
}
2
3
4
5
6
7
8
可以看到在反序列化的过程中调用了properties
的getter
方法,那什么时候Fastjson在反序列化的时候会调用getter
方法呢?我这里先说结论(同样,具体原因会在调试的过程中对源码进行分析):
满足条件的setter:
- 函数名长度大于4且以set开头
- 非静态函数
- 返回类型为void或当前类
- 参数个数为1个
满足条件的getter:
- 函数名长度大于等于4
- 非静态方法
- 以get开头且第4个字母为大写
- 无参数
- 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong
properties
属性所属的类是Properties
,而这个类又继承自Hashtable
:
Hashtable
又实现了Map
接口,所以properties
属性的成员的getter
方法可以被触发:
# Feature.SupportNonPublicField
我们前面测试过了,如果一个成员变量(public
修饰的成员变量除外)没有对应的setter
方法或者其setter
方法(或者满足条件的getter
方法)的修饰符不是public
的话,那么在反序列化的时候是无法成功的:
public class TT {
private String name;
private int age;
public TT() {
}
public TT(String name, int age) {
this.name = name;
this.age = age;
}
private void setAge(int age) {
System.out.println("age的private的setter方法被调用");
this.age = age;
}
@Override
public String toString() {
return "TT{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
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
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.demo1.TT\",\"name\":\"F4de\",\"age\":20}";
Object parse = JSON.parse(jsonString);
System.out.println(parse.toString());
}
}
2
3
4
5
6
7
可以看到,两个成员变量均没有得到JSON文本中的值。
下面我们开始介绍 Feature.SupportNonPublicField
,它是JSON.parseObject
和JSON.parse
的一个属性。我们设置了它之后,再去反序列化上面的JSON文本:
public class FJTest {
public static void main(String[] args) {
String jsonString = "{\"@type\":\"com.demo1.TT\",\"name\":\"F4de\",\"age\":20}";
Object parse = JSON.parse(jsonString, Feature.SupportNonPublicField);
System.out.println(parse.toString());
}
}
2
3
4
5
6
7
可以看到得到了我们想要的结果,而且设置了它之后是不需要调用setter
或者getter
方法的。 所以对于没有setter
方法的private
成员、或者其setter
方法被private
修饰时,反序列化的时候如果传入Feature.SupportNonPublicField
这个属性则会反序列化成功。
# 总结
试了这么多例子,我们可以把上面的结果进行一次总结:
- 使用
JSON.parse
进行反序列化的时候,如果JSON文本中指定了@type
之后,则会调用对应类中的无参构造函数、非私有属性的setter
方法、满足条件的getter
方法,反序列化的结果获得到对应类的对象。如果没有指定@type
,则不会调用任何方法,会得到一个JSONObject
对象。 - 使用
JSON.parseObject(String json)
进行反序列化的时候,如果没有使用第二个参数指定Class,并且JSON文本指定了@type
的时候,它会再调用一次parse
方法,同时将得到的结果进行包装,所以会调用对应类中的无参构造函数、非私有属性的getter
方法、非私有属性的setter
方法,如果是properties
类型的成员,如果没有setter
方法的话,则它的getter
方法会调用两次。最后得到的结果是一个JSONObject
对象。 - 使用
JSON.parseObject(String json, Class<T> class)
这样指定了Class来进行反序列化的,则和第1点的结论一样,会调用对应类的无参构造函数、非私有属性的setter
方法、满足条件的getter
方法。最后得到的结果是对应类的一个对象。 - 反序列化的时候会优先调用
setter
方法,如果setter
方法不存在才会去寻找对应的并且满足条件的getter
方法。 - 对于没有
setter
方法的private
成员、或者其setter
方法被private
修饰时,反序列化的时候如果传入Feature.SupportNonPublicField
这个属性则会反序列化成功。
为了方便观看,笔者这里做了一个简单的表格(均未传入Feature.SupportNonPublicField
):
方法 | JSON文本是否指定@type | 最终调用的方法 | 反序列化的结果 |
---|---|---|---|
parse(String json) | 否 | 不调用方法 | JSONObject对象 |
parse(String json) | 是 | 构造方法 + setter + 满足条件的getter | 对应类的对象 |
parseObject(String json) | 否 | 不调用方法 | JSONObject对象 |
parseObject(String json) | 是 | 构造方法 + settet + getter + 满足条件的getter | JSONObject对象 |
parseObject(String json,Class class) | 否 | 构造方法 + settet + 满足条件的getter | 对应类的对象 |
parseObject(String json,Class class) | 是 | 构造方法 + settet + 满足条件的getter | 对应类的对象 |
# 漏洞Demo
Fastjson之所以存在反序列化漏洞,主要原因有两点:
- JSON文本中的
@type
可以指定反序列化的类,我们可以把它指定成我们所构造的恶意类 - 反序列化的过程中会自动调用恶意类中的构造方法、
setter
或者getter
方法、或者静态代码块static{}
我们下面就写一个简单的POC:
首先编写一个恶意类,其中它的无参构造方法会触发calc
:
public class EvilClass {
public EvilClass() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
2
3
4
5
public class Main {
public static void main(String[] args) throws Exception {
EvilClass evilClass = new EvilClass();
JSON.parse(JSON.toJSONString(evilClass, SerializerFeature.WriteClassName));
}
}
2
3
4
5
6
7
运行,发现弹出了两次计算器。
当然,因为我们写的代码中第一次调用了它的无参构造函数,所以会额外的弹出一次计算器。第二次弹出计算器原因就是因为在反序列化的过程中触发了EvilClass
的无参构造方法。
我们再测试一下触发setter
或者getter
的情况:
public class EvilClass {
private Properties properties;
public EvilClass() {
}
public Properties getProperties() throws Exception {
Runtime.getRuntime().exec("calc");
return properties;
}
@Override
public String toString() {
return "EvilClass{" +
"properties=" + properties +
'}';
}
// public EvilClass() throws Exception {
// System.out.println("调用无参构造方法");
// Runtime.getRuntime().exec("calc");
// }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
public static void main(String[] args) throws Exception {
String jsonString = "{'@type':'com.pagetest.EvilClass','properties':{}}";
jsonString = jsonString.replace("'", "\"");
System.out.println(jsonString);
Object parse = JSON.parse(jsonString);
System.out.println(parse.toString());
}
}
2
3
4
5
6
7
8
9
# 最后
这一篇文章主要简单介绍了一下Fastjson序列化和反序列化的基本原理以及一个简单的POC编写,总体难度不是很大,算是一个开头,在后面的文章中笔者会就历史上的反序列化漏洞以及patch的绕过进行调试和源码分析,希望读者和我都能有一些收获。