前言

这是一篇炒冷饭性质的文章,距离FastJson的反序列化漏洞曝出已经过去了差不多两年的时间,网上相关的漏洞分析文章也是十分充足,但是我最近在阅读相关文章的时候发现普遍都是分析漏洞利用环节,许多解析部分的细节便不可得知。于是用了些时间分析了下其解析流程,也就有了本文。本篇内容主要为FastJson反序列化解析流程中涉及到的类及函数的分析。

整体调用栈

解析过程中的关键类

在用以调试的demo中,处理JSON数据的语句为:

1
JSONObject obj = JSON.parseObject(data,Feature.SupportNonPublicField);

整个JSON数据的处理也就是从parseObject这个函数开始。

com.alibaba.fastjson.parser.DefaultJSONParser

从demo开始,进入的第一个类本应是JSON,但是因为JSON类并未进行关键操作,因此就采取忽略,转而关注在com.alibaba.fastjson.JSON:74处调用的DefaultJSONParser。

在DefaultJSONParser中,我们需要关注的函数有两个:

借助这两个函数,DefaultJSONParser完成了对于JSON数据的解析。

com.alibaba.fastjson.parser.deserializer.FieldDeserializer

在Java反射中,可以通过Field获取某个类的属性或该属性的值,类FieldDeserializer用于存储字段所对应的反序列化器,FiledDeserializer会存储至ObjectDeserializer中,其中Key对应为字段名称。

FieldDeserializer类中,主要由几类函数组成:

com.alibaba.fastjson.parser.JavaBeanDeserializer

JavaBeanDeserializer是一个反序列化器,也就是我们上面在FieldDeserializer中提到的"反序列器"。

JavaBeanDeserializer在进行反序列化时,会接受如下参数:

1
protected <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName, Object object, int features)

解析过程中的关键函数

com.alibaba.fastjson.parser.DefaultJSONParser$parseObject

在此函数中,DefaultJSONParser会完成对于JSON数据的处理,我们所追求的命令执行也是在这一步中实现。该函数的主体实现包裹在一个while(True)循环中,在这个while循环中,会不断取JSON数据中的下一个token(也就是字符)进行处理:

1
2
3
4
while(true) {
lexer.skipWhitespace();
char ch = lexer.getCurrent(); /* 比如 ch = '"' */
...

以下面这个demo为例子:

1
{"name":{"@type":"..."}}

会首先取出name添加至SymbolTable,再深入对name键对应的数据进行循环处理。

com.alibaba.fastjson.parser.JSONLexerBase$scanSymbol

scanSymbol主要调用点在刚才提到的parseObject中:

1
key = lexer.scanSymbol(this.symbolTable, '"');

scanSymbol函数中,会首先取出键,比如下面这个例子中的name:

1
{"name":"..."}

在取出键之后,会调用addSymbol函数,将name这个键名添加至SymbolTable中:

com.alibaba.fastjson.parser.ParserConfig$deserialze

谈及该函数,必然需要理解ParserConfig类,ParserConfig在反序列化的过程维护了常用类型和反序列化器的对应关系,并将该对应关系存放至IdentityHashMap。我们可以通过getDeserializer方法获得对象反序列化器ObjectDeserializer

当出现未定义的类型时,则会在获取该类型的反序列化器的同时建立起对应关系,并存放至IdentityHashMap。为了更好地理解它,可以举一个例子:ParserConfig$getDeserializer(Type type),我们可以分析其实现源码:

1
2
3
4
5
6
7
8
9
10
11
ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
if (derializer != null) {
return derializer;
} else if (type instanceof Class) {
return this.getDeserializer((Class)type, type);
} else if (type instanceof ParameterizedType) {
Type rawType = ((ParameterizedType)type).getRawType();
return rawType instanceof Class ? this.getDeserializer((Class)rawType, type) : this.getDeserializer(rawType);
} else {
return JavaObjectDeserializer.instance;
}

该函数为获取ParserConfig中存储的反序列化器,当反序列化器存在时,则直接将其返回,不存在时则调用getDeserializer(Class<?> clazz, Type type),获取类与反序列器的对应关系,并在最后存入IdentityHashMap:

1
this.putDeserializer((Type)type, (ObjectDeserializer)derializer);

parseField

由于该函数存在重名实现,因此以序号分开。

  1. com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer$parseField
  2. com.alibaba.fastjson.parser.deserializer.DefaultFieldDeserializer$parseField

虽然上述的两个函数重名,然而在一般情况下,DefaultFieldDeserializer$parseFieldJavaBeanDeserializer$parseField中被调用:

1
2
3
4
5
else {
lexer.nextTokenWithColon(((FieldDeserializer)fieldDeserializer).getFastMatchToken());
((FieldDeserializer)fieldDeserializer).parseField(parser, object, objectType, fieldValues);
return true;
}

JavaBeanDeserializer$parseField中,当检测到存在与传入的Key所匹配的反序列化器时,便会调用DefaultFieldDeserializer$parseField进行反序列化。

当进入DefaultFieldDeserializer$parseField时,我们可以看到this中已经存在fieldValueDeserializer了:

随后调用对应的反序列化器,这里以我图中为例:

对应的语句为:

1
value = this.fieldValueDeserilizer.deserialze(parser, fieldType, this.fieldInfo.name);

com.alibaba.fastjson.parser.deserializer.FieldDeserializer$setValue

setValue接受两个传参:

1
public void setValue(Object object, Object value)

object为类对象,value为属性值。

setValue中会进行字段的赋值操作,对object的get方法进行触发,当检测到method返回的类型与Map类型一致时,则会进一步触发反射,完成RCE,关键代码如下所示:

1
2
3
4
5
6
 else if (Map.class.isAssignableFrom(method.getReturnType())) {
Map map = (Map)method.invoke(object);
if (map != null) {
map.putAll((Map)value);
}
}

其中,method为:

com.alibaba.fastjson.util.JavaBeanInfo.JavaBeanInfo$build

build函数接受三个参数传入:

1
public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy)

build函数中,通过反射获得传入参数的clazz类及父类的所有方法,随后遍历这些方法,将其中的set方法以及get方法取出,如图中fields所示:

其中,beanInfo的生成的语句为:

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

Reference