本篇是Java反序列化漏洞开坑的第二篇,主要是介绍了下Fastjson主要API的基本使用方式,序列化/反序列化的一些特性,以及Map和User类型在反序列化时的流程分析,对一些核心的反序列化过程进行了跟进,主要还是为了熟悉不同类型的Json字符串在反序列化过程中构造方法,setter方法,getter方法调用情况,帮助理解PoC的构造思路。在此基础上,下一篇接着会对
Templateslmpl类的利用链进行调试分析。
Fastjson反序列化漏洞基础
Fastjson简介
Fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。Fastjson在阿里巴巴大规模使用,在数万台服务器上部署,Fastjson在业界被广泛接受。在2012年被开源中国评选为最受欢迎的国产开源软件之一。出现安全问题影响范围很广。
序列化/反序列化基础知识
1 | # 使用1.2.23版本进行测试 |
API介绍
- 序列化:
String text = JSON.toJSONString(obj); - 反序列化:
JSON.parseObject返回JSONObject类型JSON.parse返回实际类型对象
使用方法
User类
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
36public class User {
private String name;
private int age;
public User() {
System.out.println("Called in 构造方法");
}
public String getName() {
System.out.println("Called in getName()");
return name;
}
public void setName(String name) {
System.out.println("Called in setName()");
this.name = name;
}
public int getAge() {
System.out.println("Called in getAge()");
return age;
}
public void setAge(int age) {
System.out.println("Called in setAge()");
this.age = age;
}
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
public class FJSer {
public static void main(String[] args) {
User user = new User();
user.setAge(18);
user.setName("BlBana");
String jsonString = JSON.toJSONString(user, SerializerFeature.WriteClassName);
System.out.println(jsonString);
}
}
// 返回结果
// Called in 构造方法
// Called in setAge()
// Called in setName()
// Called in getAge()
// Called in getName()
// {"@type":"User","age":18,"name":"BlBana"}反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import com.alibaba.fastjson.JSON;
public class FJTest {
public static void main(String[] args) {
String userString = "{\"@type\":\"User\" ,\"age\":25,\"name\":\"blbana\"}";
User user = JSON.parseObject(userString, User.class);
System.out.println(user);
System.out.println(user.getClass().getName());
}
}
// Called in 构造方法
// Called in setAge()
// Called in setName()
// User{name='blbana', age=25}
// UserSerializerFeature.WriteClassName用于在序列化后的字符串中添加@type属性,存放对象类型。在反序列化时,可以根据@type定义的类型解析生成对象。
Fastjson特性
User类
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
38public class User {
private int age;
private String name;
private String sex;
public String address;
public User() {
System.out.println("Called in User()");
}
public int getAge() {
System.out.println("Called in getAge()");
return age;
}
public String getName() {
System.out.println("Called in getName()");
return name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public String toString() {
return "User{" +
"age=" + age +
", name='" + name + '\'' +
", sex='" + sex + '\'' +
", address='" + address + '\'' +
'}';
}
}反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import com.alibaba.fastjson.JSON;
public class FJTest {
public static void main(String[] args) {
String userString = "{\"@type\":\"User\" ,\"age\":25,\"name\":\"blbana\",\"address\": \"xian\", \"sex\": \"man\"}";
User user = JSON.parseObject(userString, User.class);
System.out.println(user);
System.out.println(user.getClass().getName());
}
}
// Called in User()
// Called in setSex()
// User{age=0, name='null', sex='man', address='xian'}
// User四个属性,公有属性有无getter/setter都可以反序列化成功,私有属性有getter/setter反序列化成功,私有属性无setter反序列化失败:
private name,getter,未反序列化private age,getter,未反序列化private sex,getter/setter,反序列化成功public address,反序列化成功
Feature.SupportNonPublicField
1 | import com.alibaba.fastjson.JSON; |
上述例子中,不含有setter的私有属性无法反序列化,给parseObject方法加入Feature.SupportNonPublicField属性后,即可完成age和name两个私有属性的反序列化。
反序列化不同属性对比
主要是在parse/parseObject进行反序列化时,通过传入不同的属性对比反序列化的过程和反序列化结果有什么不同,找到类setter方法,getter方法,构造方法的调用条件,用于后期构造PoC,这里主要有三个属性影响:
- 有无Class类型
- 不同Class类型
- 有无Feature.SupportNonPublicField属性
1 | import java.util.Properties; |
- 有无Class类型/不同Class类型
无Class类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20import com.alibaba.fastjson.JSON;
public class FJTest {
public static void main(String[] args) {
String userString = "{\"@type\": \"User\" ,\"age\": 18,\"name\": \"blbana\",\"address\": \"xian\", \"sex\": \"man\", \"properties\":{}}";
Object user = JSON.parseObject(userString);
// JSONObject user = JSON.parseObject(userString);
System.out.println(user);
System.out.println(user.getClass().getName());
}
}
//Called in User()
//Called in setSex()
//Called in getProperties()
//Called in getAge()
//Called in getName()
//Called in getProperties()
//Called in getSex()
//{"address":"xian","sex":"man","age":0}
//com.alibaba.fastjson.JSONObject这种情况下调用了构造方法,所有属性的getter方法,所有属性的setter方法,其中getProperties被调用两次;定义类型无论是Object,还是JSONObject结果都一样,返回的类型都为JSONObject。
有Class类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
public class FJTest {
public static void main(String[] args) {
String userString = "{\"@type\": \"User\" ,\"age\": 18,\"name\": \"blbana\",\"address\": \"xian\", \"sex\": \"man\", \"properties\":{}}";
// Object object = JSON.parseObject(userString, Object.class);
// Object object = JSON.parseObject(userString, User.class);
User object = JSON.parseObject(userString, User.class);
System.out.println(object);
System.out.println(object.getClass().getName());
}
}
//Called in User()
////Called in setSex()
////Called in getProperties()
////User{age=0, name='null', sex='man', properties=null, address='xian'}
////User在有Class类型时,调用构造方法,所有属性的setter方法,properties属性的getter方法;定义类型无论是Object,User返回类型都是User类型。反序列化时根据@type制定类型进行解析,定义了对象的类型。
有无Feature.SupportNonPublicField属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
public class FJTest {
public static void main(String[] args) {
String userString = "{\"@type\": \"User\" ,\"age\": 18,\"name\": \"blbana\",\"address\": \"xian\", \"sex\": \"man\", \"properties\":{}}";
Object object = JSON.parseObject(userString, Object.class, Feature.SupportNonPublicField);
// Object object = JSON.parseObject(userString, User.class, Feature.SupportNonPublicField);
// User object = JSON.parseObject(userString, User.class, Feature.SupportNonPublicField);
System.out.println(object);
System.out.println(object.getClass().getName());
}
}
//Called in User()
//Called in setSex()
//Called in getProperties()
//User{age=18, name='blbana', sex='man', properties=null, address='xian'}
//User加入
Feature.SupportNonPublicField属性后,调用构造方法,所有属性的setter方法,properties属性的getter方法;返回类型都为User类型,并且不含有setter的私有属性也反序列化成功。
这里直接引用一下mi1k7ea的总结,写的非常详细:
Parse 和 ParseObject区别
两者的主要区别就是,parseObject返回的是JSONObject类型,而parse返回的是实际类型的对象。
这里是调用parseObject返回值的情况,实际parseObject调用的也是parse方法,只不过在返回之前,将目标对象转换为了JSONObect类型。
1 | // parseObject返回前会将对象转换为JSONObject类型 |
反序列化流程分析
Map反序列化
1 | public class FastjsonTest { |
关键类
跟进parseObject方法,其中在parser中有几个比较关键的类
JSONLexe—— 处理Json分词,next()可以获取Json字符串的下一个字符ParserConfig—— 包含解析配置,反序列化器,标签等各类配置信息JavaBeanDeserializer—— JavaBean反序列化类JSONScanner—— 负责扫描和获取json字符串中的Token并返回ObjectDeserializer—— 负责将json字符串反序列化,与JavaBean有关系,内置各种类型的反序列化器

在只给入Json String的情况下,默认按照Map类型进行解析

解析过程中使用skipWhitespace方法空白字符一律跳过处理

继续跟进到scanSymbol方法中,循环获取lexer分词器中的字符并根据字符的ASCII码值获取到一个hash整数,根据hash从预先处理好table中的将双引号中的key值获取并返回,此时获取到第一个键名"key1"

获取到”key1”键名后,使用stringVal方法获取到键值,并将键值对放入到JSONObect object的map中,至此已经将Json字符串中第一个键值对反序列化并放入JSONObject对象中,再判断到Json字符串未解析完时,循环开始下一个键值对的解析

User反序列化
1 | import com.alibaba.fastjson.JSON; |
跟进parseObject方法,进入到DefaultJSONParser.parseObject 方法,根据Class类型获取对象的反序列化器derializer,并进行反序列化操作

跟进this.config.getDeserializer(type)获取反序列化器的流程,先尝试从HashMap中获取User类型的预设反序列化器,由于未找到,进入this.getDeserializer()方法

继续跟进,this.getDeserializer()中主要通过比对type类型寻找合适的反序列化器,其中有一次this.denyList黑名单判断,黑名单中的类出现会抛出异常

对type Class类型,ClassName进行一系列比对,由于未能在预设的反序列化器中找到合适的,根据Class类型调用this.createJavaBeanDeserializer 创建JavaBeanDeserializer

其中beanInfo中存放着User类的Class对象,User类构造器,一些字段的Class对象及其构造器,经过一些判断后,暂且未用到这些信息,不过这种存放bean信息的类倒是挺有意思的,之后会跟进下build方法具体内容

进入到JavaBeanDeserializer 类中

再次生成beanInfo对象,并循环根据fieldInfo信息给每个字段生成对应类型的fieldDeserializer并存放到this.sortedFieldDeserializers和this.fieldDeserializers


JavaBeanDeserializer生成完毕,回到主流程开始调用derializer.deserialze方法
this.extraFieldDeserializers中存放之前this.sortedFieldDeserializers中未存放的两个属性,后面会跟进下属性为什么会分到两个不同的Map中
this.extraFieldDeserializers- name
- age
this.sortedFieldDeserializers- address
- sex
- properties
先会实例化User对象,紧接着会对User的字段进行反序列化并赋值到User对象的对应属性中

循环将this.extraFieldDeserializers和this.sortedFieldDeserializers中准备好的Field使用parseField方法解析到User object中去,进一步跟进parseField方法

按照key的类型,利用“智能匹配”功能,找到属性的反序列化器,调用反序列化器的parseField方法


parseField方法中,对属性进行反序列化处理,调用this.fieldValueDeserilizer.deserialze 方法

获取到value指后调用this.setValue方法,将属性值加入到object中

setValue对于能获取到属性Method对象的,直接利用反射将值set到属性中


setValue对于无法获取的Method对象的,使用field.set给属性设置一个新值


也就是说在属性反序列化获取到值后,都会调用setValue将值赋值到object中对应的属性中:
- 对于有setter的,利用反射调用setter方法赋值;
- 对于没有setter的,利用字段field.set的方式给属性赋值;
当所有Field对象都实例化并添加到User对象object中后,返回object,结束反序列化流程。
Note:关于JavaBeanInfo的build方法
之前可以看到每次反序列化时,getProperties方法都会在这里被调用,这里跟一下Method是如何被赋值到this.fieldInfo.method的,主要发生在build方法中

在build方法中,首先就会利用反射获取到类的字段和方法列表,以及User的构造方法

1 | // set 方法 |
之后method被循环取出,开始判断:
set方法
- 方法名要大于等于4;
- 非静态方法;
- 返回值未void或者返回当前类;
- 参数只有一个
get方法
- 方法名要大于等于4;
- 非静态方法;
- 以get开头并且第四个字母为大写;
- 参数个数为0;
- Collection & Map & AtomicBoolean & AtomicInteger & AtomicLong 方法返回类型
由于getProperties符合条件被放入到List中,在后续反序列化字段的时候这个method会被调用。
问题记录
存放
key的symbolTable是何时解析的?在SymbolTable对象生成时,$ref 和 @type 两个关键字已经被预设进去。
存放
value的方法是如何获取到值得?通过循环获取到目标值得开始和结束索引位置,通过subString方法截取目标值
参考链接
- https://www.mi1k7ea.com/2019/11/03/Fastjson%E7%B3%BB%E5%88%97%E4%B8%80%E2%80%94%E2%80%94%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86/
- https://www.mi1k7ea.com/2019/11/07/Fastjson%E7%B3%BB%E5%88%97%E4%BA%8C%E2%80%94%E2%80%941-2-22-1-2-24%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
- https://p0sec.net/index.php/archives/123/