Java反射機制
Java反射(Reflection)是Java非常重要的動態特性,通過使用反射我們不僅可以獲取到任何類的成員方法(Methods)、成員變量(Fields)、構造方法(Constructors)等信息,還可以動態創建Java類實例、調用任意的類方法、修改任意的類成員變量值等。Java反射機制是Java語言的動態性的重要體現,也是Java的各種框架底層實現的靈魂。
獲取Class對象
Java反射操作的是java.lang.Class對象,所以我們需要先想辦法獲取到Class對象,通常我們有如下幾種方式獲取一個類的Class對象:
類名.class,如:com.anbai.sec.classloader.TestHelloWorld.class。Class.forName("com.anbai.sec.classloader.TestHelloWorld")。classLoader.loadClass("com.anbai.sec.classloader.TestHelloWorld");
獲取數組類型的Class對象需要特殊注意,需要使用Java類型的描述符方式,如下:
Class<?> doubleArray = Class.forName("[D");//相當于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相當于String[][].class
獲取Runtime類Class對象代碼片段:
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);
通過以上任意一種方式就可以獲取java.lang.Runtime類的Class對象了,反射調用內部類的時候需要使用$來代替.,如com.anbai.Test類有一個叫做Hello的內部類,那么調用的時候就應該將類名寫成:com.anbai.Test$Hello。
反射java.lang.Runtime
java.lang.Runtime因為有一個exec方法可以執行本地命令,所以在很多的payload中我們都能看到反射調用Runtime類來執行本地系統命令,通過學習如何反射Runtime類也能讓我們理解反射的一些基礎用法。
不使用反射執行本地命令代碼片段:
// 輸出命令執行結果
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));
如上可以看到,我們可以使用一行代碼完成本地命令執行操作,但是如果使用反射就會比較麻煩了,我們不得不需要間接性的調用Runtime的exec方法。
反射Runtime執行本地命令代碼片段:
// 獲取Runtime類對象
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 獲取構造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 創建Runtime類示例,等價于 Runtime rt = new Runtime();
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, "UTF-8"));
反射調用Runtime實現本地命令執行的流程如下:
- 反射獲取
Runtime類對象(Class.forName("java.lang.Runtime"))。 - 使用
Runtime類的Class對象獲取Runtime類的無參數構造方法(getDeclaredConstructor()),因為Runtime的構造方法是private的我們無法直接調用,所以我們需要通過反射去修改方法的訪問權限(constructor.setAccessible(true))。 - 獲取
Runtime類的exec(String)方法(runtimeClass1.getMethod("exec", String.class);)。 - 調用
exec(String)方法(runtimeMethod.invoke(runtimeInstance, cmd))。
上面的代碼每一步都寫了非常清晰的注釋,接下來我們將進一步深入的了解下每一步具體含義。
反射創建類實例
在Java的任何一個類都必須有一個或多個構造方法,如果代碼中沒有創建構造方法那么在類編譯的時候會自動創建一個無參數的構造方法。
Runtime類構造方法示例代碼片段:
public class Runtime {
/** Don't let anyone else instantiate this class */
private Runtime() {}
}
從上面的Runtime類代碼注釋我們看到它本身是不希望除了其自身的任何人去創建該類實例的,因為這是一個私有的類構造方法,所以我們沒辦法new一個Runtime類實例即不能使用Runtime rt = new Runtime();的方式創建Runtime對象,但示例中我們借助了反射機制,修改了方法訪問權限從而間接的創建出了Runtime對象。
runtimeClass1.getDeclaredConstructor和runtimeClass1.getConstructor都可以獲取到類構造方法,區別在于后者無法獲取到私有方法,所以一般在獲取某個類的構造方法時候我們會使用前者去獲取構造方法。如果構造方法有一個或多個參數的情況下我們應該在獲取構造方法時候傳入對應的參數類型數組,如:clazz.getDeclaredConstructor(String.class, String.class)。
如果我們想獲取類的所有構造方法可以使用:clazz.getDeclaredConstructors來獲取一個Constructor數組。
獲取到Constructor以后我們可以通過constructor.newInstance()來創建類實例,同理如果有參數的情況下我們應該傳入對應的參數值,如:constructor.newInstance("admin", "123456")。當我們沒有訪問構造方法權限時我們應該調用constructor.setAccessible(true)修改訪問權限就可以成功的創建出類實例了。
反射調用類方法
Class對象提供了一個獲取某個類的所有的成員方法的方法,也可以通過方法名和方法參數類型來獲取指定成員方法。
獲取當前類所有的成員方法:
Method[] methods = clazz.getDeclaredMethods()
獲取當前類指定的成員方法:
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 參數類型如String.class,多個參數用","號隔開);
getMethod和getDeclaredMethod都能夠獲取到類成員方法,區別在于getMethod只能獲取到當前類和父類的所有有權限的方法(如:public),而getDeclaredMethod能獲取到當前類的所有成員方法(不包含父類)。
反射調用方法
獲取到java.lang.reflect.Method對象以后我們可以通過Method的invoke方法來調用類方法。
調用類方法代碼片段:
method.invoke(方法實例對象, 方法參數值,多個參數值用","隔開);
method.invoke的第一個參數必須是類實例對象,如果調用的是static方法那么第一個參數值可以傳null,因為在java中調用靜態方法是不需要有類實例的,因為可以直接類名.方法名(參數)的方式調用。
method.invoke的第二個參數不是必須的,如果當前調用的方法沒有參數,那么第二個參數可以不傳,如果有參數那么就必須嚴格的依次傳入對應的參數類型。
反射調用成員變量
Java反射不但可以獲取類所有的成員變量名稱,還可以無視權限修飾符實現修改對應的值。
獲取當前類的所有成員變量:
Field fields = clazz.getDeclaredFields();
獲取當前類指定的成員變量:
Field field = clazz.getDeclaredField("變量名");
getField和getDeclaredField的區別同getMethod和getDeclaredMethod。
獲取成員變量值:
Object obj = field.get(類實例對象);
修改成員變量值:
field.set(類實例對象, 修改后的值);
同理,當我們沒有修改的成員變量權限時可以使用: field.setAccessible(true)的方式修改為訪問成員變量訪問權限。
如果我們需要修改被final關鍵字修飾的成員變量,那么我們需要先修改方法
// 反射獲取Field類的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");
// 設置modifiers修改權限
modifiers.setAccessible(true);
// 修改成員變量的Field對象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// 修改成員變量值
field.set(類實例對象, 修改后的值);
Java反射機制總結
Java反射機制是Java動態性中最為重要的體現,利用反射機制我們可以輕松的實現Java類的動態調用。Java的大部分框架都是采用了反射機制來實現的(如:Spring MVC、ORM框架等),Java反射在編寫漏洞利用代碼、代碼審計、繞過RASP方法限制等中起到了至關重要的作用。
Java Web安全
推薦文章: