Java反射学习

发布于 2021-11-14  493 次阅读


0x00 什么是反射

反射是Java的特征之一,可以在运行时检查类、接口、方法和变量等信息,无需知道类的名字,方法名等。

对象可以通过反射获取他的类,类可以通过反射拿到所有方法(包括私有),拿到的方法可以进行调用。

反射的应用十分广泛,比如在编译器中,输入一个对象或类然后编译器会自动列出它的属性或方法,就是通过反射实现的。

还有在springmybatis等框架中也使用了反射来实现。

但是正因为反射太过灵活,如果代码编写不当,就很有可能出现一些安全问题。在一些漏洞利用中,利用反射来进行命令执行也是很常见的。因此不管是漏洞利用,还是代码审计,学习java安全绕不开反射的学习。

0x01 反射的实现

0x00 获取类对象

想要使用反射,首先得有一个类,必须获取到java.lang.Class对象,这里有几种方法获取。

0x00 forName()

forName()方法只要有一个类名称,就可以获得一个类对象。

例如如下代码

Class<?> clazz = Class.forName("java.lang.Runtime");
System.out.println(clazz);

就会输出class java.lang.Runtime,拿到了一个类对象。

在配置jdbc的时候就是通过Class.forName("com.mysql.jdbc.Driver")来进行配置的。

0x01 getClass()

如果在上下文中,已经存在了我们想要的那个类的实例,那我们可以直接通过getClass()来获取字节码对象。

Runtime runtime = Runtime.getRuntime();
Class<?> clazz = runtime.getClass();
System.out.println(clazz);

上述代码同样也会得到class java.lang.Runtime

0x02 直接获取

如果已经加载了了某个类,只是想获取到它的java.lang.Class对象,那么就直接拿它的class 属性即可。

Class<?> clazz = Runtime.class;
System.out.println(clazz);

同样也会得到class java.lang.Runtime

0x03 getSystemClassLoader().loadClass()

forName()类似,getSystemClassLoader().loadClass()也可以通过类名来获取类对象,但是不同的是,forName()的静态方法JVM会装载类,并且执行static{}中的代码,而getSystemClassLoader().loadClass()则不会。比如说存在一个类Test,内容如下

public class Test {
    static {
        System.out.println("static");
    }
}

测试代码如下,分别使用二者来获取类对象。

public static void main(String[] args) throws Exception {
    System.out.println("getSystemClassLoader:");
    Class<?> clazz = ClassLoader.getSystemClassLoader().loadClass("Test");
    System.out.println(clazz);
    System.out.println("forName:");
    clazz = Class.forName("Test");
    System.out.println(clazz);
}

输出如下,可以看到forName()执行了static{}中的代码,而getSystemClassLoader().loadClass()并没有。

getSystemClassLoader:
class Test
forName:
static
class Test

0x01 获取类方法

0x00 getMethod()

getMethod()方法可以返回一个特定的public类型的方法,包括其继承的方法,第一个参数为要返回的方法名,后面的参数为要返回的参数类型。getMethod()方法的传入参数类型为getMethod(String name, Class<?>... parameterTypes),后面是一个可变长参数,所以我们也可以将所有参数类型作为一个数组传给getMethod()

比如说这里我们有一个Test类,有一个方法print()如下。

public void print(String name, int id){
    System.out.println(name);
    System.out.println(id);
}

我们就可以通过以下代码来获取Test类的print()方法。print()方法需要两个参数,第一个参数为String类型,第二个参数为int类型,我们可以传入一个数组{String.class, int.class},也可以直接传入两个参数String.class, int.class,二者是等价的。

Class<?>[] classArray = {String.class, int.class};
Class<?> clazz = Class.forName("Test");
System.out.println(clazz.getMethod("print", classArray));

就可以得到输出public void Test.print(java.lang.String,int),就拿到了Test类的print()方法。

getMethod()方法只能返回一个特定的public类型的方法,不能返回protectedprivate类型的方法,否则会报错Exception in thread "main" java.lang.NoSuchMethodException

