Java反射机制

java动态特性:一段代码,改变其中的变量,将会使这段代码产生功能性的变化。

Java反射机制是指在运行状态时,对于任意一个类,都能够获取这个类的所有属性和方法;对于一个对象,都能够调用它的任意一个方法和属性(包括私有方法和属性),这种动态获取信息以及动态调用对象方法的功能就称为Java的反射机制。

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

1
2
3
4
5
public void execute(String className, String methodName) throws Exception {
Class clazz = Class.forName(className); // 动态加载类
clazz.getMethod(methodName) // 获取方法对象
.invoke(clazz.newInstance()); // 创建实例并调用方法
}

可以看到当我们创建一个类文件后,经过javac编译,就会形成.class文件,同时jvm内存会查找生成的.class文件读入内存和经过ClassLoader加载,同时会创建生成一个Class对象,里面拥有其获取的成员变量,成员方法和构造方法等。

Class类

JVM为每个加载的class创建了对应的Class实例,并在实例中保存了该class的所有信息。如果获取了某个Class实例,就可以通过这个Class实例获取到该实例对应的class的所有信息

  • 如果已经加载了某个类,就可以通过一个class的静态变量class获得。使用**.class来创建Class对象的引用时,不会自动初始化该Class对象**。

    1
    Class cls = String.class;

    未初始化该Class对象,不会触发静态代码块和静态变量赋值。

  • 如果上下文中存在某个类的实例,可以通过该实例变量提供的getClass()方法获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Main {
    public static void main(String[] args) throws ClassNotFoundException {
    String s="b1uel0n3";
    Class cls=s.getClass();

    System.out.println(cls.getName()); //获取类名,输出java.lang.String
    System.out.println(cls.getSimpleName());

    System.out.println(cls == String.class); //检查Class对象是否表示String类,输出true
    }
    }

    此时获取的是已知实例的class对象,所以已初始化

  • 知道一个class的完整类名,可以通过静态方法Class.forName()获取。使用forName()会初始化该Class对象,即会触发静态代码块和静态变量赋值

    1
    Class cls=Class.forName("java.lang.String");

foaName有两个函数重载:

1
2
Class<?> cls=Class.forName(String name)
Class<?> cls=Class.forName(String name, **boolean** initialize, ClassLoader loader)

Class<?>表示某个未知类型的对象,泛型<?>是为了保证类型安全,避免强制类型转换的风险

1
2
3
Class.forName(className)
//等于
Class.forName(className,true,currentLoader)

name指定加载的类名,initialize指定是否初始化,loader指定类加载器,若为null,默认使用系统类加载器

第二个参数表示是否初始化,在forName的时候,构造函数并不会执行,而是执行类初始化。他会先执行static{}静态块里的内容:

1
2
3
4
5
6
7
8
9
10
11
public class LazyClass {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", LazyClass.class);
}
public LazyClass() {
System.out.printf("Initial %s\n", this.getClass());
}
}
1
2
3
4
5
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
LazyClass train = new LazyClass();
}
}

可以看到,当实例化这个类时,首先调用的是静态块static{},其次是实例块{},最后是构造函数。其中static{}就是在类初始化的时候调用的

静态块是类加载时执行一次,实例块是每次实例化时执行,构造函数也是每次实例化时执行

而由于Class实例在JVM中是唯一的,所以,上诉方法获取的Class实例是同一个实例

Field类

访问字段

1
2
3
4
Field getField(name): 根据字段名获取某个public的field(包括父类)
Field getDeclaredField(name): 根据字段名获取当前类的某个field(不包括父类)
Field[] getFielads(): 获取所有public的field(包括父类)
Field[] getDeclaredFields(): 获取当前类的所有field(不包括父类)

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws Exception {
Class stdClass=Student.class;
System.out.println(stdClass.getField("age"));
System.out.println(stdClass.getDeclaredField("score"));
System.out.println(stdClass.getField("name"));
}
}

class Student extends Person{
public int score;
private int grade;
}

class Person{
public String name;
public int age;
}
1
2
3
public int Person.age
public int Student.score
public java.lang.String Person.name

一个Field对象包含了一个字段的所有信息:

  • getName():返回字段名称
  • getType():返回字段类型,也是一个Class实例
  • getModifiers():返回字段修饰符
  • get(obj):获取字段值
  • set:修改字段值

获取字段值

在Java反射机制中,主要通过Field.get(Object obj)来获取字段值,而字段值是存储在对象实例中的,因此我们要通过反射获取或修改某个字段值时,必须明确操作的具体对象实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws Exception {
Person std=new Student();
Class stdClass=std.getClass();
System.out.println(stdClass.getField("score").get(stdClass.newInstance()));
System.out.println(stdClass.getField("name").get(stdClass.newInstance()));
System.out.println(stdClass.getDeclaredField("grade").get(stdClass.newInstance()));
}
}

class Student extends Person{
public int score=100;
private int grade=90;
}

class Person{
public String name="b1uel0n3";
}

stdClass.getField(“score”)用于获取字段对象,注意stdClass是一个对象而并非实例,所以需要创建一个实例

