Java 代碼審計 — 2. Reflection

參考:

https://zhishihezi.net/b/5d644b6f81cbc9e40460fe7eea3c7925

https://stackoverflow.com/questions/16966629/what-is-the-difference-between-getfields-and-getdeclaredfields-in-java-reflectio

簡介

反射機制是 java 語言的動態性的重要體現,也是 java 的各種框架底層實現的靈魂。通過反射我們可以:

  • 獲取到任何類的成員方法 (Methods)、成員變量 (Fields)、構造方法 (Constructors) 等信息。
  • 動態創建 java 類實例、調用任意的類方法、修改任意的類成員變量值等。

總而言之,程序在運行時的行爲是固定的,如果想在運行時改變,就需要用到反射技術。

java 反射在編寫漏洞利用代碼、代碼審計、繞過 RASP 方法限制等中起到了至關重要的作用。

假想一個場景,如果我們需要根據用戶輸入來動態的創建類對象。可能會想到這樣的代碼。

# className 爲用戶輸入的動態參數。
String className = "java.lang.Runtime";
Object object = new className();

但這個操作是不行的,java 靜態編譯特性決定了編譯無法通過。而藉助反射機制可以完成這個目的。

練習中學習反射

我們以 java.lang.Runtime 爲例,因爲它有一個 exec 方法可以執行系統命令,所以在很多 exp 中都能看到通過反射調用它來 rce 。這塊我們就嘗試通過它來執行系統命令。

在進入代碼之前,介紹下基本步驟:

  1. 獲取目標類 Class 對象,以便獲取目標類的構造方法。

  2. 獲取目標類構造方法,以便創建目標實例。

    因爲 Runtime 構造方法是 private 的,無法直接調用,所以需要獲取到修改一下訪問權限。

  3. 創建目標實例,以便調用執行類中的方法。

  4. 獲取目標類中要執行的方法,並調用執行該方法。

  5. 獲取執行輸出。

// 獲取Runtime類對象
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 獲取構造方法。
Constructor constructor = runtimeClass1.getDeclaredConstructor();
// 因爲構造方法是 private 的,無法直接調用,所以需要修改方法的訪問權限。
// 創建Runtime類示例,等價於 Runtime rt = new Runtime();
constructor.setAccessible(true);
Object runtimeInstance = constructor.newInstance();
// 獲取Runtime的exec(String cmd)方法。
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 調用exec方法,等價於 rt.exec(cmd)
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);
// 獲取命令執行結果
InputStream in = process.getInputStream();
// 輸出命令執行結果
System.out.println(IOUtils.toString(in, "GBK"));

獲取 Class 對象

java 反射操作的是 java.lang.Class 對象,所以我們需要先想辦法獲取到這個對象,通常我們有如下幾種方式獲取一個類的 Class 對象,以 java.lang.Runtime 爲例:

String className     = "java.lang.Runtime";
Class  runtimeClass1 = Class.forName(className);
Class  runtimeClass2 = java.lang.Runtime.class;
Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
// 通常也可以通過 對象實例.getClass() 這種方式獲取。但是 java.lang.Runtime 這個類的構造方法是私有的,不能直接通過 new 創建對象實例。
// Class runtimeClass4 = runtimeInstance.getClass();
  • 幾個獲取 Class 的方式有些區別,涉及是否初始化目標類的問題,詳見文章末尾。
  • 如果需要反射內部類,則有 特殊的語法

獲取構造方法

因爲我們最終要執行 exec 函數是需要一個對象實例的,所以我們需要創建一個對象實例,並且由於 Runtime 的構造方法是私有的,所以我們需要使用 constructor 對象來修改訪問權限。

image-20211029181629649

從 Runtime 類代碼註釋,可以看到它本身是不希望除了其自身以外的任何人去創建該類實例,因此我們沒辦法 new 一個 Runtime 類實例。我們可以藉助反射機制,修改方法訪問權限從而間接的創建出了 Runtime 對象。

下面是 Class 對象獲取構造方法的相關函數。

image-20211029181600225
  • getConstructorgetDeclaredConstructor

    前者只能獲取到公有的構造方法,而後者可以獲取到所有構造方法。

創建類實例

獲取到 Constructor 以後我們可以通過 constructor.newInstance() 來創建類實例。

image-20211029225537109
  • 若無訪問權限,則可以使用 constructor.setAccessible(true) 進行修改。

獲取類方法

爲了執行 exec 這個方法,我們需要獲取到這個方法。

下面是 Class 對象獲取方法的相關函數。

image-20211029181725007
  • getMethodgetDeclaredMethod

    前者會返回當前類公有方法和繼承的公有方法,而後者會返回當前類所有方法。

調用類方法

獲取到 java.lang.reflect.Method 對象後,我們可以通過其 invoke 方法來調用該方法。

image-20211029230048772
  • 如果調用的是 static 方法,則實例對象需要傳入 null
  • 若無調用權限,則可以使用 method.setAccessible(true) 進行修改

修改類的成員變量

Java 反射不但可以獲取類所有的成員變量名稱,還可以無視權限修飾符實現修改對應的值。

image-20211029181828901
  • getFieldgetDeclaredField

    前者會返回當前類公有字段和繼承的公有字段,而後者會返回當前類所有字段。

  • 若無修改權限,則可以使用 field.setAccessible(true) 進行修改。

  • 若修改 final 屬性的變量,則需要 特殊的語法

其它

初始化

以下順序的代碼塊,哪個會先執行呢?

import org.junit.Test;

class TestInit{
    {
        System.out.println("{}");
    }
    static {
        System.out.println("static{}");
    }
    public TestInit(){
        super();
        System.out.println("public TestInit(){}");
    }
}

public class TestXXX {

    @Test
    public void test1(){
        try {
            String className = "JNDI.TestInit";
            
            Class.forName(className); 
            // 觸發 static{}
            
//            Class.forName(className,false,this.getClass().getClassLoader()); 
            //都不觸發
            
//            Class  runtimeClass2 = TestJNDI.class; 
            //都不觸發
            
//            Class  runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className); 
            // 都不觸發
            
//            Class runtimeClass4 = new TestInit().getClass(); 
            // 觸發順序 static{} , {} , public TestInit(){}
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 關於類中的代碼段
    • static{} 是在類初始化時調用的。
    • {} 代碼會放在構造函數中 super() 後,但當前構造函數內容前。
  2. 關於幾種獲取 Class 對象方法
    • forName 第二個參數就是控制是否執行類的初始化,默認爲 true。
image-20211115112218863
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章