FastJson反序列化分析笔记
This_is_Y Lv6

两个特性

用户可控制反序列化的对象

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package org.example;

public class user {
private String name;
private int age;
private String hobby;

public user() {
System.out.println("user构造函数user(无参数)");
}

public user(String name, int age, String hobby) {
System.out.println("user构造函数user(有参数)");
this.name = name;
this.age = age;
this.hobby = hobby;
}

public String getName() {
System.out.println("user调用了getName");
return name;
}

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

public int getAge() {
System.out.println("user调用了getAge");
return age;
}

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

public String getHobby() {
System.out.println("user调用了getHobby");
return hobby;
}

public void setHobby(String hobby) {
System.out.println("user调用了setHobby");
this.hobby = hobby;
}

@Override
public String toString() {
return "user{" +
"name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}

然后是调用函数,我都是写在各种小test里,然后在main中调用

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

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

public class Main {
public static void main(String[] args) {
//test1();
//test2();
test3();
//test4();
}
}

安全的使用方法:

先看正确的使用方法:

1
2
3
4
5
6
    public static void test3(){
// 安全的写法
String p1 = "{\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
user tmpuser = JSON.parseObject(p1, user.class); //限制死了反序列化的类型为user
System.out.println(tmpuser.getAge());
}

image-20231027102647188

可以看到输出中,出现了

user构造函数user(无参数)

这是因为需要构造user对象tmpuser。

user调用了setAge user调用了setHobby user调用了setName

这三个出现,是因为在构造过程中,给age,name,hobby这三个属性赋值了。

user调用了getAg
18

是tmpuser.getAge()这一行输出的。

限制死了反序列化的类型为user,这种由开发者控制的反序列化的类型,是安全的。

不安全的使用方法:

因为fastjson有一个特性,可以由用户指定反序列化的类型,通过@type字段。可以先设置另一个user2对象

代码如下

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package org.example;

public class user2 { private String name;
private int age;
private String hobby;

public user2() {
System.out.println("user2构造函数user2(无参数)");
}

public user2(String name, int age, String hobby) {
System.out.println("user2构造函数user2(有参数)");
this.name = name;
this.age = age;
this.hobby = hobby;
}

public String getName() {
System.out.println("user2调用了getName");
return name;
}

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

public int getAge() {
System.out.println("user2调用了getAge");
return age;
}

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

public String getHobby() {
System.out.println("user2调用了getHobby");
return hobby;
}

public void setHobby(String hobby) {
System.out.println("user2调用了setHobby");
this.hobby = hobby;
}

@Override
public String toString() {
return "user{" +
"name='" + name + '\'' +
", age=" + age +
", hobby='" + hobby + '\'' +
'}';
}
}

然后是调用代码:

1
2
3
4
5
6
7
8
public static void test4() {
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
String p2 = "{\"@type\":\"org.example.user2\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parse(p1):");
Object P1 = JSON.parse(p1);
System.out.println("JSON.parse(p2):");
Object P2 = JSON.parse(p2);
}

输出结果如下:

image-20231027111614060

可以将test()理解为一个函数,用户输入序列号后的字符串交给服务器,服务器对字符串进行反序列化,这里输入的p1,p2就可以理解为调用了两次函数。

调用getName方法

但反序列化字符串的函数不是parse而是parseObject时,就会调用对象中的getter类方法,比如getName,getAge,getHobby。

1
2
3
4
5
6
7
public static void test6() {
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parse(p1):");
Object P1 = JSON.parse(p1);
System.out.println("JSON.parseObject(p1):");
Object P2 = JSON.parseObject(p1);
}

image-20231027112258302

在了解这两种机制后,就可能会出现下面这种情况:代码中出现了 JSON.parseObject(p1); 可能设计初衷只是想解析反序列化”{"age":18,"hobby":"gaming","name":"y"}”这样的字符串,但是因为这两个特性,就可能出现一些意想不到的结果

调用流程分析

先来捋顺正常的调用,再来看漏洞的调用

正常流程

识别@type

使用如下代码,

1
2
3
4
5
public static void test7(){
String p1 = "{\"@type\":\"org.example.user\",\"age\":18,\"hobby\":\"gaming\",\"name\":\"y\"}";
System.out.println("JSON.parseObject(p1):");
Object P2 = JSON.parseObject(p1);
}

断点下载 Object P2 = JSON.parseObject(p1);

前面都是普通的调来调去,一直到这里

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);
…………
…………

