反射
初解
反射(Reflection)是Java提供的一种机制,允许程序在运行时获取类的结构信息(如类名、方法、构造函数、字段等),并动态地创建对象、调用方法或访问字段。它通常用于框架、动态代理和工具类库中。
反射相关的类主要位于 java.lang.reflect
包下,而 Class
类是进入反射世界的入口。
RCE流程
:
forName
获取类 -> newInstance
获取该类的实例 -> getMethod(getConstructor)
获取该实例的方法 ->invoke
执行方法
1 | public class reflect1 { |
forName
可以加载任意类的神器
必须全限定名,只有默认包才不需要(默认包就是没有声明package
)
如果是内部类就是需要
example.Class1$evilClass
java9之后的升级
1 | Class<?> getEvilClass = Class.forName("example.evilClass"); |
**避免使用原始类型 (
raw type
)**:使用Class<?>
是为了避免泛型相关的警告,保证代码的类型安全性。Java中的泛型提供了在编译时进行类型检查的能力,如果不使用泛型,可能会导致运行时的类型转换异常。
newInstance()
已过时:clazz.newInstance()
这个方法依赖于无参构造函数,且在Java 9中被标记为过时。推荐使用getDeclaredConstructor().newInstance()
,这样不仅可以显式获取无参构造函数,还能更加灵活地处理私有构造函数的实例化。
getMethod()
只能获得公有方法
,包括从父类继承
的方法,所以新版则是获得声明的方法:getDeclaredMethod()
更加灵活,遇到私有属性可以setAccessible(true)
,但是从父类里继承来的就不包含了。
简单例子
1 | Class reflectClass = Class.forName("java.lang.Runtime"); |
这当然是失败的,因为Runtime 类的构造方法是
私有的
。
- 注意 这里
String.class
是告诉getMethod()
,需要找到接受String
作为参数的方法。 String.class
的.class
是获取Class
对象的语法,表示当前类型对应的Class
对象。具体来说:- **
String.class
**:它返回Class<String>
,即代表String
类的Class
对象。 - **
int.class
**:表示基本类型int
的Class
对象。 - **
void.class
**:表示void
返回类型的Class
对象。
- **
.class
是一种编译时获取类型的 Class
对象的方式,它可以在编译时告诉 Java 反射机制当前的参数类型是什么。这在反射中非常重要,因为你需要准确指定方法的参数类型,反射才能正确地找到匹配的方法。
两种解决方法:
通过 Runtime.getRuntime() 来获取到 Runtime 对象。
1 | Class reflectClass = Class.forName("java.lang.Runtime"); |
新方法
1 | Class<?> getEvilClass = Class.forName("example.evilClass"); |
重载
重载(Overloading) 是面向对象编程中一种方法名称的多态性,指的是在同一个类中,允许存在多个同名的方法,但是这些方法的参数列表(即参数的类型、数量或顺序)必须不同。通过重载,可以在相同的方法名下实现不同的功能,编译器会根据方法调用时的参数类型和数量,选择对应的重载方法。
例子
有三种重载选第一种单参数的重载就可以。
invoke
invoke()
是 Java 反射机制中的一个关键方法,属于 Method
类。它用于在运行时调用通过反射获取的方法。换句话说,invoke()
允许你在不知道或无法直接访问某个类的情况下,动态地调用该类的某个方法。
1 | Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException |
- **
obj
**:要调用该方法的对象。如果调用的是静态方法,则该参数传递null
。 - **
args
**:传递给方法的参数。如果该方法不需要参数,则可以传递null
或空数组。
返回值:invoke()
返回的是该方法的返回值,如果该方法的返回类型是 void
,那么 invoke()
返回 null
。
直白一点:
正常执行方法是 [1].method([2], [3], [4]...)
,其实在反射里就是
method.invoke([1], [2], [3], [4]...)
。
所以上面的例子的另一种写法
1 | Class reflectClass = Class.forName("java.lang.Runtime"); |
ProcessBuilder
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
这其实是一个强制类型转换
将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 | Object obj = clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")); |
在这里,instanceof
用来检查 obj
是否是 ProcessBuilder
类型。如果是,则进行强制类型转换;否则,输出错误信息。
其他常见的强制类型转换场景
向下转型:从父类或接口类型向子类转型,这是最常见的强制类型转换。例如:
1
2Object obj = new String("Hello");
String str = (String) obj; // 强制类型转换,将 Object 转换为 String泛型类型擦除:由于 Java 泛型在运行时会被类型擦除,有时候你可能需要强制将
Object
类型转换为某个泛型类型。例如:1
2List<?> list = new ArrayList<>();
List<String> stringList = (List<String>) list; // 强制转换为泛型类型接口类型转换:当一个类实现了某个接口时,你可以将该对象强制转换为接口类型。例如:
1
2List<String> list = new ArrayList<>();
Collection<String> collection = (Collection<String>) list; // 强制转换为 Collection 类型
泛型类型擦除
实际情况:Java 编译时的泛型类型和运行时的类型擦除
例如,假设你有一个 List<String>
,在编译时,Java 会知道这个 List
是用于存储 String
类型的对象,但是在运行时,Java 会将它当作一个普通的 List
,类型信息被“擦除”。
1 | List<String> stringList = new ArrayList<>(); |
在运行时,stringList
和 intList
看起来一样,它们都是 List
,并且不保留 String
或 Integer
类型的信息。这就是类型擦除的本质。
1 | public class GenericBox<T> { |
虽然 GenericBox<String>
和 GenericBox<Integer>
在编译时有区别,但在运行时,这两个对象是相同的,都是 GenericBox
类型。Java 编译器在编译时确保类型安全,但在运行时,所有泛型类型的具体信息都被擦除。
泛型类型擦除的好处和限制:
- 好处:泛型类型擦除让 Java 的泛型能够与已有的类库和接口兼容(如
Collection
),不需要对旧代码做大的修改。 - 限制:由于类型信息被擦除,Java 的泛型不能用于基础类型(如
int
),也无法在运行时检查对象的泛型类型。你无法在运行时获取泛型类型的具体信息。
例如,下面代码是非法的,因为泛型类型在运行时被擦除,Java 无法知道 List<Integer>
和 List<String>
的区别:
1 | if (stringList instanceof List<String>) { // 错误:类型擦除后,无法区分 List<String> 和其他类型 |
接口类型转换
假设你有一个类实现了多个接口,你可以在代码中将该对象转换为不同的接口类型。例如,类 Person
实现了 Singer
和 Programmer
两个接口。根据具体场景,你可以将 Person
对象转换为 Singer
或 Programmer
。
1 | interface Singer { |
接口类型转换的规则:
- 如果一个类实现了某个接口,你可以将该对象转换为这个接口类型。
- 如果尝试将对象转换为它没有实现的接口,编译器会抛出错误。
- 接口转换的好处是让代码更加灵活,增强了多态性,使对象可以以不同的方式在不同的场景中工作。
强制类型转换(接口类型转换):
接口类型转换使用 Java 的强制类型转换,就像将一个对象从父类转换为子类一样,必须通过 (InterfaceType)
的语法进行转换。
1 | Programmer programmer = (Programmer) person; |
可变长参数
首先可以知道ProcessBuilder
有两个构造函数
Java里的可变长参数(varargs
)了。正如其他语言一样,Java也支持可变长参数,就是当你
定义函数的时候不确定参数数量的时候,可以使用...
这样的语法来表示“这个函数的参数个数是可变
的”。
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价
的及签名一样(也就不能重载):
1 | public void hello(String[] names) {} |
也由此,如果我们有一个数组,想传给hello函数,只需直接传即可:
1 | String[] names = {"hello", "world"}; |
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。
所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二
种构造函数:
1 | Class reflectClass = Class.forName("java.lang.ProcessBuilder"); |
所以有两层数组,
- 外层为调用构造函数所需要的
数组
- 内层为需要传给
已经实例化的对象的方法
的命令
也是一个数组
1 | Class reflectClass = Class.forName("java.lang.ProcessBuilder"); |
About this Post
This post is written by void2eye, licensed under CC BY-NC 4.0.