fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
fastjson有很多的分析文章,我这里从一个不同的角度来看fastjson
对fastjson有一点了解的同学都知道,fastjson可以把对象变成json字符串,也可以把json字符串再转化为对象,这个过程其实很类似于java原生序列化\反序列化,只不过我们的输出流从java自定义的格式变成了json格式,那么我觉得可以从这两个相似机制的对比入手,能够让大家了解到fastjson一些比较关键的流程。
序列化
序列化做的就是把对象的值提取出来,然后按照规定格式输出。所以序列化主要分为两个过程,一个是类的基本信息获取,比如field信息等,他需要知道后面我们到底要读取哪些字段,另一个就是获取字段值并写入输出流
那么就会涉及几个问题
- 哪些字段会读取,哪些字段不会读取
- 怎么去获取这些字段的值?是利用反射去读取?还是利用getter?
java原生序列化
信息获取
会处理继承关系,继承链上所有Serializable的类都会进行处理,按照从上往下的顺序进行
可以看到,在getDefaultSerialFields获取Fields的时候,只会保留非static非transient的field
获取值
writeSerialData按照从父类到子类的顺序进行处理(getClassDataLayout返回是按照从superclass->subclass的顺序)
因为序列化支持重写WriteObject,重写后可以自定义一些处理逻辑,我们这里只关注默认的处理逻辑也就是defaultWriteFields
defaultWriteFields会处理当前类desc里面的所有fields(这里面的fields就是前面信息获取阶段留下的fields),在这里getPrimFieldValues获取基础类型的值,getObjFieldValues获取Obj
getPrimFieldValues和getObjFieldValues都是使用unsafe,使用unsafe的原因是可以不考虑访问权限,然后可以看到这两个函数都是逐个遍历对应的fields字段
后面调用writeObject0处理非基础类型字段的值,因为writeObject0会check是否是Serializable,如果我们字段是一个对象并且非Serializable的话,会报错(如果不赋值的话是null,虽然不会报错,但是也没有什么意义)
fastjson把对象转化为json
1 | <dependency> |
首先对于fastjson来说他不关注是否是Serializable
信息获取
TypeUtils.buildBeanInfo这个函数里获取类的基本信息
首先获取所有field并放入Cache
这里会遍历继承类,把所有的DeclaredFields都放入cache,这里注意,不会去判断是否是static
然后调用computeGetters处理fieldCacheMap
computeGetters顾名思义,处理getter的,大概分为两个部分
- 处理Methods,调用getMethods获取包括继承的public方法,然后在函数里面做了一些判断,最后只留下包括继承的public的getter方法(is是boolean的getter),这里会做getter和Field的匹配,并保存在fieldInfoMap里
- 处理Fields,调用getFields获取包括继承的public成员变量,如果是static那么就跳过,如果已有的Map里面包含了对应的Fields,那么跳过,只会把没有包含的Field添加进去
处理methods
代码里有比较长的逻辑用来判断函数是否是一个getter,具体的可以自己看一下(因为getter大概有什么要求还是有概念的,比如说非static,无参,返回值非void)
如果对应的getter匹配到了对应的字段,那么就生成一个fieldInfo放入Map里
那么这里也有可能出现没有匹配到field的情况,比如说我们这里随便写一个满足getter的函数,他最后生成的Fieldinfo就没有field
这里也注意一个细节,getter对应的field可以是static的,因为parserAllFieldToCache不会判断field是否static
处理fields
如果子类中声明了一个与父类中同名的方法(并且签名相同),这在Java中被称为方法覆盖。字段和方法的继承规则有所不同。当子类和父类有同名的字段时,它们实际上是两个独立的实体,并没有被覆盖或重写的概念。这个特性是跟java的多态有关,多态在Java中主要通过方法重写和接口实现来实现,而静态方法、字段和构造器不参与多态。(提这个主要是前面getMethods时候,如果子类和父类同名,那么只会输出子类的,但是getFields就算重名也都会输出出来)
首先非static
如果前面处理完methods后的fieldInfoMap里没有对应的字段,那么就新创建一个fieldInfo,设置methods为空,放入fieldInfoMap里
那么在这个里面生成的fieldinfo都是没有method的
获取值
fastjson有两种Serializer,一种是JavaBeanSerializer,一种是ASMSerializer
默认情况下会使用asm,但是asm动态生成的类不方便调试,所以我们先分析使用JavaBeanSerializer的方式
JavaBeanSerializer
首先关上asm
1 | SerializeConfig.getGlobalInstance().setAsmEnable(false); |
如果关了asm,那么写入的逻辑在JavaBeanSerializer.write里,这里会依次遍历getter, 然后有下面两个判断
1.默认是会skipTransient的,所以只会写入非Transient的field(当然没有field是不受影响的),
2.ignoreNonFieldGetter默认为false,所以有些没有field的getter也是不受影响的
获取value
如果有getter,执行getter,没有直接用反射去获取
ASMSerializer
在处理的时候,如果遇到field是null的getter,会进行一个特殊处理
如果遇见字段是Transient的也会进行特殊处理
在这里可以把对应的asm动态类字节码dump出来
查看动态类的代码,如果getter的field是Transient的话,他会判断是否开启了SkipTransientField(1024),如果没有开启,他会把值写到json字符串里,如果开启了就会跳过 (这一点来说有些不同,对于Transient的字段来说,默认情况下走asm的话他是会调用getter的,不走asm的话是不会调用getter的)
而对于没有field的getter来说,他会判断是否开启了IgnoreNonFieldGetter (33554432),如果没有开启,他会进一步去调用getter把值写到json字符串里,如果开启了,那么就跳过,这个倒和前面的保持一致
而对于普通的getter来说,就比较简单,调用getter然后输出
对于没有getter的情况,asm也是直接获取值,然后输出
当然可能还会遇见其他值的feature,比如说128,像这种就是一些对于值的处理逻辑,就不过多赘述了
总结
- java原生序列化会判断是否Serializable,fastjson不会
- 二者都会处理子类以及继承来的字段
- 原生序列化要求字段非static非Transient,而对于fastjson来说,如果是被public的getter匹配到的字段只要求非Transient,如果没有匹配到的要满足public非static非Transient
- 原生序列化获取值是通过unsafe反射来获取,对于fastjson来说,有getter的是通过getter,没getter的通过反射
可以看到,java原生序列化里默认情况下是不会调用类自己的函数,除非重写writeObject,但是fastjson是会调用getter的,如果类的getter里面有危险操作,那么就可能带来风险。所以如果在题目里发现存在恶意的getter方法,那么可以考虑结合fastjson的toString\toJSONString来利用,在CTF里面常用来配合java原生的反序列化来调用恶意的getter。
细节
通过前面部分,对fastjson序列化大体上的逻辑和思路已经有了一个认识,那么这部分主要是解决一些细节上的问题,比如说,getter的调用顺序是什么?
getter的调用顺序
在computeGetters的最后,会调用getFieldInfos把map结构转化为一个list
然后会调用sort进行排序
在FieldInfo的compareTo可以看到是对name进行了排序(对于没有field对应的getter来说,他的name就是getter函数解析以后的属性名)
然后把我们排序好的Fields放到BeanInfo里
JavaBeanSerializer
在JavaBeanSerializer的构造函数里面,会把前面的sortedFields再生成FieldSerializer
然后在JavaBeanSerializer的write里面会进行判断
而我们默认创建的时候,传入的DEFAULT_FEATURE里包含了sortField
所以在这里,我们的getters就变成了按照属性名排列的getters
然后就是按照顺序遍历getters,调用getPropertyValueDirect获取值
ASMSerializer
从BeanInfo里拿出排序好的getters
在generateWriteMethod里按照顺序遍历getter动态生成对应代码
fastjson+TemplatesImpl
对于TemplatesImpl来说,第一个就是outputProperties
直接调用newTransformer触发exp
payload
1 | byte[] bytecode = Files.readAllBytes(Paths.get("/Users/xuemo/Desktop/java_project/debug/target/classes/Evil.class")); |
调用栈(没有使用asm的)