February 26, 2024

反射全解

反射

初解

反射(Reflection)是Java提供的一种机制,允许程序在运行时获取类的结构信息(如类名、方法、构造函数、字段等),并动态地创建对象、调用方法或访问字段。它通常用于框架、动态代理和工具类库中。

反射相关的类主要位于 java.lang.reflect 包下,而 Class 类是进入反射世界的入口。

RCE流程

forName获取类 -> newInstance获取该类的实例 -> getMethod(getConstructor)获取该实例的方法 ->invoke执行方法

1
2
3
4
5
6
7
public class reflect1 {
public static void execute(String className, String methodName) throws Exception {
Class reflectClass = Class.forName(className);
reflectClass.getMethod(methodName).invoke(reflectClass.newInstance());
}
}
// 记得 import java.lang.reflect.*;

forName

可以加载任意类的神器

必须全限定名,只有默认包才不需要(默认包就是没有声明package)

如果是内部类就是需要example.Class1$evilClass

java9之后的升级

1
2
3
4
5
6
7
8
9
Class<?> getEvilClass = Class.forName("example.evilClass");

Object instance = getEvilClass.getDeclaredConstructor().newInstance();

Method evilMethod = getEvilClass.getDeclaredMethod("evilMethod");

//evilMethod.setAccessible(true); 如果方法是私有类

evilMethod.invoke(instance);

**避免使用原始类型 (raw type)**:使用 Class<?> 是为了避免泛型相关的警告,保证代码的类型安全性。Java中的泛型提供了在编译时进行类型检查的能力,如果不使用泛型,可能会导致运行时的类型转换异常。

newInstance() 已过时clazz.newInstance() 这个方法依赖于无参构造函数,且在Java 9中被标记为过时。推荐使用 getDeclaredConstructor().newInstance(),这样不仅可以显式获取无参构造函数,还能更加灵活地处理私有构造函数的实例化。

getMethod()只能获得公有方法,包括从父类继承的方法,所以新版则是获得声明的方法:getDeclaredMethod()更加灵活,遇到私有属性可以setAccessible(true),但是从父类里继承来的就不包含了。

简单例子

1
2
Class reflectClass = Class.forName("java.lang.Runtime");
reflectClass.getMethod("exec", String.class).invoke(reflectClass.newInstance(), "id")

这当然是失败的,因为Runtime 类的构造方法是私有的

.class 是一种编译时获取类型的 Class 对象的方式,它可以在编译时告诉 Java 反射机制当前的参数类型是什么。这在反射中非常重要,因为你需要准确指定方法的参数类型,反射才能正确地找到匹配的方法。

两种解决方法:

通过 Runtime.getRuntime() 来获取到 Runtime 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Class reflectClass = Class.forName("java.lang.Runtime");
reflectClass.getMethod("exec", String.class).invoke(reflectClass.getMethod("getRuntime").invoke(reflectClass), "id")
//感觉很不优美

// 1. 获取 Runtime 类的 Class 对象
Class<?> reflectClass = Class.forName("java.lang.Runtime");

// 2. 获取 Runtime 类的 getRuntime() 方法,并通过反射调用它来获得 Runtime 实例
Method getRuntimeMethod = reflectClass.getMethod("getRuntime");
Object runtimeInstance = getRuntimeMethod.invoke(null); // getRuntime 是静态方法,因此第一个参数传 null

// 3. 获取 exec(String) 方法
Method execMethod = reflectClass.getMethod("exec", String.class);

// 4. 调用 exec 方法,传入命令 "id"
execMethod.invoke(runtimeInstance, "id");
//这就很爽了

新方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Class<?> getEvilClass = Class.forName("example.evilClass");

Constructor<?> constructor = getEvilClass.getDeclaredConstructor();
//有参 getEvilClass.getDeclaredConstructor(String.class);

Object instance = constructor.newInstance();
//传参 constructor.newInstance("Hello, World!");

Method evilMethod = getEvilClass.getDeclaredMethod("evilMethod");

evilMethod.setAccessible(true);

evilMethod.invoke(instance);

重载

重载(Overloading) 是面向对象编程中一种方法名称的多态性,指的是在同一个类中,允许存在多个同名方法,但是这些方法的参数列表(即参数的类型、数量或顺序)必须不同。通过重载,可以在相同的方法名下实现不同的功能,编译器会根据方法调用时的参数类型和数量,选择对应的重载方法。

例子

image-20240906170738779

有三种重载选第一种单参数的重载就可以。

invoke

invoke() 是 Java 反射机制中的一个关键方法,属于 Method 类。它用于在运行时调用通过反射获取的方法。换句话说,invoke() 允许你在不知道或无法直接访问某个类的情况下,动态地调用该类的某个方法。

1
Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException

返回值:invoke() 返回的是该方法的返回值,如果该方法的返回类型是 void,那么 invoke() 返回 null

直白一点:

正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是

method.invoke([1], [2], [3], [4]...)

所以上面的例子的另一种写法

1
2
3
4
5
Class reflectClass = Class.forName("java.lang.Runtime");
Method execMethod = reflectClass.getMethod("exec", String.class);
Method getRuntimeMethod = reflectClass.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(reflectClass);
execMethod.invoke(runtime, "calc.exe");

ProcessBuilder

1
2
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();

这其实是一个强制类型转换

clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")) 的返回值从 Object 强制转换为 ProcessBuilder 类型。

强制转换

在 Java 中,反射相关的方法(如 newInstance())返回的类型通常是 Object。为了调用具体类的方法,必须将返回的 Object 类型强制转换为正确的类型。

