參考:
簡介
反射機制是 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 。這塊我們就嘗試通過它來執行系統命令。
在進入代碼之前,介紹下基本步驟:
-
獲取目標類 Class 對象,以便獲取目標類的構造方法。
-
獲取目標類構造方法,以便創建目標實例。
因爲 Runtime 構造方法是 private 的,無法直接調用,所以需要獲取到修改一下訪問權限。
-
創建目標實例,以便調用執行類中的方法。
-
獲取目標類中要執行的方法,並調用執行該方法。
-
獲取執行輸出。
// 獲取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 對象獲取構造方法的相關函數。
-
getConstructor
和getDeclaredConstructor
前者只能獲取到公有的構造方法,而後者可以獲取到所有構造方法。
創建類實例
獲取到 Constructor
以後我們可以通過 constructor.newInstance()
來創建類實例。
- 若無訪問權限,則可以使用
constructor.setAccessible(true)
進行修改。
獲取類方法
爲了執行 exec 這個方法,我們需要獲取到這個方法。
下面是 Class 對象獲取方法的相關函數。
-
getMethod
和getDeclaredMethod
前者會返回當前類公有方法和繼承的公有方法,而後者會返回當前類所有方法。
調用類方法
獲取到 java.lang.reflect.Method
對象後,我們可以通過其 invoke
方法來調用該方法。
- 如果調用的是 static 方法,則實例對象需要傳入
null
- 若無調用權限,則可以使用
method.setAccessible(true)
進行修改
修改類的成員變量
Java 反射不但可以獲取類所有的成員變量名稱,還可以無視權限修飾符實現修改對應的值。
-
getField
和getDeclaredField
前者會返回當前類公有字段和繼承的公有字段,而後者會返回當前類所有字段。
-
若無修改權限,則可以使用
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();
}
}
}
- 關於類中的代碼段
- static{} 是在類初始化時調用的。
- {} 代碼會放在構造函數中 super() 後,但當前構造函數內容前。
- 關於幾種獲取 Class 對象方法
- forName 第二個參數就是控制是否執行類的初始化,默認爲 true。