0x01 前言
最近学习了Fastjson反序列化漏洞,看视频、读文章,才勉勉强强搞明白原理。这里我把学习到的内容简单总结出来,希望对各位理解Fastjson反序列化漏洞有所帮助。如果文章中有错误之处,还请批评指正,我会尽快修改。
0x02 前置知识
什么是json字符串?
形如下面的字符串,就被称为一个json字符串,也就是字典的形式
{
"name":"BossFrank",
"age":23
}
Fastjson是什么?
一个开源的java工具库,用来把Java对象序列化成json字符串或者把json字符串反序列化成一个Java对象
对象属性动态赋值的方式
1、调用对象的getter/setter方法
2、反射赋值
安全问题
FastJson引入了一个特性,即可以通过@type字段来指定反序列化出来的对象,该字段的值为对象所属类的全限定名。由于这个@type,在Fastjson反序列化的时候,可以指定任意反序列化的类。如果用户传入恶意的JSON字符串,那么就会产生安全问题
FastJson的序列化和反序列化函数
序列化函数:JSON.toJSONString(Object object)
反序列化函数:Fastjson反序列化采用JSON.parseObject()和JSON.parse()这两个方法
parseObject():返回JSON对象
第一处parse返回出来的对象,就是我们普通的java对象,然后会进行逻辑判断,最终转成JSON对象返回出来,所以,无论是否使用@type来指定对象,返回出来的只会是JSON对象。
而调用parseObject想要获取到普通的对象,有两者方式,一种是对parseObject传第二个参数,即想要返回对象的类
实际上呢,这里调用的并非前面的那个parseObject,而是它的一个同名函数
而第二种方式呢,就是拿到JSON对象之后,调用toJavaObject函数
Parse():返回我们本身的类对象
可以看到我们用@type指定类型之后,返回出来的是person对象
同时我们注意到,在调用parse和parseObject的时候,会自动调用person类的getter和setter方法,至于为什么会调用,后面流程分析会知道。同时还发现两者调用的getter/setter方法有差异,即parseObject会调用getter和setter方法,而parse时只会调用setter方法
这里差别的原因可以先粗浅理解为:parseObject会先创建一个普通的java类对象,这里是person对象,前面我们知道,给对象属性赋值,要么反射,要么set方法,这里要给person对象的属性赋值,所以要调用set方法,又因为返回时要转成JSON对象{"name":"mike","age":17},所以要调用get方法来获取每个属性的值
0x03流程分析
在parseObject处下个断点,跟进去看,发现调用了只接受字符串的parse函数
继续往里跟,然后调用到parse(String text, int features),也就是说只接受字符串的parseObject函数和parse函数最后都会调用到这里
这里实例化了一个默认的Json解析器,然后调用了一个不接受参数的parse函数,继续往里跟,最后会来到一个接受Object类型参数的parse函数,这个函数里面的逻辑是,获取令牌,根据令牌的类型进行解析,比如说左大括号、JSON数组、左方括号等等
因为我们是JSON字符串,也就是左大括号,然后会走到这里
new了一个JSON对象,然后传给了DefaultJSONParser里面的parseObject(final Map object, Object fieldName)函数,继续跟进去,开头有一些边界值的检查,核心逻辑就是这里try里面的内容
继续往下走,定义了一个key,下面就是一些if-else来获取到key的值
然后就会走到这里,对key值进行判断,我们实际上key值就是JSON.DEFAULT_TYPE_KEY,也就是下面会按照Java对象解析,所以会走到这个if里面
if里面首先把类加载进来,然后就是一些if判断,最主要的还是最后这里
获取了person类的反序列化器,然后用这个反序列化器进行反序列化,这里返回就是我们的person对象了,我们跟进去getDeserializer这个函数
这个函数里面,先根据我们的type尝试从缓存中获取一个已经存在的反序列化器,这里是没有的,所以不会直接返回,然后就调用到ParserConfig里的一个同名函数getDeserializer((Class<?>) type, type),我们跟进去
在这里有一个黑名单,继续往下走过一些大大小小的if之后,最后会来到这里,创建一个JavaBean反序列化器,跟进去
先看到里面有个按钮asmEnable,默认为true
然后下面有一些if判断,可以改变asmEnable的值,接着到这里,新建一个JavaBeanInfo。这里要知道的是,在创建类的反序列化器的时候,要先了解类里面的一些内容,比如字段、构造函数、setter/getter函数等,然后把这些内容放到JavaBeanInfo里面去,我们跟进去
这里获取person类的所有字段和方法
然后继续往下走到这里,大概说一下这三处循环的作用,分别是获取这个类满足条件的所有setter方法、字段和getter方法
简单说一下第一处循环的逻辑,遍历所有的方法,开头有一些if判断,目的是找到满足以下条件的方法:
- 方法名长度大于4
- 不是静态函数
- 返回值为void
- 函数接受一个参数
- set开头
接着截取setxxx后面的字段名,转成小写,然后把获取到的信息放到new出来的FieldInfo,加到fieldList里面去
第二处循环的大概逻辑是添加了public、private、未在列表中存在等一些字段到fieldList中
第三处循环与第一处差不多,找到满足条件的方法加到fieldList,其实也就是getter方法,这里要满足的条件大概是:
- 方法名长度大于4
- 不是静态函数
- get开头
- 返回值必须为Collection、Map、AtomicBoolean、AtomicInteger、AtomicLong其中之一
- 函数接受参数为0
- 并且前面没有被加到过FileList的字段,也就是说没有对应的setter方法
然后也会上面处理set一样,截取getxxx后面的字段名,转成小写,加到fieldList里面
处理完这三个循环之后,就把这些信息封装成JavaBeanInfo返回出去,接着根据JavabeanInfo来创建并返回JavaBean反序列化器
一路返回,我们回来到这里
我们跟进deserialze函数,看看是怎么反序列化的,发现只有值的变化,具体的代码执行调试不了,因为asmEnable这个按钮是开着的,我们返回的是asm临时创建的反序列化器
我们要想要调试,那么就需要返回JavaBeanDeserializer,也就是要把asmEnable的值改成flase,我们可以在反序列化前修改一下配置,修改asmEnable的值
可以调试后,我们跟进deserialze函数,过掉一些if后,这里把我们前面加到FiledList里面的字段拿到
然后这里构造对象
继续往下走调用赋值函数
然后里面呢,就会调用到对应setter函数和相应的只有getter的函数,所以我们一开始看到的调用parseObject函数会把setter函数给调用了,同时这里我们还知道,对于只有getter的函数,也是在返回person对象的过程中调用的
而对于有对应setter函数的getter函数,而是在返回person对象之后,调用toJSON函数调用的,也就是下面这个地方,所以调用parse()时并不会自动调用到setter函数
我们编写一个恶意类来试试
可以看到弹出来计算器了,同时我们注意到,JSON字符串中存在类里面没有setter/getter函数对应的字段或者不存在的字段也是可以的,因为前面我们知道它会在setter/getter函数的处理来截取后面的部分作为字段,所以传入的JSON字符串的变量名用的是setter/getter函数的后面部分,值才会传进对应的stter/getter函数。我们实际利用中大概率也不会直接能在stter函数里面找到恶意代码,通常是以setter/getter函数为入口点,往下找反序列化的链子。