java 反射研究

文章最后更新时间为:2021年06月22日 10:34:36

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

成功执行命令。

参考文章

1 + 9 =
快来做第一个评论的人吧~