但我们会发现,上面代码无法获取private字段,会抛出IllegalAccessException错误,这是因为没有访问权限

在访问私有字段前,我们可以通过设置Field.setAccessible(true)强制开启访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception {
Person std=new Student();
Class stdClass=std.getClass();
System.out.println(stdClass.getField("score").get(stdClass.newInstance()));
System.out.println(stdClass.getField("name").get(stdClass.newInstance()));
Field f=stdClass.getDeclaredField("grade");
f.setAccessible(true);
System.out.println(f.get(stdClass.newInstance()));
}
}

class Student extends Person{
public int score=100;
private int grade=90;
}

class Person{
public String name="b1uel0n3";
}
/*
100
b1uel0n3
90
*/

修改字段值

1
2
Field f = stdClass.getDeclaredField("grade");
f.set(obj, "xxxx")

修改和读取的实例要相同,stdClass.newInstance() 每次调用都会创建一个新的 Student 实例。所以不要f.set(f.get(stdClass.newInstance()),50);

如果修改非public字段,需要先调用setAccessible(true)开启访问权限

Method类

获取方法

1
2
3
4
Method getMethod(name,Class...): 获取某个public的Method(包括父类)
Method getDeclaredMethod(name,Class...): 获取当前类的某个Method(不包括父类)
Method[] getMethods(): 获取所有public的Method(包括父类)
Method[] getDeclaredMethods(): 获取当前类的所有Method(不包括父类)

Demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=Student.class;
System.out.println(cls.getMethod("getName", String.class));
System.out.println(cls.getMethod("getScore", String.class));
System.out.println(cls.getDeclaredMethod("getGrade", int.class));
}
}

class Student extends Person{
public int score;
private int grade;
public int getScore(String s){
return 1;
}
public int getGrade(int i){
return 11;
}
}

class Person{
public String name="b1uel0n3";
public String getName(String m){
return "hello";
}
}
1
2
3
public java.lang.String Person.getName(java.lang.String)
public int Student.getScore(java.lang.String)
public int Student.getGrade(int)

一个Method对象包含一个方法的所有信息:

  • getName(): 返回方法名称
  • getReturnType(): 返回方法返回值类型,也是一个Class实例
  • getParameterTypes(): 返回方法的参数类型,是一个Class数组
  • getModifiers(): 返回方法的修饰符

调用方法

invoke:调用方法

invoke用于执行方法,它的第一个参数是:

  • 如果这个方法是一个实例方法,那么第一个参数是实例对象

    1
    2
    3
    4
    5
    6
    public void sayHello() {
    System.out.println("Hello!");
    }
    Student student = new Student();
    Method method = Student.class.getMethod("sayHello");
    method.invoke(student);
  • 如果第一个方法是静态方法,那么第一个参数是类

这里用substring举个例子:

1
2
3
4
5
6
7
8
9
10
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
String name="b1uel0n3";
Method substring = String.class.getMethod("substring", int.class);
System.out.println(substring.invoke(name,3));
}
}
//el0n3

调用非public方法

同样需要通过getAccessible(true)允许调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=Student.class;
Method m = cls.getDeclaredMethod("hello", String.class);
m.setAccessible(true);
m.invoke(cls.newInstance(),"b1uel0n3"); //输出hello b1uel0n3
}
}

class Student extends Person{
public int score;
private int grade;

private void hello(String s){
System.out.println("hello "+s);
}
}

class Person{
public String name="b1uel0n3";
}

多态

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

public class Main {
public static void main(String[] args) throws Exception {
Class cls=Person.class;
Method m = cls.getDeclaredMethod("hello");
m.invoke(new Student()); //Student:hello
}Student:hello
}

class Student extends Person{
public void hello(){
System.out.println("Student:hello");
}
}

class Person{
public void hello(){
System.out.println("Person:hello");
}
}

Person类中定义了hello()方法,并且它的子类Student覆写了hello()方法,而尽管m是通过从Person.class获取的Method,由于实际调用时JVM会根据对象的实际类型进行动态绑定。所以作用于Student实例时,同样遵循多态原则,实际调用Student类的hello()方法。

调用构造方法

Java的反射API提供了Constructor对象,它包含一个构造方法的所有信息,可以创建一个实例。

通过Class实例获取Constructor的方法如下:

1
2
3
4
getConstructor(Class...): 获取某个public的Constructor
getDeclaredConstructor(Class...): 获取某个Constructor
getConstructors(): 获取所有public的Constructor
getDeclaredConstructors(): 获取所有Constructor

调用非public的Constructor时,必须先通过setAccessible(true)设置允许访问。

获取继承关系

获取父类:

1
Class.getSuperclass()
1
2
Class cls=Student.class.getSuperclass();
System.out.println(cls.getName()); //Person

获取接口interface:

1
Class.getInterface()

利用反射命令执行

Runtime

java.long.Runtime中可以通过exec方法执行本地命令:

1
Runtime.getRuntime().exec("calc.exe");

同样我们也能用反射实现:

