作者:Lucifaer
博客:https://www.lucifaer.com/
在廖大2017年的一篇博文中就对Fastjson的反序列化流程进行了总结:
在具体的跟进中也可以很清晰的看到如图所示的架构。
对于编程人员来说,只需要考虑Fastjson所提供的几个静态方法即可,如:
并不需要关注json序列化及反序列化的过程。深入Fastjson框架,可以看到其主要的功能都是在DefaultJSONParser
类中实现的,在这个类中会应用其他的一些外部类来完成后续操作。ParserConfig
主要是进行配置信息的初始化,JSONLexer
主要是对json字符串进行处理并分析,反序列化在JavaBeanDeserializer
中处理。
在真实的调试过程中会遇到一些非常好玩的问题,而在其它文章中并没有对这些进行完整的叙述,我这里结合自己的理解来说一说。以下的调试的例子的demo为:
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字符串)的处理过程简单来说分为这几部分:
DefaultJSONParser
的初始化
这一步是parseObject()
是否指定了第二个参数,也就是是否指定了clazz
字段:
clazz
字段,则首先根据clazz
类型来获取相应deserializer
,如果不是initDeserializers
中的类的话,则会调用JavaBeanDeserializer#deserialze
转交FastjsonASMDeserializer
利用Fastjson自己实现的ASM流程生成处理类,调用相应的类并将处理流程转交到相应的处理类处理json字符串内容。(这里的描述有一些些问题,后面会尽量相近的描述一下)StringCodec
类来处理json字符串。最终都转交由DefaultJSONParser#parse
中根据lexer.token
来选择处理方式,这里的例子中都为12也就是{
(因为要处理json字符串需要一个起始标志位,所以判断当前json字符串的token是很重要的),接下来就是对json字符串进行处理(这里是一个循环处理,摘取类似"name":"123"
这样的关系)。
判断解析的json字符串中是否存在symbolTable
中的字段(如@type
,$ref
这样的字段),如果出现了@type
则交由public final Object parseObject(final Map object, Object fieldName)
来处理,然后重复步骤2的过程知道执行成功或报错。
初始化过程非常的简单,分两部分,一部分为ParserConfig
的初始化,另外一部分为DefaultJSONParser
的初始化。
ParserConfig
的初始化是在com.alibaba.fastjson.JSON
中调用的:
一路跟到ParserConfig#ParserConfig
方法中:
前面指定了asm的工厂类,并进行了实例化,后面是初始化deserializers
,将用户自定义黑白名单加入到原有的黑白名单中。
DefaultJSONParser
的初始化是在com.alibaba.fastjson.JSON#parseObject
中调用并完成的:
这里初始化了DefaultJSONParser
之后调用了其parseObject
方法进行后续的操作。
跟进DefaultJSONParser
可以看到JSONScanner
的实例化以及lexer.token
的初始化设置:
进入到这里步就稍微有点复杂了,需要仔细跟进一下。根据上一节我们可以看到完成初始化操作后主要的处理流程集中于T value = (T) parser.parseObject(clazz, null);
这一步的操作中,跟进看一下具体流程:
简单来说就是一个根据type获取对应的derializer
并且调用derializer.deserialze
进行处理的过程,这里的config是之前初始化的ParserConfig
。这里要注意的是type
这个参数,跟踪了整个流程后会发现,如果在写代码时指定了第二个参数如Group group = JSON.parseObject(jsonString, Group.class);
则第二个参数也就是Group.class
即为type
如果未指定第二个参数的话将会获取第一个参数的类型作为type
,当未指定第二个参数的时候将会调用与第一个参数类型相符的方法来处理:
了解了这些后,就可以跟进看一下getDeserializer
的实现了:
首先会尝试在deserializers
中匹配type
的类型,如果匹配到了就返回匹配的derializer
,否则就判断是否是Class泛型的接口,如果是则调用getDeserializer((Class<?>) type, type)
继续处理,这一部分代码很长,我只截最关键的一个地方:
当类不显式匹配上面的情况时,就会调用createJavaBeanDeserializer
来创建一个新的derializer
,并将其加入到deserializers
这个map中。接下来跟进createJavaBeanDeserializer
的处理流程,我截取了关键的一部分:
在这里首先会根据类名和propertyNamingStrategy生成beanInfo,之后利用asm工厂类的createJavaBeanDeserializer
生成处理类:
写过asm的应该可以一眼看出这里是用asm来生成处理类,分别生成构造函数,deserialze
方法和deserialzeArrayMapping
方法。我们来看一下asm生成的类是什么样的。这里由于代码很多我只截取一些关键的地方:
至此便完成了利用asm生成处理类的过程了。
上一节中我们已经动态生成了FastjsonASMDeserializer_1_Group
这个处理类,那么现在可以继续向下跟进,看看后续的处理流程是怎么样的。
首先,跟进一下构造函数:
这里利用createFieldDeserializer
将type
类中的变量等信息转换为FieldDeserializer
类型,并存储到sortedFieldDeserializers
这个数组中,这里可以记一下这个数组的名字,后面会用到:
在完成构造函数后,根据上文的跟踪,就会调用asm生成的处理类中的deserialze
方法,由于我这里是把生成的bytecode抓下来写成文件来看的,所以很多东西看的不是很清晰,但是整段处理的关键点在于最后的return:
其中的各个参数为:
跟进parseRest
来看一下:
这里直接调用了JavaBeanDeserializer#deserialze
。这里我截取几处比较关键的代码:
这里需要注意的有两个变量:beanInfo
和sortedFieldDeserializers
,这两个变量的生成过程上文都有提及,根据这两个变量的值,我们能很好的理解JavaBeanDeserializer#deserialze
这部分的代码,这里会遍历整个sortedFieldDeserializers
中所有的key,并尝试根据类型来提取jsonstring中相应的信息,如果成功则转交给asm生成的处理类的createInstance实例化对象,如果不成功则扫描jsonstring中是否具有特殊的指令集,如果有,则尝试解析指令集否则就报错。下面具体看一下处理的流程:
如果失败则尝试解析指令集:
可以看到这里会尝试解析$ref
和@type
,如果匹配到了@type
且其内容为string,则尝试利用lexer.stringVal()
通过字符串截取来获取其内容:
但是由于我们发送的jsonstring中是没有与sortedFieldDeserializers
所对应的键名的,所以这里仍无法匹配到。因为没有办法找到与设定的type相符的键,这个时候获取到的内容为空,fastjson会将当前这个字段判断为一个键值,根据当前符号的下一个符号来判断这个键所对应的值是什么类型,如果是{
则这个键所对应的值也是一个key-value的格式,如果是"
则为具体的值。在当前例子中,我们知道下一个字段应为{
,fastjson在处理时会再次调用parseObject
来处理这个新的键值对格式,下面便是如何将处理流程转交parserObject
进行二次处理的过程。这里需要用到FieldDeserializers
来进行解析了:
跟进parseField
中,关键的处理流程为:
继续跟进:
这里首先通过fieldInfo.fieldClass
和fieldInfo.fieldType
来获取fieldValueDeserilizer
由于这里对应的jsonstring是string类型,则这里最后获取到的fieldValueDeserilizer
是StringCodec
。所以接下来就是跟进StringCodec#deserialze
中:
传入的clazz应为String类型,而非StringBuffer或StringBuilder,所以继续跟进deserialze
:
最终调用DefaultJSONParser#parse
解析jsonstring:
现在解析的位置应为{
所对应的的token,所以应为12,也就是LBRACE,这里将调用parseObject
来对jsonstring进行解析,我这里截取关键部分:
在这段代码的前面都是lexer对jsonstring的截取和处理操作,当检测到jsonstring中含有以@type
为键名的字段后,获取其值,将值传入checkAutoType
中做长度检测以及黑白名单的检测:
如果通过的话,则调用config.getDeserializer
获取clazz的类:
根据jsonstring中的val
字段来获取obj的值:
这里将objVal
的名称以字符串的形式赋值给strVal
。后面会根据clazz
的类型将处理流程转交给不同的流程这里由于指定了java.lang.class
所以是转交到TypeUtils.loadClass
来处理的:
前面将对传入的className
进行解析,如果符合相应格式就会进行相应的解析(这里也是之前漏洞所在地),而后面的则会判断cache
是否为true
,如果为真则将实例化后的类加入到mappings中(这也是这次漏洞的核心),最终都将把实例化后的类进行返回。
其实在前文都有涉及,在这里将化繁为简,总结一下关键点在哪几个地方。
纵观整个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的解析方式会首先判断当前标志位是什么,这里拿完整的解析过程来举个例子:
最开始解析的标志位为{
判断下一个标志位是否为"
,如果是"
则提取key值,这时的标志位为"
。
判断下一个标志位是否为:
:
:
则判断下一个标志位是否为"
,如果是,则获取value值,这时的标志位为"
。{
则重复1、2的过程。判断下一个标志位是否为}
:
}
则表示这一个单元的解析结束,
则表示要解析下一个kv的数据,重复1、2、3根据不同的标志位进行不同的解析。当解析的过程中碰到了@type
或$ref
时,将当做特殊的标志做相应的处理。
当解析过程中找到了@type
这个关键的标志时,将提取其所对应的值,并检测这个值是否在黑名单中:
先过黑名单再过白名单,这样保证了@type
所引用的类是较为安全的。
jsonstring经过解析且经过安全性验证后,最终都要变成相应的对象,而变成对象的过程就是利用反射完成的,这个过程就是反序列化的过程。而该过程主要在DefaultJSONParser#parseObject
中调用deserializer.deserialze()
完成:
这里会根据@type
所指定的类来获取或生成反序列化类,完成反序列化过程,这里如果是在预定数组中的类的话就可以直接调用相关类的deserialze
方法完成反序列化操作:
如果没有则会进入asm创建处理类的流程。
在具体进行反射前还有一个操作,将会解析看jsonstring中是否存在val
字段,如果有,则将其提取出来赋给objVal
,并将objVal
的类名赋值给strVal
:
之后根据clazz
类型交由不同的流程来处理:
当clazz
是一个class类型时,就会进入TypeUtils.loadClass
中根据strVal
进行类调用:
这里有两个点需要注意,而这两个点就是造成Fastjson两个rce的关键点。
第一个点会对传入的@type
的值进行解析,如果符合相应的格式则直接进行类加载。
第二个点首先会反射调用@type
的值所设置的类,然后将其加入到mappings中,当后面再次经过checkAutoType
时,将会调用:
将首先从mappings中获取和typeName
相同的类,也就是说这里在进行黑名单检测前就已经返回了类,从而绕过了黑名单。
就目前来说,针对Fastjson的攻防集中于对于@type
的检测的利用以及黑名单的绕过这两部分。而从整体的运行逻辑上来看,由于Fastjson很多地方写的比较死,很难出现重新调用构造方法覆盖黑名单或者覆盖mapping的操作,所以就现在最新版的Fastjson而言是比较难以绕过防护措施的。
未来可以参考struts2 ognl的攻防手法,看是否能从置空黑名单或者操作mappings来尝试绕过防护。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/994/