java 反射研究

1. 为什么需要java反射

java 代码在 new 某个类生成对象之前,都需要先 import 这个类,否则在编译时 jvm 会提示找不到这个类。

但是有些场景需要在没有 import 这个类的情况下调用这个类的某个方法。这个时候可以使用 java 反射,在运行时动态获取类的方法、属性、父类、接口等。

反射是 java 漏洞利用的基础,我们一般使用 java.lang.Runtime 类去执行命令,但是一般代码中都不会用到这个类,所以我们需要利用反射动态调用 Runtime 的方法,比如 exec 方法,去执行任意命令。

2. 代码实现java反射

首先我们创建一个 Print 类,有两个构造方法,有参构造方法支持修改参数 s 。类的 PrintSome 方法打印出字符串 s。

package class1;

public class Print {
    public String s ;
    public Print(){
        this.s = "我被调用了";
    }
    public Print(String st){
        this.s = st;
    }
    public void PrintSome()  {
        System.out.println(this.s);
    }
}

然后正常调用和反射调用这个类:

package class1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Revoke {
    public static void main(String[] args) {
        System.out.println("正常调用:");
        Print p = new Print();
        p.PrintSome();
        System.out.println("反射调用:");
        try{
            execute("class1.Print","PrintSome");
        }catch(Exception e){
            e.printStackTrace();
        }
        System.out.println("end");
    }
    public static void execute(String className, String methodName) throws Exception {
        // 1. 获取反射中的Class对象
        Class clz = Class.forName(className); // 获取类的 Class 对象实例

        // 2. 通过反射创建类对象
        Constructor constructor = clz.getConstructor(); // 根据 Class 对象实例获取 Constructor 对象 默认的构造方法
        Object object = constructor.newInstance(); // constructor 对象的 newInstance 方法获取反射类对象

        // 3. 通过反射获取类属性方法构造器
        Method method = clz.getMethod(methodName);

        // 4. 利用 invoke 方法调用方法
        method.invoke(object);
    }
}

执行结果:

正常调用:
我被调用了
反射调用:
我被调用了
end

2021-05-10T07:04:53.png

反射调用一个方法一般需要四个步骤

  1. 通过类名获取 Class 对象
  2. 通过反射创建类对象
  3. 通过反射获取类属性、方法、构造器
  4. 执行获取到的方法

下面依次来说这几个步骤的实现

2.1 通过类名获取Class对象

获取 Class 类对象有三种方法:

第一种也是最常用的方法,使用 Class.forName 静态方法。这种方法必须知道全路径。

Class clz = Class.forName("test1.Print");

第二种,使用 .class 方法。如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就 拿它的 class 属性即可。

Class clz = Print.class;

第三种,使用对象本身的 getClass() 方法。如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过 obj.getClass() 来获取它的类

String str = new Print();
Class clz = str.getClass();

2.2 通过反射创建类对象

通过反射创建类对象主要有两种方式:通过 Class 对象的 newInstance() 方法、通过 Constructor 对象的 newInstance() 方法。

第一种:通过 Class 对象的 newInstance() 方法。class.newInstance() 的作用就是调用这个类的无参构造函数。

Class clz = Print.class;
Print p = (Print)clz.newInstance();

第二种:通过 Constructor 对象的 newInstance() 方法

Class clz = Print.class;
Constructor constructor = clz.getConstructor();
Print p = (Print)constructor.newInstance();

通过 Constructor 对象创建类对象的好处是可以选择特定构造方法,而通过 Class 对象则只能使用默认的无参数构造方法。比如

package class1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class PrintRevoke {
    public static void main(String[] args)throws Exception {
        Class clz = Class.forName("class1.Print");
        Constructor constructor = clz.getConstructor(String.class);
        Object object = constructor.newInstance("我是构造方法的参数");
        Method method = clz.getMethod("PrintSome");
        method.invoke(object);
    }
}

2021-05-10T08:24:01.png

2.3 通过反射获取类属性、方法、构造器

1.获取属性

// getFields() 方法可以获取 Class 类的公有属性
// getDeclaredFields() 方法则可以获取所有属性

Field[] fields = clz.getDeclaredFields();
for (Field field : fields) {
    System.out.println(field.getName());
}

2.获取方法

// 获取当前类和父类中声明的公有方法,后面的参数是函数的参数类型列表。
Method method = clz.getMethod(String methodName,Class<?>... parameterTypes);

// 获取当前类中声明的所有方法
Method method = clz.getDeclaredMethod(methodName);   

3.4 函数执行

对Method实例调用invoke就相当于调用该方法,invoke的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。

Object invoke(Object obj,Object...args)
- obj:实例化后的对象
- args:用于方法调用的参数

如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数无所谓了,传递一个null即可。

3. 反射调用Runtime

看完第二部分,下面我们开始尝试使用反射执行任意命令。最常见的是用Runtime弹个计算器,下面是正常调用Runtime执行命令:

package class1;

import java.lang.Runtime;

public class ExecRuntime {
    public static void main(String[] args) throws Exception{
        Runtime.getRuntime().exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

接下来我们尝试用反射来弹个计算器,按照上面的过程4步走:

package class1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ExecRuntime {
    public static void main(String[] args) throws Exception{
        // 1. 获取反射中的Class对象
        Class clz = Class.forName("java.lang.Runtime");

        // 2. 通过反射创建类对象
        Constructor constructor = clz.getConstructor(); // 根据 Class 对象实例获取 Constructor 对象 默认的构造方法
        Object object = constructor.newInstance(); // constructor 对象的 newInstance 方法获取反射类对象

        // 3. 通过反射获取类方法
        Method method = clz.getMethod("exec", String.class);

        // 4. 利用 invoke 方法调用方法
        method.invoke(object,"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

执行的时候却发现执行出错了:

2021-05-10T09:34:20.png

提示NoSuchMethod,导致使用 getConstructor 执行失败。其实是因为Runtime的构造方法是私有的, 有两种解决方法:

1.可以通过使用getDeclaredConstructor方法获取私有构造方法,再将其私有的构造方法设置为公有。

package class1;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ExecRuntime {
    public static void main(String[] args) throws Exception{
        // 1. 获取反射中的Class对象
        Class clz = Class.forName("java.lang.Runtime");

        // 2. 通过反射创建类对象
        Constructor constructor = clz.getDeclaredConstructor(); // getDeclaredConstructor可以获取私有的构造函数
        constructor.setAccessible(true); // 通过setAccessible(true)可以打破私有方法访问限制从而进行调用
        Object object = constructor.newInstance();

        // 3. 通过反射获取类方法
        Method method = clz.getMethod("exec", String.class);

        // 4. 利用 invoke 方法调用方法
        method.invoke(object,"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

2.可以通过getRuntime方法返回一个对象。

package class1;

import java.lang.reflect.Method;

public class ExecRuntime {
    public static void main(String[] args) throws Exception{
        // 1. 获取反射中的Class对象
        Class clz = Class.forName("java.lang.Runtime"); // 获取类的 Class 对象实例

        // 2. 通过反射创建类对象
        Method getRunTimeMethod = clz.getMethod("getRuntime");
        Object object = getRunTimeMethod.invoke(clz);

        // 3. 通过反射获取类方法
        Method execMethod = clz.getMethod("exec", String.class);

        // 4. 利用 invoke 方法调用方法
        execMethod.invoke(object,"/System/Applications/Calculator.app/Contents/MacOS/Calculator");
    }
}

2021-05-10T09:54:44.png

成功执行命令。

参考文章