Java反序列化Fastjson篇01-FastJson基础
0x00 前言
本文将缩减调试配图,增加言语概述,对于新手理解本文有一定的难度(新手也不会看本文吧)。
0x01 概述
概念
Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。
提供两个主要接口来分别实现序列化和反序列化操作。
- JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程
- JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象;反序列化的过程
依赖
1 | <dependency> |
0x02 表象
假设服务端要解析一串JSON数据:
对应的Person类:
1 | import java.util.Map; |
我们会发现执行结果:
显然Fastsjon自动调用了Person类的setter和getter。
只要是动态调用,就可能会存在危害。目前,我们先理一下该反序列化的整个流程,再思考怎么利用这个动态调用。
0x03 调试流程
显而易见,反序列化的过程封装在parseObject方法:
步入,发现parseObject方法本质上还是调用同类的parse方法:
不禁思考,为什么要这么做呢?显而易见,parseobject方法就比parse多了一层壳,那么这层壳又有什么用呢?
强制运行到下一行,发现obj是Person对象,那么这层壳就要求返回值必须是JSONObject对象。
FastJson中的
parse()
和parseObject()
方法都可以用来将JSON字符串反序列化成Java对象,parseObject()
本质上也是调用parse()
进行反序列化的。但是parseObject()
会额外的将Java对象转为 JSONObject对象,即JSON.toJSON()
。所以进行反序列化时的细节区别在于,parse()
会识别并调用目标类的setter
方法及某些特定条件的getter
方法,而parseObject()
由于多执行了JSON.toJSON(obj)
,所以在处理过程中会调用反序列化目标类的所有setter
和getter
方法。
言归正传,步入parse方法,走到这里:
调用默认解析器,显然核心逻辑还在parser.parse方法里。
走到这里:
由于传进来的JSON字符串的第一个字符是左大括号,程序判断解析尚未开始,所以先初始化一个JSONObject对象,再进行解析。
步入parseObject方法:
该for循环需要关注continue、break以及return等关键字。左大括号之后的字符是双引号,下面一行代码直接得到key。
往下面走几步,发现对key进行了判断:
如果key值是@type,则程序会加载对应的类,这里就是加载Person类。
走到这里:
还记得这个Object是什么吗?是JSONObject,其本质上是一个Map,用来存放JSON里的键值对,这里也就是Person类的属性和对应的值。因为本次解析过程,我们尚未解析一个Field,所以该表还是空表。
看看下面两行:
先获取反序列化器,再进行反序列化。先看看该反序列化器的流程:
derializer是一个反序列化器表,在构造方法中,derializer存放了很多类对应的反序列化器。但是由于Person类是我们自己创建的,显然是不在该表的。
一直顺着流程走到了黑名单判断:
该判断与线程有关,与本次调试无关。
然后程序又经过好几次判断,发现怎么都找不到对应的反序列化器,最后判断clazz是JavaBean类型,直接创建JavaBean的反序列化器:
步入,开幕就很值得注意:
asmEnable是什么?先想想asm是什么:
显然asm是一种动态生成和操控class的技术。那么这里的asmEnable又有什么用呢?先留心一下该属性,下面有大用。
接着走到这里:
当前的任务是创建JavaBean的反序列化器,创建该反序列化器需要对应JavaBean的信息,这里把信息封装到JavaBeanInfo类。
步入,注意JavaBeanInfo类的几个属性。
下图是核心流程:
具体流程如何,推荐大家自行读读源码,这段源码不难理解。这里只讲大概流程:
第一个Method的for循环,关注setter方法,并把对应的Field添加到FieldList。
然后是Field的for循环,这里不关注。
第二个Method的for循环,关注getter方法,并把符合如下类型要求的Field添加到FieldList:
这里插一个题外话:
翻开methods数组,里面不仅有Person的方法,还有根对象Object的方法——这些方法是我们所不需要关注的:
所以不妨给断点下一个条件,减轻工作量:
这一段很重要,但是为什么重要现在还不需要知道,先往下走。
最后创建JavaBeanInfo的方法没什么好看的,回到这里:
下面是一系列的判断,涉及到asmEnable的修改。
到这里,稍微留心一下:
如果fieldInfo的getOnly属性为true,则asmEnable为false。
说人话,如果有一个Field只有getter方法,并且属于程序要求的那几个类,则可以让asmEnable=false。
当然不止这一种方法使得asmEnable=false,但是这里特意提到了,显然我们要用到这段逻辑。
下面还是一堆类似的,使得asmEnable=false的判断,但是我们都进入不了。
重点来了:
如果asmEnable=false,则调用JavaBeanDeserializer构造方法,返回的JavaBeanDeserializer对象(也就是JavaBean的反序列化器)有class文件。
但是,但是,如果asmEnable=true,那么会返回一个asmFactory的创建反序列化器的方法,而该反序列化器是没有class文件的,也就是说,该反序列化器是动态生成的。
直白地说,动态的反序列化器没有对应的源码,我们无法进行针对性的调试,更别说攻击了。所以我们需要程序走到调用JavaBeanDeserializer构造方法,也就是asmEanble=false。
上文提及,让asmEnable=false有好几种方法,但是目前可行的,就是getOnly属性。
让我们回到Field.getOnly属性。之前的流程里,似乎没怎么见过该属性。为了遇到该属性,我们可以给Person类加一个private的Map类属性,并如法炮制一个getter方法。
那么在第二个Method的for循环里,add方法就会进行getOnly=true:
这个新的JavaBeanInfo如下:
成功调控asmEnable:
成功返回有class文件的JavaBeanDeserializer对象:
进入反序列化流程:
进来后,一路走到这里:
该对象即是Person对象,但是此时属性尚未赋值。这里进行赋值:
步入,发现了invoke方法:
每一个属性的赋值都会调用setValue方法,然后在setValue方法里进行反射调用。
如果我反序列化一个危险类Test,该类只有一个属于cmd属性的getter方法,该方法有恶意代码,那么invoke就可以执行该恶意代码。例子如下:
小结:
由浅入深,由深归浅。该反序列化的POC实在上就是一段JSON字符串而已。但是现实中显然不可能有这么理想的情况,一个危险类可以直接被反射调用。
今儿只是把这种反序列化的逻辑稍微分析了一下。具体怎么用,待下文讲解。