Java 程式碼審計 — 2. Reflection
參考:
簡介
反射機制是 java 語言的動態性的重要體現,也是 java 的各種框架底層實現的靈魂。通過反射我們可以:
- 獲取到任何類的成員方法 (
Methods
)、成員變數 (Fields
)、構造方法 (Constructors
) 等資訊。 - 動態建立 java 類例項、呼叫任意的類方法、修改任意的類成員變數值等。
總而言之,程式在執行時的行為是固定的,如果想在執行時改變,就需要用到反射技術。
java 反射在編寫漏洞利用程式碼、程式碼審計、繞過 RASP 方法限制等中起到了至關重要的作用。
假想一個場景,如果我們需要根據使用者輸入來動態的建立類物件。可能會想到這樣的程式碼。
# className 為使用者輸入的動態引數。
String className = "java.lang.Runtime";
Object object = new className();
但這個操作是不行的,java 靜態編譯特性決定了編譯無法通過。而藉助反射機制可以完成這個目的。
練習中學習反射
我們以 java.lang.Runtime
在進入程式碼之前,介紹下基本步驟:
-
獲取目標類 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。