作者:天融信阿尔法实验室
公众号: https://mp.weixin.qq.com/s/vsFRpyPTmj-h3kk6KhEfeg
FastJson是alibaba的一款开源JSON解析库,可用于将Java对象转换为其JSON表示形式,也可以用于将JSON字符串转换为等效的Java对象。近几年来fastjson漏洞层出不穷,本文将会谈谈近几年来fastjson RCE漏洞的源头:17年fastjson爆出的1.2.24反序列化漏洞。以这个漏洞为基础,详细分析fastjson漏洞的一些细节问题。
关于Fastjson 1.2.24反序列化漏洞,自从17年以来已经有很多人分析过了,一些基础内容本文就不再陈述了。此次漏洞简单来说,就是Fastjson通过parseObject/parse将传入的字符串反序列化为Java对象时由于没有进行合理检查而导致的
本文将着重分析一下这个漏洞没有被详细介绍过的细节问题,如下:
1、parseObject(String text) 、parse (String text)、 parseObject(String text, Class/<T> clazz)三个方法从代码层面上来看,究竟有何不同?
2、使用TemplatesImpl攻击调用链路构造poc时,为什么一定需要构造 _tfactory
以及 _name
字段?
3、 _outputProperties
与其getter方法getOutputProperties()方法名字并不完全一致是如何解决的?
除此之外,本文在介绍TemplatesImpl攻击调用链路时,以模拟寻找漏洞利用链的思路,从最终的执行点开始向上寻找入口,模拟还原出挖掘这个TemplatesImpl利用链的完整过程。
关于parse (String text) 、parseObject(String text)、 parseObject(String text, Class/<T> clazz)三个方法,我们进行一个测试
FastJsonTest类中变量以及其setter/getter关系如下表
public String t1 | private int t2 | private Boolean t3 | private Properties t4 | private Properties t5 | |
---|---|---|---|---|---|
setter | 有 | 有 | 无 | 无 | 有 |
getter | 有 | 有 | 有 | 有 | 有 |
接下来,我们分别使用下图三种方式分别将JSON字符串反序列化成Java对象
1、Object obj = JSON.parse(jsonstr);
2、Object obj = JSON.parseObject(jsonstr, FastJsonTest.class);
3、Object obj = JSON.parseObject(jsonstr);
首先我们运行一下Object obj = JSON.parse(jsonstr);这种方式
结果:
setT1() 、setT2() 、getT4() 、setT5() 被调用
JSON.parse(jsonstr)最终返回FastJsonTest类的对象
接着我们运行下Object obj = JSON.parseObject(jsonstr, FastJsonTest.class);
结果:
与JSON.parse(jsonstr);这种方式一样setT1() 、setT2() 、getT4() 、setT5() 被调用
JSON.parse(jsonstr)最终返回FastJsonTest类的对象
最后我们运行下Object obj = JSON.parseObject(jsonstr);
结果:
这次结果与上两次大不相同,FastJsonTest类中的所有getter与setter都被调用了,并且 JSON.parseObject(jsonstr);
返回一个JSONObject对象
通过上文运行结果,不难发现有三个问题
使用JSON.parse(jsonstr);与JSON.parseObject(jsonstr, FastJsonTest.class);两种方式执行后的返回结果完全相同,且FastJsonTest类中getter与setter方法调用情况也完全一致,parse(jsonstr)与parseObject(jsonstr, FastJsonTest.class)有何关联呢?
使用JSON.parse(jsonstr);与JSON.parseObject(jsonstr, FastJsonTest.class);两种方式时,被调用的getter与setter方法分别为setT1()、setT2()、setT5()、getT4()。FastJsonTest类中一共有五个getter方法,分别为getT1()、getT2()、getT3()、getT4()、getT5(),为什么仅仅getT4被调用了呢?
JSON.parseObject(jsonstr);为什么返回值为JSONObject类对象,且将FastJsonTest类中的所有getter与setter都被调用了
经过调试可以发现,无论使用JSON.parse(jsonstr);或是JSON.parseObject(jsonstr,FastJsonTest.class);方式解析json字符串,程序最终都会调用位于com/alibaba/fastjson/util/JavaBeanInfo.java中的JavaBeanInfo.build()方法来获取并保存目标Java类中的成员变量以及其对应的setter、getter
首先来看下 JSON.parse(jsonstr)
这种方式,当程序执行到JavaBeanInfo.build()方法时情景如下图
此时的调用链如下图
此时传入 JavaBeanInfo.build()
方法的参数值如下图
再来看下 JSON.parseObject(jsonstr,FastJsonTest.class)
这种方式,当程序执行到 JavaBeanInfo.build()
方法时情景如下图
此时的调用链如下图
此时传入 JavaBeanInfo.build()
方法的参数值如下图
二者执行到 JavaBeanInfo.build()
方法时调用链对比如下
可见二者后面的调用链是完全一样的。二者不同点在于调用 JavaBeanInfo.build()
方法时传入clazz参数的来源不同:
JSON.parseObject(jsonstr, FastJsonTest.class)
在调用 JavaBeanInfo.build()
方法时传入的clazz参数源于parseObject方法中第二个参数中指定的“FastJsonTest.class”。
JSON.parse(jsonstr);
这种方式调用 JavaBeanInfo.build()
方法时传入的clazz参数获取于json字符串中/@type字段的值。
关于 JSON.parse(jsonstr);
从json字符串中/@type字段获取clazz参数,具体代码如下
程序通过解析传入的json字符串的/@type字段值来获取之后传入JavaBeanInfo.build()方法的clazz参数
因此,只要Json字符串的/@type字段值与JSON.parseObject(jsonstr,FastJsonTest.class);中第二个参数中类名一致,见下图
JSON.parse(jsonstr)
与 JSON.parseObject(jsonstr,FastJsonTest.class)
这两种方式执行的过程与结果是完全一致的。二者唯一的区别就是获取clazz参数的途径不同
使用JSON.parse(jsonstr)与JSON.parseObject(jsonstr, FastJsonTest.class)两种方式时,被调用的getter与setter方法分别为setT1() 、setT2()、setT5() 、getT4()。FastJsonTest类中一共有五个getter方法,分别为getT1()、getT2()、getT3()、getT4()、getT5(),为什么仅仅getT4被调用了呢?
这个问题要从 JavaBeanInfo.build()
方法中获取答案:
通过上文的分析可以发现,程序会使用 JavaBeanInfo.build()
方法对传入的json字符串进行解析。在 JavaBeanInfo.build()
方法中,程序将会创建一个fieldList数组来存放后续将要处理的目标类的 setter 方法及某些特定条件的 getter方法。通过上文的结果可见,目标类中所有的setter方法都可以被调用,但只有getT4()这一个getter被调用,那么到底什么样的getter方法可以满足要求并被加入fieldList数组中呢?
在 JavaBeanInfo.build()
方法可见如下代码
程序从clazz(目标类对象)中通过getMethods获取本类以及父类或者父接口中所有的公共方法,接着进行循环判断这些方法是否可以加入fieldList中以便后续处理
条件一、方法名需要长于4
条件二、不是静态方法
条件三、以get字符串开头,且第四个字符需要是大写字母
条件四、方法不能有参数传入
条件五、继承自Collection || Map || AtomicBoolean || AtomicInteger || AtomicLong
条件六、此getter不能有setter方法(程序会先将目标类中所有的setter加入fieldList列表,因此可以通过读取fieldList列表来判断此类中的getter方法有没有setter)
JSON.parseObject(jsonstr)为什么返回值为JSONObject类对象,且将FastJsonTest类中的所有getter与setter都被调用了
通过上文的分析可以发现,JSON.parse(jsonstr)与JSON.parseObject(jsonstr,FastJsonTest.class)两种方式从执行流程几乎一样,结果也完全相同;然而使用JSON.parseObject(jsonstr)这种方式,执行的结果与返回值却与前两者不同:JSON.parseObject(jsonstr)返回值为JSONObject类对象,且将FastJsonTest类中的所有getter与setter都被调用。
通过阅读源码可以发现 JSON.parseObject(String text)
实现如下
parseObject(String text)其实就是执行了parse(),随后将返回的Java对象通过JSON.toJSON()转为 JSONObject对象。
JSON.toJSON()方法会将目标类中所有getter方法记录下来,见下图
随后通过反射依次调用目标类中所有的getter方法
完整的调用链如下
总结:
上文例子中, JSON.parse(jsonstr)
与 JSON.parseObject(jsonstr, FastJsonTest.class)
可以认为是完全一样的,而parseObject(String text)是在二者的基础上又执行了一次JSON.toJSON()
parse(String text)、parseObject(String text)与parseObject(String text, Class/<T> clazz)目标类Setter/Getter调用情况
parse(String text) | parseObject(String text) | parseObject(String text, Class/<T> clazz) | |
---|---|---|---|
Setter调用情况 | 全部 | 全部 | 全部 |
Getter调用情况 | 部分 | 部分 | 全部 |
此外,如果目标类中私有变量没有setter方法,但是在反序列化时仍想给这个变量赋值,则需要使用 Feature.SupportNonPublicField
参数。(在下文中,为TemplatesImpl类中无setter方法的私有变量 _tfactory
以及 _name
赋值运用到的就是这个知识点)
针对于上文的分析可以发现,无论使用哪种方式处理JSON字符串,都会有机会调用目标类中符合要求的Getter方法
如果一个类中的Getter方法满足调用条件并且存在可利用点,那么这个攻击链就产生了。
TemplatesImpl类恰好满足这个要求:
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
中存在一个名为 _outputPropertiesget
的私有变量,其getter方法中存在利用点,这个getter方法恰好满足了调用条件,在JSON字符串被解析时可以调用其在调用FastJson.parseObject()序列化为Java对象时会被调用,下面我们详细说明一下:
首先我们从漏洞点开始,一层层往入口分析:首先看一下TemplatesImpl类中的getTransletInstance方法
其中455行调用 _class[_transletIndex]
的newInstance( )方法来实例化对象的操作
我们看一下 _class[_transletIndex]
是如何获取的,是否可以控制
_class
与 _transletIndex
值皆由451行处defineTransletClasses()方法中获取
我们跟入defineTransletClasses()方法中一探究竟
在defineTransletClasses()方法中,首先在393行判断 _bytecodes
值是否为空
值得注意的是, _bytecodes
变量是TemplatesImpl类的成员变量
因此 _bytecodes
变量可以在构造json字符串时传入,在构造poc时属于可控变量
_bytecodes
变量非空值时,程序将会继续执行至下图红框处
此时,需要满足 _tfactory
变量不为null,否则导致程序异常退出。这就是为什么公开的poc中需要设置设置 _tfactory
为{}的原因。因为 _tfactory
为私有变量,且无setter方法,这里需要指定 Feature.SupportNonPublicField
参数来为_tfactory赋值
接下来,程序将会把 _bytecodes
变量中的值循环取出并通过loader.defineClass处理后赋值给 _class[i]
我们首先来看下loader.defineClass方法是什么
可见,loader.defineClass方法其实就是对ClassLoader. defineClass的重写。defineClass方法可以从传入的字节码转化为Class
回头分析下上述流程
_bytecodes
变量非空值时,程序将会把 _bytecodes
数组中的值循环取出,使用loader.defineClass方法从字节码转化为Class对象,随后后赋值给_class[i]。
如果此时的class为main class, _transletIndex
变量值则会是此时 _bytecodes
数组中的下标值
因此当我们构造出 _bytecodes:[evilCode]
这样的json字符串(evilCode字符串为我们构造的恶意类的字节码)后,程序会将evilCode化为Class对象后赋值给_class[0]
现在回到getTransletInstance()方法中
此时的 _class[_transletIndex]
即为我们构造传入的evilCode类
程序通过调用evilCode类的newInstance()方法来实例化对象的操作,这将导致我们构造的evilCode类中的恶意代码被执行
但在此之前,需要在poc构造json字符串时使得成员变量 _name
不为空,否则程序还未执行到将evilCode类实例化就提前return
注意:由于私有变量 _name
没有setter方法,在反序列化时想给这个变量赋值则需要使用Feature.SupportNonPublicField参数。
在分析完存在漏洞的getTransletInstance方法,我们需要找到一条调用链,这条调用链需要在使用fastjson处理json字符串时成功串连到存在漏洞的getTransletInstance方法上。
我们继续向上跟踪代码
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.javanewTransformer()
方法中调用了getTransletInstance()
继续向上跟踪
com/sun/org/apache/xalan/internal/xsltc/trax/TemplatesImpl.javagetOutputProperties()
方法中调用了newTransformer()
getOutputProperties()方法为 _outputProperties
成员变量的getter方法
细心的读者可能会发现,成员变量 _outputProperties
与其getter方法getOutputProperties()方法名字并不完全一致,多了一个下划线,fastjson是如何将其对应的呢?
实际上,fastjson在解析的时候调用了一个smartMatch() 方法
在寻找 _outputProperties
的getter方法时,程序将下划线置空,从而产生了成员变量 _outputProperties
与getter方法getOutputProperties()对应的形式
首先说TemplatesImpl类。经过上文分析可发现:TemplatesImpl中存在一个反序列化利用链,在反序列化过程中,如果该类的getOutputProperties()方法被调用,即可成功触发代码执行漏洞。
再来分析下FastJson:经过上文对FastJson三种不同途径处理JSON字符串时关于getter方法被调用的条件来看,TemplatesImpl类 _outputProperties
成员变量的getter方法满足被调用条件。无论通过fastjson哪种方式解析json字符串,都会触发getOutputProperties()方法。
二者放在一起一拍即合:FastJson在反序列化TemplatesImpl类时会恰好触发TemplatesImpl类的getOutputProperties()方法;TemplatesImpl类的getOutputProperties()方法被触发就会引起反序列化代码执行漏洞。所以说这个漏洞利用很是巧妙。
针对Fastjson 1.2.24反序列化漏洞的利用方式有很多,本文由于篇幅有限仅对比较巧妙的TemplatesImpl攻击调用链路进行举例。后续将会对Fastjson历史漏洞进行详细的分析,希望大家喜欢。