Java學習之反射篇
0x00 前言
今天簡單來記錄一下,反射與註解的一些東西,反射這個機制對於後面的java反序列化漏洞研究和程式碼審計也是比較重要。
0x01 反射機制概述
Java反射是Java非常重要的動態特性,通過使用反射我們不僅可以獲取到任何類的成員方法、成員變數、構造方法等資訊,還可以動態建立Java類例項、呼叫任意的類方法、修改任意的類成員變數值等。Java反射機制是Java語言的動態性的重要體現,也是Java的各種框架底層實現的靈魂。
0x02 Java反射
Java反射操作的是java.lang.Class物件,所以我們需要要先獲取到Class物件。
獲取Class物件的方式:
1. Class.forName("全類名"):將位元組碼檔案載入進記憶體,返回Class物件
多用於配置檔案,將類名定義在配置檔案中。讀取檔案,載入類
2. 類名.class:通過類名的屬性class獲取
多用於引數的傳遞
3. 物件.getClass():getClass()方法在Object類中定義著。
多用於物件的獲取位元組碼的方式
程式碼例項:
方式一:
Class cls1 = Class.forName("Domain.Person");
System.out.println(cls1);
方式二:
Class cls2 = Person.class;
System.out.println(cls2);
方式三:
Person p = new Person();
Class cls3 = p.getClass();
System.out.println(cls3);
同一個位元組碼檔案(*.class)在一次程式執行過程中,只會被載入一次,不論通過哪一種方式獲取的Class物件都是同一個。
class類方法:
獲取成員變數方法:
1. 獲取成員變數們
* Field[] getFields() :獲取所有public修飾的成員變數
* Field getField(String name) 獲取指定名稱的 public修飾的成員變數
* Field[] getDeclaredFields() 獲取所有的成員變數,不考慮修飾符
* Field getDeclaredField(String name)
獲取構造方法:
* Constructor<?>[] getConstructors()
* Constructor<T> getConstructor(類<?>... parameterTypes)
* Constructor<T> getDeclaredConstructor(類<?>... parameterTypes)
* Constructor<?>[] getDeclaredConstructors()
獲取成員方法:
* Method[] getMethods()
* Method getMethod(String name, 類<?>... parameterTypes)
* Method[] getDeclaredMethods()
* Method getDeclaredMethod(String name, 類<?>... parameterTypes)
獲取全類名:
String getName()
成員變數設定:
Field:成員變數
* 操作:
1. 設定值
* void set(Object obj, Object value)
2. 獲取值
* get(Object obj)
3. 忽略訪問許可權修飾符的安全檢查
* setAccessible(true):暴力反射
構造方法:
建立物件:
* T newInstance(Object... initargs)
* 如果使用空引數構造方法建立物件,操作可以簡化:Class物件的newInstance方法
方法物件:
執行方法:
* Object invoke(Object obj, Object... args)
* 獲取方法名稱:
* String getName:獲取方法名
使用getField方法獲取成員變數
我們現在這裡編寫一個person類。
person程式碼:
package Domain;
public class Person {
private String name ;
private int age;
public String a ;
public Person() {
}
public void eat(){
System.out.println("eat");
}
public void eat(String food){
System.out.println("eat "+food);
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", a='" + a + '\'' +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
main類程式碼:
public static void main(String[] args) throws Exception {
Class cls = Class.forName("Domain.Person");
Field a = cls.getField("a"); //獲取成員a變數
Person person = new Person();
Object o = a.get(person); //獲取成員變數的值
System.out.println(o);
a.set(person,"abc"); //修改成員a變數的值為abc
System.out.println(person);
}
使用getDeclaredFields獲取所有成員變數
該方法不考慮修飾符
public static void main(String[] args) throws Exception {
Class cls = Class.forName("Domain.Person");
System.out.println(person);
Field[] declaredFields = cls.getDeclaredFields();
for (Field declaredField : declaredFields) {
System.out.println(declaredField);
}
使用getDeclaredField獲取指定成員變數
Class cls = Class.forName("Domain.Person");
Field b = cls.getDeclaredField("b");
b.setAccessible(true); //使用暴力反射機制,忽略訪問許可權修飾符的安全檢測
Person person = new Person();
Object o1 = b.get(person);
System.out.println(o1);
這裡person該類中的成員變數是被private修飾的,我們想要訪問他的值必須使用暴力反射,暴力反射
可以,忽略訪問許可權修飾符的安全檢測。
獲取構造方法
Class cls = Class.forName("Domain.Person");
Constructor constructor = cls.getConstructor(String.class,int.class);//獲取構造器
System.out.println(constructor);
//有參構造
Object o = constructor.newInstance("123", 18); //建立物件
System.out.println(o);
//無參構造
Object o1 = constructor.newInstance();
System.out.println(o1);
獲取方法
Class cls = Class.forName("Domain.Person");
//無引數方法
Method eat = cls.getMethod("eat");
Person person = new Person();
eat.invoke(person); //呼叫eat方法
//有引數方法
Method eat1 = cls.getMethod("eat", String.class); //獲取eat方法並且設定引數
eat1.invoke(person,"fish");
獲取所有public修飾方法
Class cls = Class.forName("Domain.Person");
Method[] methods = cls.getMethods();
for (Method method : methods) {
System.out.println(method);
}
獲取類名
Class cls = Class.forName("Domain.Person");
String name = cls.getName();
System.out.println(name);
前面這些只是簡單的一些方法的使用,後面來看一個小案例來了解反射的具體應用。
步驟
1.首先我們需要建立一個配置檔案,然後定義需要建立的兌現和需要執行的方法定義在配置檔案裡面。
2.在程式中讀取配置檔案
3.使用反射機制載入類檔案進記憶體
4.建立物件
5.執行方法
一般java裡面的配置檔案都是以.properites結尾,那麼就定義一個pro.properites檔案。
pro.properites檔案內容:
className=Domain.Person //寫入需要載入的類
methodName=eat //寫入需要載入的方法
main類裡面內容:
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
Properties properties = new Properties(); //建立properties物件
ClassLoader classLoader = Person.class.getClassLoader(); //獲取載入
InputStream resourceAsStream = classLoader.getResourceAsStream("pro.properites"); //獲取路徑檔案流
properties.load(resourceAsStream); //載入檔案
//獲取配置檔案定義的資料
String className = properties.getProperty("className"); //獲取類名
String methodName = properties.getProperty("methodName");//獲取方法名
Class cls = Class.forName(className); //將類載入進記憶體
Object o = cls.newInstance(); //建立無參構造物件
Method method = cls.getMethod(methodName); //建立方法
method.invoke(o); //呼叫方法
}
}
如果我們需要修改呼叫的方法或者說類,可以直接在配置檔案裡面進行修改,無需修改程式碼。
0x03 反射呼叫Runtime
Runtime這個函式有exec方法可以本地執行命令,大部分關於jsp命令執行的payload可能都是呼叫Runtime進行Runtime的exec方法進行命令執行的。
不利用反射執行命令
package com;
import org.apache.commons.io.IOUtils;
import java.io.IOException;
import java.io.InputStream;
public class Test {
public static void main(String[] args) throws IOException {
InputStream ipconfig = Runtime.getRuntime().exec("ipconfig").getInputStream();
String s = IOUtils.toString(ipconfig,"gbk"); //使用IOUtils.toString靜態方法將位元組輸入流轉換為字元
System.out.println(s);
}
}
這樣的程式碼基本都是固定死的,如果要多次傳入引數執行命令的話,這樣的寫法肯定是不行的,那麼這時候就可以用到反射。
package com;
import org.apache.commons.io.IOUtils;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class Test2 {
public static void main(String[] args) throws Exception {
String command = "ipconfig";
Class cls = Class.forName("java.lang.Runtime"); //Runtime載入進記憶體
Constructor declaredConstructor = cls.getDeclaredConstructor(); //獲取構造方法
declaredConstructor.setAccessible(true); //暴力反射
Object o = declaredConstructor.newInstance(); //建立Runtime類
Method exec = cls.getMethod("exec", String.class); //獲取exec方法,設定需要引數string型別引數
Process process = (Process) exec.invoke(o,command); //執行exec方法,並傳入ipconfig引數
// System.out.println(process);
InputStream inputStream = process.getInputStream(); //獲取輸出的資料
String ipconfig = IOUtils.toString(inputStream,"gbk"); //位元組輸出流轉換為字元
System.out.println(ipconfig);
}
}
這時候只需要修改command的值,無需修改程式碼就可以執行其他的命令了。
0x04 結尾
一邊除錯程式碼,一邊碼文章,寫完不知不覺已經5點了。還是洗洗誰吧。