Java學習之反射篇

nice_0e3發表於2020-08-13

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點了。還是洗洗誰吧。

相關文章