在上面的payload中,clazz.getConstructor().newInstance() 返回的是 Object 类型,这个对象在实际运行时是 ProcessBuilder 的实例。因此,为了能够调用 ProcessBuilder 的方法(如 start()),必须将它转换为 ProcessBuilder 类型。

为了防止抛出ClassCastException异常

1
2
3
4
5
6
7
8
9
Object obj = clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"));

if (obj instanceof ProcessBuilder) {
ProcessBuilder pb = (ProcessBuilder) obj;
pb.start();
} else {
System.out.println("对象不是 ProcessBuilder 类型");
}

在这里,instanceof 用来检查 obj 是否是 ProcessBuilder 类型。如果是,则进行强制类型转换;否则,输出错误信息。

其他常见的强制类型转换场景

  1. 向下转型:从父类或接口类型向子类转型,这是最常见的强制类型转换。例如:

    1
    2
    Object obj = new String("Hello");
    String str = (String) obj; // 强制类型转换,将 Object 转换为 String
  2. 泛型类型擦除:由于 Java 泛型在运行时会被类型擦除,有时候你可能需要强制将 Object 类型转换为某个泛型类型。例如:

    1
    2
    List<?> list = new ArrayList<>();
    List<String> stringList = (List<String>) list; // 强制转换为泛型类型
  3. 接口类型转换:当一个类实现了某个接口时,你可以将该对象强制转换为接口类型。例如:

    1
    2
    List<String> list = new ArrayList<>();
    Collection<String> collection = (Collection<String>) list; // 强制转换为 Collection 类型

泛型类型擦除

实际情况:Java 编译时的泛型类型和运行时的类型擦除

例如,假设你有一个 List<String>,在编译时,Java 会知道这个 List 是用于存储 String 类型的对象,但是在运行时,Java 会将它当作一个普通的 List,类型信息被“擦除”。

1
2
3
4
5
6
7
List<String> stringList = new ArrayList<>();
stringList.add("hello!");

List<Integer> intList = new ArrayList<>();
intList.add(111);

// 在运行时,类型信息被擦除,两者都变成 List

在运行时,stringListintList 看起来一样,它们都是 List,并且不保留 StringInteger 类型的信息。这就是类型擦除的本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GenericBox<T> {
private T item;

public void set(T item) {
this.item = item;
}

public T get() {
return item;
}
}

public class Main {
public static void main(String[] args) {
GenericBox<String> stringBox = new GenericBox<>();
stringBox.set("kanye");

GenericBox<Integer> intBox = new GenericBox<>();
intBox.set(111);

System.out.println(stringBox.get()); //kanye
System.out.println(intBox.get()); //111
}
}

虽然 GenericBox<String>GenericBox<Integer> 在编译时有区别,但在运行时,这两个对象是相同的,都是 GenericBox 类型。Java 编译器在编译时确保类型安全,但在运行时,所有泛型类型的具体信息都被擦除

泛型类型擦除的好处和限制:

例如,下面代码是非法的,因为泛型类型在运行时被擦除,Java 无法知道 List<Integer>List<String> 的区别:

1
2
3
4
if (stringList instanceof List<String>) {  // 错误:类型擦除后,无法区分 List<String> 和其他类型
System.out.println("This is a list of strings");
}

接口类型转换

假设你有一个类实现了多个接口,你可以在代码中将该对象转换为不同的接口类型。例如,类 Person 实现了 SingerProgrammer 两个接口。根据具体场景,你可以将 Person 对象转换为 SingerProgrammer

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
29
30
interface Singer {
void sing();
}

interface Programmer {
void code();
}

class Person implements Singer, Programmer {
public void sing() {
System.out.prinln("Singing...");

public void code() {
System.out.println("Coding...");
}
}

public class Main {
public static void main(String[] args) {
Person person = new Person();

// 将 person 转换为 Singer 类型,调用 sing 方法
Singer singer = (Singer) person;
singer.sing(); // 输出: Singing...

// 将 person 转换为 Programmer 类型,调用 code 方法
Programmer programmer = (Programmer) person;
programmer.code(); // 输出: Coding...
}
}

接口类型转换的规则:

强制类型转换(接口类型转换):

接口类型转换使用 Java 的强制类型转换,就像将一个对象从父类转换为子类一样,必须通过 (InterfaceType) 的语法进行转换。

1
Programmer programmer = (Programmer) person;

可变长参数

首先可以知道ProcessBuilder有两个构造函数

image-20240906184334741

Java里的可变长参数(varargs)了。正如其他语言一样,Java也支持可变长参数,就是当你

定义函数的时候不确定参数数量的时候,可以使用...这样的语法来表示“这个函数的参数个数是可变

的”。

对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价

的及签名一样(也就不能重载):

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

也由此,如果我们有一个数组,想传给hello函数,只需直接传即可:

1
2
String[] names = {"hello", "world"};
hello(names);

那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。

所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二

种构造函数:

1
2
Class reflectClass = Class.forName("java.lang.ProcessBuilder");
reflectClass.getConstructor(String[].class);

所以有两层数组,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Class reflectClass = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)
reflectClass.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})
);

//完全反射编写
Class<?> reflectClass = Class.forName("java.lang.ProcessBuilder");

Constructor<?> constructor = reflectClass.getConstructor(String[].class);

Object processBuilderInstance = constructor.newInstance((Object) new String[] {"calc.exe"});

Method<?> startMethod = reflectClass.getMethod("start");
//当然可以用getDeclareMethod,但是ProcessBuilder的实例里的start方法是public的,所以没必要用getDclareMethod

startMethod.invoke(processBuilderInstance);
//还记得之前写的invoke的使用逻辑吗?
//比如aa(bb,cc).dd用invoke调用的话就是dd.invoke(aa,bb);

About this Post

This post is written by void2eye, licensed under CC BY-NC 4.0.

#Web