Fastjson反序列化(一)

Chiexf Lv4

0x00 介绍

Fastjson是一款由阿里巴巴公司开发的Java语言编写的高性能JSON解析库。它采用了特殊的算法和数据结构,可以快速地将JSON格式的字符串解析为Java对象,或将Java对象序列化为JSON字符串

Fastjson提供了两个主要接口来分别实现对于Java Object的序列化和反序列化操作。

  • JSON.toJSONString
  • JSON.parseObject/JSON.parse

0x01 简单使用

  • person.java

这是一个简单的JavaBean

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 Person {
private String name;
private int age;

public Person() {
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(person)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FJTest01 {
public static void main(String[] args) {
Person person = new Person();
person.setName("hello");
person.setAge(6);
String jsonString = JSON.toJSONString(person);
System.out.println(jsonString);
}
}

//输出结果
构造函数
setName
setAge
getAge
getName
{"age":6,"name":"hello"}
  • JSON.toJSONString(person,SerializerFeature.WriteClassName)

Fastjson序列化时的一个配置选项,用于设置是否在序列化过程中写入类名。

在默认情况下,Fastjson序列化对象是不会写入类名的

如果添加了第二个参数SerializerFeature.WriteClassNameFastjson会在序列化对象时会多出一个**@type,即写入被序列化的对象的类名信息。这样在反序列化时,Fastjson会自动根据类名信息绑定正确的Java**类型,从而简化了反序列化的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class FJTest01 {
public static void main(String[] args) {
Person person = new Person();
person.setName("hello");
person.setAge(6);

String jsonString1 = JSON.toJSONString(person, SerializerFeature.WriteClassName);
System.out.println(jsonString1);
}
}

//输出结果
构造函数
setName
setAge
getAge
getName
{"@type":"org.example.Person","age":6,"name":"hello"}

反序列化

  • JSON.parse(String text)

解析为JSONObject类型或者JSONArray类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FJTest02 {
public static void main(String[] args) {
String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}";
Object parse = JSON.parse(jsonstring);
System.out.println(parse);
System.out.println(parse.getClass().getName());
}
}

//输出结果
构造函数
setAge
setName
org.example.Person@4459eb14
org.example.Person
  • JSON.parseObject(String text)

JSON文本解析成JSONObject类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FJTest02 {
public static void main(String[] args) {
String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}";
JSONObject jsonObject = JSON.parseObject(jsonstring);
System.out.println(jsonObject);
}
}

//输出结果
构造函数
setAge
setName
getAge
getName
{"name":"hello","age":6}
  • *JSON.parseObjectparseObject(String text, Class clazz)*

JSON文本解析成对应的**.class**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FJTest02 {
public static void main(String[] args) {
String jsonstring ="{\"@type\":\"org.example.Person\",\"age\":6,\"name\":\"hello\"}";
Person person = JSON.parseObject(jsonstring, Person.class);
System.out.println(person);
System.out.println(person.getClass().getName());
}
}

//输出结果
构造函数
setAge
setName
org.example.Person@4459eb14
org.example.Person

0x02 流程分析

反序列化

断点位置

1
JSONObject jsonObject = JSON.parseObject(jsonstring);

首先会到parseObject里面,去调用parse

1
2
3
4
5
6
7
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}

一路走到parse,创建了个DefaultJSONParser对象

1
2
3
4
5
6
7
8
9
10
public static Object parse(String text, int features) {
if (text == null) {
return null;
}
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();
parser.handleResovleTask(value);
parser.close();
return value;
}

parse中的DefaultJSONParser,会在该方法中盘断json字符串是否以**{[**开头,然后分配不同的解析流程,以其他字符开头的后面的解析过程中会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public DefaultJSONParser(final Object input, final JSONLexer lexer, final ParserConfig config){
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACE;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase) lexer).token = JSONToken.LBRACKET;
} else {
lexer.nextToken(); // prime the pump
}
}

比较重要的是parse中调用的parse方法,在parse里面会先创建一个空的JSONObject

然后再调用parseObject

1
2
3
4
5
6
7
8
9
10
public Object parse(Object fieldName) {
final JSONLexer lexer = this.lexer;
switch (lexer.token()) {
//省略部分代码......
case LBRACE:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return parseObject(object, fieldName);
//省略部分代码......
}
}

parseObject会先去获取key,也就是**@type**,会对其对应的值进行类加载操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public final Object parseObject(final Map object, Object fieldName) {
//省略部分代码......
try {
boolean setContextFlag = false;
for (;;) {
//省略部分代码......
Object key;
if (ch == '"') {
key = lexer.scanSymbol(symbolTable, '"');
lexer.skipWhitespace();
ch = lexer.getCurrent();
if (ch != ':') {
throw new JSONException("expect ':' at " + lexer.pos() + ", name " + key);
}
}
//省略部分代码......
}
} finally {
this.setContext(context);
}
}

如果匹配到**@Type,首先会利用TypeUtils.loadClass**去加载这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final Object parseObject(final Map object, Object fieldName) {
//省略部分代码......
try {
boolean setContextFlag = false;
for (;;) {
//省略部分代码......
if (key ==JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

//省略部分代码......
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}
//省略部分代码......
}
} finally {
this.setContext(context);
}
}

主要是来看一下getDeserializer,经过一系列的调用

getDeserializer#createJavaBeanDeserializer -> createJavaBeanDeserializer#JavaBeanInfo.build

创建了个derializer

跟进到bulid方法中,最主要的是后面的几个for方法

其作用是构建JavaBeanInfo对象,用于描述Java类的结构信息,包括字段、方法、构造函数等。