1
2
3
4
5
6
7
8
9
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.Runtime");
Method m = cls.getMethod("exec", String.class);
m.invoke(cls.newInstance(),"calc.exe");
}
}

Class.newInstance()的作用是创建实例调用这个类的无参构造函数,但执行后发现报错IllegalAccessException,产生的原因可能有两点:

  • 使用的类没有无参构造方法
  • 使用的无参构造方法是私有的

而其中Runtime类的无参构造方法是私有的,且禁止外部直接通过new实例化:

同时发现Runtime.exec方法有六个重载,这里使用最简单的exec(String command)即可:

可以看到该重载方法只有一个参数且为String类型。

注意到Runtime类有一个getRuntime方法可以获取Runtime对象,且该方法是公有的:

所以我们可以通过使用cls.getMethod("exec", String.class);获取Runtime.exec方法,Runtime.getRuntime()用来获取Runtime对象,从而执行Runtime.exec方法

由于Runtime.getRuntime()是一个静态方法,所以在invoke调用时,第一个参数要传入一个Runtime类:

1
2
3
4
5
6
7
8
9
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.Runtime");
Method m = cls.getMethod("exec", String.class);
m.invoke(cls.getMethod("getRuntime").invoke(cls), "calc.exe");
}
}

首先m获取Runtime.exec方法,要调用invoke,则第一个参数为实例对象,而cls.getMethod("getRuntime")获取Runtime.getRuntime方法,通过调用invoke返回Runtime对象作为m.invoke的第一个参数,由于getRuntime为静态方法,所以参数为一个类即cls

还可以利用设置setAccessible(true)修改构造方法访问权限实现暴力访问权限

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls = Class.forName("java.lang.Runtime");
Method m = cls.getMethod("exec", String.class);
Constructor c=cls.getDeclaredConstructor();
c.setAccessible(true);
m.invoke(c.newInstance(), "calc.exe");
}
}

ProcessBuilder

ProcessBuilder有两个构造函数:

利用第一种形式的构造函数,我们可以在getConstructor处传入List.class,获取start方法执行:

1
2
3
4
5
6
7
8
9
10
11
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=ProcessBuilder.class;
Method m = cls.getMethod("start");
m.invoke(cls.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
}
}

原理就是由于ProcessBuilder类中存在接受List参数的构造方法,由于Java泛型的类型擦除,反射时List.class对应List<String>List<Integer>等所有泛型形式。所以通过cls.getConstructor(List.class)来获取第一种形式构造方法,然后再通过获取的构造方法创建实例,这里直接将我们的命令通过Arrays.asList()封装成List对象,然后调用start()方法启动进程执行命令

分解下我们的payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=ProcessBuilder.class;
Method m = cls.getMethod("start");
Constructor c=cls.getConstructor(List.class);
Object p = c.newInstance(Arrays.asList("calc.exe"));
m.invoke(p);
}
}

当然我们也可以利用第二种形式的构造函数,对于可变长参数,Java其实在编译时会编译成一个数组,也就是说,如下两种写法其实是等价的:

1
2
public void hello(String[] names){}
public void hello(String...names){}

对于反射来说,如果要获取的目标函数里包含可变长参数,我们可以直接认为是数组。

思路还是不变的,通过start进程启动命令即可,我们可将字符串数组的类String[].class传给getConstructor即可,同时注意在创建实例时注意构造函数接收的是可变长参数,而我们传给ProcessBuilder的是一个List<String>类型,二者叠加为一个一个二维数组,如果传一维数组会导致多个String参数报错:

1
2
3
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getConstructor(String[].class).newInstance(new
String[][]{{"calc.exe"}})).start();

由于newInstance接收一个Object数组,数组每个元素对应构造函数的一个参数,所以newInstance要求参数是object[],写入new String[][]{{"cmd"}}可等效于new Object[]{String[]{"cmd"}}

反射payload:

1
2
3
4
5
6
7
8
9
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=ProcessBuilder.class;
Method m = cls.getMethod("start");
m.invoke(cls.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
}
}

分解payload:

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Class cls=ProcessBuilder.class;
Method m = cls.getMethod("start");
Constructor c=cls.getConstructor(String[].class);
Object p = c.newInstance(new String[][]{{"calc.exe"}});
m.invoke(p);
}
}

利用反射修改final关键字修饰的成员变量

  • 反射获取Field类的字段修饰符modifiers

    1
    Field modifiers=field.getClass().getDeclaredField("modifiers");
  • 设置modifiers修改权限

    1
    modifiers.setAccessible(true);
  • 修改成员变量的Field对象的modifiers值

    1
    modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
  • 修改成员变量值

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

参考

https://www.cnblogs.com/ElloeStudy/p/16065219.html

https://www.javasec.org/javase/Reflection/Reflection.html

https://pdai.tech/md/java/basic/java-basic-x-reflection.html#%E5%8F%8D%E5%B0%84%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B%E5%B0%8F%E7%BB%93

https://nivi4.notion.site/Java-8e0ad38312714da48ec396a6544dd3d2#cc9b11b89ca84dbb81065621377306a6