Java反射机制

基本概念

Java反射是Java非常重要的动态特性,反射的核心是当JVM处于运行状态时才动态加载类,此时对于任意类都能够知道该类的所有的属性和方法,并且能够调用任意一个对象的方法,这种动态获取信息和动态调用对象方法的功能称之为Java的反射机制。通过使用反射,不仅可以获取到任意类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。其中需要注意的是,Java的反序列化问题都基于反射机制。

反射机制流程

在下图中,首先创建了一个类,在javac编译过后会形成class文件,与此同时jvm内存会查找生成的class文件并读入内存中,经过ClassLoader加载,接着会自动创建一个Class对象,里面拥有其成员变量、成员方法、构造方法等,最后是常见的new创建对象。

使用方式

获取Class对象

1
2
3
4
5
6
7
8
9
10
11
12
13
# 方法一: 已知具体的类, 通过类的class属性获取, 安全性高, 程序性能好, 多用于参数的传递
Class class1 = reflectTestClass.class;

# 方法二: 已知某个类的实例, 调用该实例的getClass方法获取class对象, 多用于对象的获取字节码的方式
ReflectTestClass reflectTestClass = new ReflectTestClass();
Class class2 = reflectTestClass.getClass();

# 方法三: 已知一个类的名称及路径, 且在该类路径下可以通过class类的静态方法forName获取, 需要注意可能抛出ClassNotFoundException, 多用于配置文件
Class class3 = Class.forName("CyberSpace.reflectTestClass");

# 方法四: 利用ClassLoader来获取类
ClassLoader classLoader = this.getClass().getClassLoader();
Class class4 = classLoader.loadClass("CyberSpace.reflectTestClass");

获取成员变量Field

1
2
3
4
java.lang.Class#getFields()    ## 获取所有的public修饰的成员变量
java.lang.Class#getField(String) ## 获取指定名称的public修饰的成员变量
java.lang.Class#getDeclaredFields() ## 获取所有的成员变量(不考虑修饰符)
java.lang.Class#getDeclaredField(String) ## 获取指定名称的成员变量(不考虑修饰符)

获取成员方法Method

1
2
3
4
java.lang.Class#getMethods()    ## 返回所有的public方法, 包括类自身声明的public方法, 父类中的public方法、实现的接口方法等
java.lang.Class#getMethod(String, Class[]) ## 返回该类或接口所声明的public方法
java.lang.Class#getDeclaredMethods() ## 返回该类所有声明方法, 但不包括继承方法
java.lang.Class#getDeclaredMethod(String, Class[]) ## 返回该类指定的声明方法

获取构造方法Constructor

1
2
3
java.lang.Class#getConstructors()    ## 返回public修饰的构造函数
java.lang.Class#getConstructor(Class[]) ## 返回匹配和参数配型相符的public修饰的构造函数
java.lang.Class#getDeclaredConstructors() ## 返回匹配和参数配型相符的构造函数(不考虑修饰符)

执行命令

Runtime函数有exec方法可以供本地执行命令,在jsp中大部分命令执行的payload都是调用Runtime的exec方法来进行命令执行的。

非反射执行命令

1
2
3
4
5
6
7
8
9
10
11
import org.apache.commons.io.IOUtils;
import java.io.InputStream;

public class EvilClass {

public static void main(String[] args) throws Exception{

InputStream inputStream = Runtime.getRuntime().exec("whoami").getInputStream();
System.out.println(IOUtils.toString(inputStream, "utf-8"));
}
}

反射执行命令

在非反射执行命令中,代码格式基本上是定死的,当需要多次传入参数执行命令时,便可以利用反射来完成需求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.apache.commons.io.IOUtils;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class EvilReflectClass {

public static void main(String[] args) throws Exception {

String command = "ipconfig"; // 待执行的命令
Class clazz = Class.forName("java.lang.Runtime"); // 将Runtime加载进内存中
Constructor constructor = clazz.getDeclaredConstructor(); // 获取构造方法
constructor.setAccessible(true); // 暴力反射, 设置权限可访问
Object runtimeObject = constructor.newInstance(); // 创建Runtime类
Method exec = clazz.getMethod("exec", String.class); // 获取Runtime类中exec方法
Process process = (Process) exec.invoke(runtimeObject, command); // 执行exec方法
InputStream inputStream = process.getInputStream(); //获取输出数据
String output = IOUtils.toString(inputStream, "gbk"); // 将字节输出流转换为字符
System.out.println(output); // 打印输出字符
}
}

特别的,method.invoke中第一个参数必须为类实例对象,当调用的是static方法时该值可以为null(在Java中调用静态方法不需要有类实例,可以直接用类名.方法名进行调用);method.invoke中第二个参数不是必要的,若当前调用方法中无参数时,则第二个参数可以没有,但是若有参数,则必须严格的依次传入对应的参数。