0x01 getDeclaredMethod()

getDeclaredMethod()方法与getMethod()方法类似,但是getDeclaredMethod()方法可以返回publicprotectedprivate和默认方法,但是不包括继承的方法。这里就不做演示了。

0x02 getMethods()

getMethods()方法会返回某个类的所有public方法,包括其继承的方法,与getMethod()类似。

比如如下代码就会返回java.lang.Runtime类中的所有public方法。

Class<?> clazz = Class.forName("java.lang.Runtime");
Method[] methods = clazz.getMethods();
for(Method m:methods){
    System.out.println(m);
}

0x03 getDeclaredMethod()

同理,getDeclaredMethod()也类似,可以返回某个类所有publicprotectedprivate和默认方法,但是不包括继承的方法。

0x02 初始化对象

0x00 一般情况

获取到类的对象及方法后,我们想要调用方法,就得先实例化这个类对象。

class.newInstance()的作用就是调用这个类的无参构造函数,比如说通过以下代码我们就能拿到一个Test类的实例化对象。

Class<?> clazz = Class.forName("Test");
System.out.println(clazz.newInstance());

0x01 不存在无参构造方法

如果该类没有无参构造方法,只有一个含参的构造方法时,就可以利用getConstructor()来进行实例化了。

比如说Test类存在一个含参的构造方法

public Test(String name, int id) {
    this.name = name;
    this.id = id;
}

就可以通过如下代码来实例化,其中getConstructor()方法的参数为所选的类的构造方法的参数类型,newInstance()方法的参数为构造方法的参数值。

Class<?> clazz = Class.forName("Test");
System.out.println(clazz.getConstructor(String.class, int.class).newInstance("So4ms", 666));

0x02 构造方法私有

但是当我们想通过同样的方法去获取java.lang.Runtime的实例化对象时,就会出现报错Exception in thread "main" java.lang.IllegalAccessException: class SerializeMain cannot access a member of class java.lang.Runtime (in module java.base) with modifiers "private"

这是因为它的无参构造方法为private Runtime() {},是一个private类型的方法,我们只能选择通过getRuntime()来获取到Runtime 对象。

public static Runtime getRuntime() {
    return currentRuntime;
}

这样写就不会报错了。

Class<?> clazz = Class.forName("java.lang.Runtime");
System.out.println(clazz.getMethod("getRuntime").invoke(clazz);

如果想要获取的类不存在类似java.lang.Runtime这样存在单例模式里的静态方法的话,可以通过之前提到的getDeclaredConstructor()方法来进行获取构造方法。getDeclaredConstructor()getConstructor()的关系可以和之前提到的getMethod()getDeclaredMethod()进行类比。

同样,这里的getDeclaredConstructor()方法的参数为想要获取的构造方法的参数类型,newInstance()方法的参数为构造方法的参数值。同时还得使用m.setAccessible(true);来对作用域进行修改。

Class<?> clazz = Class.forName("Test");
Constructor<?> m = clazz.getDeclaredConstructor(String.class, int.class);
m.setAccessible(true);
System.out.println(m.newInstance("So4ms", 666));

就可以调用私有的构造方法进行实例化了。

0x03 方法执行

0x00 invoke()

invoke()方法,只要通过反射获取到方法名之后,就可以调用对应的方法。

invoke()方法有两个参数,第一个参数是要调用方法的对象,即我们上面通过getMethod()等方法获取的类对象。后面的参数是调用方法要传入的参数,invoke()getMethod()一样也是使用的可变长参数,当调用的方法存在多个参数时,可选择传入多个等多个参数,也可选择传入一个数组。

比如我们来使用java.lang.Runtime进行命令执行,就是如下步骤。

Class<?> clazz = Class.forName("java.lang.Runtime");
Runtime runtime = (Runtime)clazz.getMethod("getRuntime").invoke(clazz);
Method exec = clazz.getMethod("exec", String.class);
exec.invoke(runtime, "calc.exe");

综合在一起就是:

Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");