setter的循环中,如果添加到fieldList后,同时符合的getter也不会存进去

1
2
3
4
5
6
7
8
9
10
11
12
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy) {
//代码太多了,省略掉......

// setter methods
for (Method method : methods)
// public static fields
for (Field field : clazz.getFields())
// getter methods
for (Method method : clazz.getMethods())
return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList);

}

总结一下settergetter需要符合的条件有哪些

  • setter
1
2
3
4
长度大于4
非静态函数
返回类型为void或当前类
参数个数为1个
  • getter
1
2
3
4
长度大于4
非静态方法
无参数
返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

而且在创建derializer之前,反射加载的类会对类进行黑名单检查,在1.2.24后面的版本中会使用到

1
2
3
4
5
6
7
8
9
10
11
12
public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) {
//省略部分代码......
String className = clazz.getName();
className = className.replace('$', '.');
for (int i = 0; i < denyList.length; ++i) {
String deny = denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("parser deny : " + className);
}
}
//省略部分代码......
}

调用setter方法

之后一路返回到parseObject

调用setter方法主要是在deserialze

一路走到com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze方法中的**fieldDeser.setValue(object, fieldValue)**方法

package com.alibaba.fastjson.parser.deserializer.FieldDeserializer#setValue

有个method.invoke(object, value)调用了setter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final Object parseObject(final Map object, Object fieldName) {
//省略部分代码......
try {
boolean setContextFlag = false;
for (;;) {
//省略部分代码......
if (key ==JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
//省略部分代码......
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);
}
//省略部分代码......
}
} finally {
this.setContext(context);
}
}

调用getter方法

我们可以看到在上面的例子中,不只是调用了setter,还调用了getter

具体是在最开始调用parseObject的地方,有个toJSON方法中调用了getter方法

1
2
3
4
5
6
7
public static JSONObject parseObject(String text) {
Object obj = parse(text);
if (obj instanceof JSONObject) {
return (JSONObject) obj;
}
return (JSONObject) JSON.toJSON(obj);
}

一路走到toJSON,在toJSON中调用了getObjectWriter方法

主要是用来根据指定的类信息获取对应的ObjectSerializer对象

先通过缓存查找,若找不出,在根据ObjectSerializerFactory工厂对象中查找对应的ObjectSerializer实例

1
2
3
4
5
6
7
8
public static Object toJSON(Object javaObject, SerializeConfig config) {
//省略部分代码......

ObjectSerializer serializer = config.getObjectWriter(clazz);


//省略部分代码......
}

接着往下有个if判断,主要关注try中的getFieldValuesMap方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static Object toJSON(Object javaObject, SerializeConfig config) {
//省略部分代码......
if (serializer instanceof JavaBeanSerializer) {
JavaBeanSerializer javaBeanSerializer = (JavaBeanSerializer) serializer;
JSONObject json = new JSONObject();
try {
Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);
for (Map.Entry<String, Object> entry : values.entrySet()) {
json.put(entry.getKey(), toJSON(entry.getValue()));
}
} catch (Exception e) {
throw new JSONException("toJSON error", e);
}
return json;
}
//省略部分代码......
}

getFieldValuesMap中的for循环里,调用了getPropertyValue方法

1
2
3
4
5
6
7
8
9
public Map<String, Object> getFieldValuesMap(Object object) throws Exception {
Map<String, Object> map = new LinkedHashMap<String, Object>(sortedGetters.length);

for (FieldSerializer getter : sortedGetters) {
map.put(getter.fieldInfo.name, getter.getPropertyValue(object));
}

return map;
}

getPropertyValue方法中又调用了get方法

1
2
3
4
public Object getPropertyValue(Object object) throws InvocationTargetException, IllegalAccessException {
Object propertyValue = fieldInfo.get(object);
//省略部分代码......
}

跟进去get方法后可以看到,利用invoke调用了getter方法

1
2
3
4
5
6
7
public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {
if (method != null) {
Object value = method.invoke(javaObject, new Object[0]);
return value;
}
return field.get(javaObject);
}

0x03 反序列化漏洞

根据前面的分析,可以知道parse会调用setter方法,parseObject会调用settergetter方法,并且都会调用构造函数

由于存在autoType特性,所以说

如果**@type中标识的类里的构造函数settergetter存在恶意代码,那么就可能存在fastjson**反序列化漏洞

Demo

  • CalcTest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CalcTest {
public String calc;

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

public String getCalc() {
System.out.println("getter");
return calc;
}

public void setCalc(String calc) throws IOException {
this.calc = calc;
Runtime.getRuntime().exec("calc");
System.out.println("setter");
}
}
  • FastjsonTest
1
2
3
4
5
6
public class FastjsonTest {
public static void main(String[] args) {
String JsonCalc = "{\"@type\":\"org.example.CalcTest\",\"Calc\":\"kkk\"}";
JSON.parseObject(JsonCalc);
}
}

成功执行了setter中的代码

0x04 总结

根据上面的分析可以知道,我们只要找到合适的JavaBean,并且满足以下两点就有可能存在漏洞点

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

0x05 参考资料

https://goodapple.top/archives/832

https://drun1baby.top/2022/08/04/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Fastjson%E7%AF%8701-Fastjson%E5%9F%BA%E7%A1%80/#Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Fastjson-%E7%AF%87-01-Fastjson-%E5%9F%BA%E7%A1%80

https://meizjm3i.github.io/2019/06/05/FastJson%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E8%A7%A3%E6%9E%90%E6%B5%81%E7%A8%8B/