Java反射机制

本文最后更新于:2024年9月7日 晚上

在Java安全方面的一个重要机制,著名的Spring框架也是基于此。

Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。

Java的大部分框架都是采用了反射机制来实现的(如:Spring MVCORM框架等),Java反射在编写漏洞利用代码、代码审计、绕过RASP方法限制等中起到了至关重要的作用。

获取Class对象

首先我们需要知道的是,在Java中一切皆对象,“类”本身也是被抽象成为一个类,称为Class类,每个类都会有一个类对象,每当编译一个新类就产生一个Class对象,也就是Class类的实例。

Class 类只有一个私有的构造方法,不能通过 new 关键字来创建,而是在类加载的时候由 Java 虚拟机以及类加载器来自动构造的。

Java反射操作的基础就是java.lang.Class对象,通常有如下3种方法:

  1. 类名.class
  2. Class.forName("com.example.test")
  3. classLoader.loadClass("com.example.test")

对于数组类型的Class对象需要特殊注意,需要使用Java类型的描述符方式,如下:

1
2
Class<?> doubleArray = Class.forName("[D");//相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;");// 相当于String[][].class

当需要调用内部类时,使用$代替.,比如test类有一个内部类t,那么这样获取:

Class t = class.forName("com.example.test$t")

反射执行系统命令

在Java中,执行系统命令通常使用java.lang.Runtime库中的exec函数实现,那么通过反射执行系统命令需要以下几个步骤:

  1. 获取Runtime.class
  2. 创建Runtime.class的构造方法,创建实例
  3. 获取Runtime类的函数exec
  4. 通过invoke函数执行exec函数

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取Runtime类对象
Class rC = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = rC.getDeclaredConstructor();
constructor.setAccessible(true);
// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();
// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = rC.getMethod("exec", String.class);
// 调用exec方法,等价于 rt.exec(cmd);
Process p = (Process) runtimeMethod.invoke(runtimeInstance, "whoami");
// 获取命令执行结果
InputStream in = p.getInputStream();
// 输出命令执行结果
System.out.println(org.apache.commons.io.IOUtils.toString(in, "UTF-8"));

反射创建类实例

在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。

在反射机制中,有两个函数可以获取到类构造器:

  1. getDeclaredConstructor:可以获取到类的私有构造方法
  2. getConstructor:只能获取公有的构造器

如果构造方法有一个或多个参数的情况下,应该在获取构造方法时候传入对应的参数类型数组。

如:clazz.getDeclaredConstructor(String.class, String.class)

获取类的所有构造方法:clazz.getDeclaredConstructors来获取一个Constructor数组。

当我们没有访问构造方法权限时我们应该调用constructor.setAccessible(true)修改访问权限就可以成功的创建出类实例了。

获取到构造器之后,可以使用constructor.newInstance()来创建类实例。

反射调用类的方法

Class对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法。

获取当前类所有的成员方法:

1
Method[] methods = clazz.getDeclaredMethods()

获取当前类指定的成员方法:

1
2
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);

getMethodgetDeclaredMethod都能够获取到类成员方法,区别在于getMethod只能获取到当前类和父类的所有有权限的方法(如:public),而getDeclaredMethod能获取到当前类的所有成员方法(不包含父类)。

反射调用方法

获取到java.lang.reflect.Method对象以后我们可以通过Methodinvoke方法来调用类方法。

调用类方法代码片段:

1
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。

method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

反射调用成员变量

这个其实在Java安全领域没有太大的作用,姑且列在这里,毕竟开发才是大头。

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

获取当前类的所有成员变量:

1
Field fields = clazz.getDeclaredFields();

获取当前类指定的成员变量:

1
Field field  = clazz.getDeclaredField("变量名");

getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod

获取成员变量值:

1
Object obj = field.get(类实例对象);

修改成员变量值:

1
field.set(类实例对象, 修改后的值);

同理,当我们没有修改的成员变量权限时可以使用: field.setAccessible(true)的方式修改为访问成员变量访问权限。

如果我们需要修改被final关键字修饰的成员变量,那么我们需要先修改方法

1
2
3
4
5
6
7
8
// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");
// 设置modifiers修改权限
modifiers.setAccessible(true);
// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// 修改成员变量值
field.set(类实例对象, 修改后的值);

Java反射机制
https://3xsh0re.github.io/2024/06/17/Java反射机制/
作者
3xsh0re
发布于
2024年6月17日
许可协议