1. 程式人生 > 其它 >Java 程式碼審計 — 2. Reflection

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 物件來修改訪問許可權。

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

下面是 Class 物件獲取構造方法的相關函式。

  • getConstructorgetDeclaredConstructor

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

建立類例項

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

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

獲取類方法

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

下面是 Class 物件獲取方法的相關函式。

  • getMethodgetDeclaredMethod

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

呼叫類方法

獲取到 java.lang.reflect.Method 物件後,我們可以通過其 invoke 方法來呼叫該方法。

  • 如果呼叫的是 static 方法,則例項物件需要傳入 null
  • 若無呼叫許可權,則可以使用 method.setAccessible(true) 進行修改

修改類的成員變數

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

  • 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。