初识FastJson

Background

FastJson提供了toJsonString和parseObject方法可用于将一个JavaBean转换为Json数据, 或将Json数据还原为JavaBean对象。

FastJson Version:1.2.24

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

...
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>
</project>

Java Bean序列化Json

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
public class FastJsonLearning {
public static void main(String[] args) {
User user = new User("Search?=Null", 19);
String user_json = JSON.toJSONString(user);
System.out.println(user_json);
}
}

class User {
public String name;
public int age;

User (String name, int age) {
System.out.println("User Constructor Called.");
this.name = name;
this.age = age;
}

public String getName() {
System.out.println("getName Called.");
return name;
}

public void setName(String name) {
System.out.println("setName Called.");
this.name = name;
}

public int getAge() {
System.out.println("getAge Called.");
return age;
}

public void setAge(int age) {
System.out.println("setAge Called.");
this.age = age;
}
}

输出:

{“age”:19,”name”:”Search?=Null”}

其在序列化对象为Json时会调用Bean对象的get方法(构造方法是实例化对象时调用的, 非序列化所调用):

image-20210305105409656

相反, 在将Json转换为Java对象时也会调用其set方法:

image-20210305105750608

探测FastJson

1
{"@type":"java.net.InetAddress","val":"dnslog"}

使用如上Payload当dnslog收到请求时则证明后端使用了FastJson, 为何该Payload能够发起DNS请求。跟随本文从parseObject开始分析。

com.alibaba.fastjson.JSON#parseObject

接收json数据调用parse方法:

image-20210305143746340

调用parse方法传入json数据和DEFAULT_PARSE_FEATURE(989):

image-20210305143946463

实例化com.alibaba.fastjson.DefaultJSONParser对象时其会判断JSON数据的第一个是否为{还是[, 根据不同结果赋值token, 然后调用DefaultJSONParser#parse

image-20210305151827008

进入case 12的分支实例化JSONObject对象后调用parseObject方法:

image-20210305152130557

在parseObject中的while循环所谓又长又臭(足足400余行代码), 遂只能一行一行琢磨该循环中都做了哪些事。循环的第一行代码便是调用skipWhitespace方法, 在该方法中会判断字符是否为空或换行符等, 因此在JSON数据中加入空格和换行符或许可以规避WAF的检测并正常解析:

image-20210305153126409

1
2
3
// 如下两种形式均可解析发起DNS请求
{"@type":"java.net.InetAddress","val":"dnslog"}
{\r\n"@type":/**/"java.net.InetAddress","val":/**/"dnslog"}

回到循环中继续向下看代码, 如果AllowArbitaryCommas开启的话(默认开启)并且当前字符为,号会跳过该字符继续向下解析。因此这部分代码是用于跳过JSON数据中的空格, 换行符, 及逗号的解析。

image-20210306142713240

下表为复现版本中各配置项属性默认值:

属性 默认值
AutoCloseSource True
AllowComment False
AllowUnQuotedFieldNames True
AllowSingleQuotes True
InternFieldNames True
AllowISO8601DateFormat False
AllowArbitraryCommas True
UseBigDecimal True
IgnoreNotMatch True
SortFeidFastMatch True
DisableASM False
DisableCircularReferenceDetect False
InitStringFieldAsEmpty False
SupportArrayToBean False
OrderedField False
DisableSpecialKeyDetect False
UseObjectArray False
SupportNonPublicField False

从代码172行开始就是正式解析JSON数据的部分了, 当前字符为"时调用scanSymbol方法获取其双引号中的内容为键, 获得完整的键名后如果其后面的字符不为:则会抛出异常。

image-20210306143937229

获取到键的值后调用resetStringposition重置字符串位置为0, 判断key如果是@type并且DisableSpecialKeyDetect为关闭情况下, 依旧通过scanSymbol获取value的值(value是完整类名)。通过TypeUtils.loadClass加载类。

image-20210306150328410

TypeUtils#loadClass中获取当前线程的类加载器加载类并写入mappings中:

image-20210306214952645

加载完类后nextToken用于判断当前字符为逗号时跳过逗号继续解析, 接着通过ParserConfig#getDeserializer方法获取ParserConfig对象deserializer属性指定Key的值。

image-20210307141009149

其返回的是一个MiscCodec对象:

image-20210307141103868

最后调用MiscCodec.deserialze(DefaultJSONParser, java.net.InetAddress.class, null)并return该返回值。对应代码:

1
2
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;

MiscCodec#deserialze在else分支中判断键的内容为val后解析其value的内容:

image-20210307142615526

最终在MiscCodec322行处会调用InetAddress#getByName发起DNS请求:

image-20210307142925475

总结

  • FastJson1.2.24版本默认开启AutoType
  • FastJson1.2.24默认会忽略空格 注释符并可以解析十六进制和unicode字符
  • FastJson1.2.24可使用白名单的InetAddress类进行DNS探测