image-20231030110516526

这里的lexer是由之前JSON.java中的

DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);

这一行定义和赋值

在swtich中,识别到第一个字符串是{ (也就是 LBRACE )后,通过

return parseObject(object, fieldName);

进入到了parseObject函数中,这个函数还是在当前DefaultJSONParser.java文件中

image-20231030111221334

经过几个判断后,进入到try finally 代码块中,这里面是一个for(;;)死循环,然后可以看到有一个判断

1
2
3
4
5
6
7
8
9
10
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
String typeName = lexer.scanSymbol(symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

if (clazz == null) {
object.put(JSON.DEFAULT_TYPE_KEY, typeName);
continue;
}
………………
………………

JSON.DEFAULT_TYPE_KEY@type

lexer.isEnabled()会返回入参的mask字段,这边的Feature.DisableSpecialKeyDetect内容如下,所以!lexer.isEnabled(Feature.DisableSpecialKeyDetect)的结果为true

image-20231030112553007

随后就获取到@type的value:org.example.user

这边的意思大概就是,通过识别到{开头,然后开始进行反序列化的字符从识别,识别到@type,意味着需要指定类进行java反序列化。这边的下一行

Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());

就是加载制定的类以便于反序列化

加载指定类

在上面用loadlass加载完类后,继续这个if代码块,在最下面有一个

1
2
ObjectDeserializer deserializer = config.getDeserializer(clazz);
return deserializer.deserialze(this, clazz, fieldName);

这边是为了获取反序列化器deserializer,跟进getDeserializer,因为type instanceof Class<?>,跳到 getDeserializer((Class<?>) type, type)中,

在这个getDeserializer,会经过一些替换处理,if判断,比如说会把$换成.

  • className = className.replace(‘$’, ‘.’);

会检测是否java.awt.开头等等。

最后因为传入的value是org.example.user,流程会来到461行的

derializer = createJavaBeanDeserializer(clazz, type);

image-20231030115601476

跟进createJavaBeanDeserializer(),这个函数里的判断主要是围绕 asmEnable 变量的true和false来进行的,在一系列的处理后,来到了

1
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);

image-20231030144038009

跟进这个build()函数,这函数内容也非常多,不过因为两个长的 if 代码块不会运行,所以可以忽略,重点是下面的三个 for 代码块,

image-20231030144752366

其中第一个是在指定的java类中找setter类方法。那些“函数命名以set开头,第四个字母大写”之类的条件,应该就是在这里进行判断处理的。

image-20231030145529136

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
String methodName = method.getName();
if (methodName.length() < 4) { //方法名长度大于4
continue;
}

if (Modifier.isStatic(method.getModifiers())) { //非静态方法
continue;
}

// support builder set
if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}
Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

JSONField annotation = method.getAnnotation(JSONField.class);

if (annotation == null) {
annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
}

if (annotation != null) {
if (!annotation.deserialize()) {
continue;
}

ordinal = annotation.ordinal();
serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
parserFeatures = Feature.of(annotation.parseFeatures());

if (annotation.name().length() != 0) {
String propertyName = annotation.name();
add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, null, null));
continue;
}
}

if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}

char c3 = methodName.charAt(3);

String propertyName;
if (Character.isUpperCase(c3) //
|| c3 > 512 // for unicode method name
) {
if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}

随便加点什么

 Comments