反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有 ⽅法(包括私有),拿到的⽅法可以调⽤,总之通过反射,我们可以将 Java 这种静态语⾔附加上动态 特性。 – P 神
前言
近期温习,记录 📝 一些自己对于 Java 这门语言中反射的思考,文章结构从 0 到 1……
class 和 Class 和 .class 的区别
- class:class 是 Java 中的一个关键字,类嘛就是一种模板,一种数据结构。
public class exp {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
-
Class:Class 是 Java 中的一个类,在 java.lang 包中,Class 类的实例是 Person、String…… 等类的描述符,而 Class 类本身是定义这些描述符的类。
-
.class:用 javac 编译 Java 源代码文件时,编译器会将 Java 源代码转换为 JVM 可执行的字节码文件,最终生成一个 .class 文件。
.class 文件包含的是机器可读的字节码,这些字节码由 JVM 执行,Class 对象则是对该字节码的抽象描述,可以通过反射机制在运行时动态地操作类的结构。
类编译和类加载和类初始化的区别
背景:有一个 Test.java 文件
-
类编译:用 javac 编译 Test.java 时,编译器会把 Test.java 转换为字节码文件 Test.class。此时,Test.class 文件已经存在于文件系统中,但是这个 .class 文件并没有被加载到 JVM 内存中。
-
类加载:类加载发生在 JVM 运行时。只有当类需要被执行或访问时,JVM 才会加载该类。加载过程由类加载器(ClassLoader)管理,通常是按需加载(懒加载),即只在类被使用时加载。
-
类初始化:是指执行类的静态初始化块(static {})和静态变量的赋值。它会发生在类的第一次使用时。
类可以被加载但类不一定被初始化,简单例子如下–>
public class exp {
static{
System.out.println("Loaded exp static block");
}
public static void main(String[] args) {
try {
Class.forName("com.reflection02.exp", false, exp.class.getClassLoader());
Class.forName("com.reflection02.misc", false, misc.class.getClassLoader());
}catch (ClassNotFoundException e){
e.printStackTrace();
}
}
}
class misc{
static{
System.out.println("Loaded misc static block");
}
}
输出结果:
Loaded exp static block
其中着重看 misc 类,它被加载了,但是没有被初始化,所以也没有去执行静态块,无输出。Loaded exp static block
是在Class.forName("com.reflection02.exp", false, exp.class.getClassLoader());
这条语句执行前就已经输出了,原因是 exp 类是 public 类,并且包含 main 方法,JVM 会在启动时自动加载并初始化 exp 类,因此静态初始化块被执行。
forName 函数
它有两个函数重载,第一个是forName(String className)
,第二个是forName(String className, boolean initialize, ClassLoader loader)
,其中initialize
参数表示是否要初始化该类,loader
参数表示要加载的类所在的类加载器。
其中initialize
参数是false
,那么就不会去初始化该类,initialize
参数是true
,那么就会去初始化该类。有关于类的初始化相关的内容,可以复习一下浅谈 Java 内存分配机制,弄清楚类初始化过程中各个部分执行的先后顺序。
为什么要获取 Class 对象
它是反射的必然产物且反射需要它,那么何为反射?最精辟的话总结一下:反射可以让一个类被使用,但是在程序开始运行时候不被 JVM 加载,自由控制类被加载的时机和方式。一旦类被加载了,那就会生成一个对应的有关这个类的描述型信息,也就是 Class 对象,之后的种种操作也都离不开这个 Class 对象。
获取 Class 对象的方式
1.misc.class
,如果已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接
拿它的 class 属性即可,但前提一定是这个类已经加载了。下面代码说明一下–>
public class exp {
public static void main(String[] args) {
Class<?> clazz = misc.class;
}
}
class misc{
static {
System.out.println("Loaded misc static block");
}
}
输出结果:
Loaded exp static block
显而易见的结果,misc 类被加载了但是没有去初始化,所以没有输出Loaded misc static block
。这种其实不是反射,因为在写出这样的 exp.java 的时候,misc 类就已经要被 JVM 加载了。
2.Class.forName("com.reflection02.misc")
,这个方法是通过类的全限定名来获取类的 Class 对象,它会加载并初始化该类。上文见过,这里就不再赘述。
3.object.getClass()
,这个方法是通过对象来获取类的 Class 对象,它会加载并初始化该类。这种也属于反射,毕竟是在对对象进行操作,肯定是在运行时候去加载类的,而非一开始类就被 JVM 加载。举个例子–>
public class exp {
public static void main(String[] args) {
misc misc = new misc();
Class<?> clazz = misc.getClass();
}
}
class misc{
static {
System.out.println("Loaded misc static block");
}
}
输出结果:
Loaded misc static block
反射中基础的 4 个方法
- 获取 Class 对象的⽅法:forName
- 实例化类的⽅法:newInstance
- 获取函数的⽅法:getMethod
-
执⾏函数的⽅法:invoke
1.forName 在上文已经详细记录,按顺序看看 newInstance,这个方法的作用是通过 Class 对象创建类的实例,
newInstance()
调用类的无参构造函数 来创建类的实例。写一个例子说明(显示的写出来了,不写这个 Misc 类的构造函数也是会被 newInstance 隐式调用的)–>
public class EXP {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.reflection04.Misc");
Object obj = clazz.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
class Misc{
static {
System.out.println("Loaded Misc static block");
}
Misc(){
System.out.println("Loaded Misc block");
}
}
输出结果:
Loaded Misc static block
Loaded Misc block
如果这个时候把 Misc 的构造函数重写为有参构造或者改为 private,那么就会报错–>
public class EXP {
public static void main(String[] args) {
try {
Class<?> clazz = Class.forName("com.reflection04.Misc");
Object obj = clazz.newInstance();
}catch (Exception e){
e.printStackTrace();
}
}
}
class Misc{
static {
System.out.println("Loaded Misc static block");
}
Misc(String msg) {
System.out.println("Loaded Misc block with message: " + msg);
}
}
输出结果:
这就是写 EXP 时候,使用 newInstance 方法去实例化某些类会报错的原因,要么是使用的类没有无参构造函数,要么是该类构造函数是私有的,java.lang.Runtime
这个类就是这样的,它的构造函数是私有的,所以不能通过 newInstance 去实例化它。
2.把 getMethod 和 invoke 放在一起记录,getMethod 的作用是通过反射获取一个类的某个特定的公有方法,注意点是要传函数名与参数类型(和方法的重载有关,光靠一个函数名是确定不了函数的功能的)。invoke 的作用是执行方法,注意点式 invoke 传入的第一个参数–>如果这个方法是一个普通方法,那么第一个参数是类对象,如果这个方法是一个静态方法,那么第一个参数是类。理解如下–>
正常执行方法: [1].method([2], [3], [4]...)
反射执行方法:method.invoke([1], [2], [3], [4]...)
3.实操:构造 EXP 拿java.lang.Runtime.exec()
来执行命令
下面这种写法肯定是不可以 ❌ 的
public class EXP {
public static void main(String[] args) {
try{
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.newInstance(),"whoami");
}catch (Exception e){
e.printStackTrace();
}
}
}
输出结果:
原因是java.lang.Runtime
这个类的构造方法是私有的,所以不能通过 newInstance 去实例化它,下面贴图 Runtime 类实现单例模式的关键的三块代码
则修改为如下代码–>
public class EXP {
public static void main(String[] args) {
try{
Class<?> clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"ping 9mkbi.cxsys.spacestabs.top");
}catch (Exception e){
e.printStackTrace();
}
}
}
运行后触发命令执行
反射中的多态
这个本来打算放上面的 newInstance 函数这里,都放上面可能有点臃肿,还是单独拉出来了,给三行体现多态关键代码(还是从上面给的实例中摘出来的)
Class<?> clazz = Class.forName("com.reflection04.Misc");
Object obj = clazz.newInstance();
Misc misc = (Misc) obj;
这个和之前学习 Java 特性中的多态是一样的,回顾一下多态,简单写个代码示例–>
public class Father {
public static void main(String[] args) {
Father father = new Son();
father.eat();
Son son = (Son)father;
son.eat();
}
public void eat() {
System.out.println("Father Eating");
}
}
class Son extends Father{
public void eat() {
System.out.println("Son Eating");
}
public void sleep() {
System.out.println("Son Sleeping");
}
}
不难发现都是先去实例化了子类(反射那个例子,所有类都默认继承了 Object 类,所以把 Object 类看作一个父类),然后用父类型的变量去接受这个实例化的子类,最后去做了一个强转。
反射中实例化类的几种方法
最理想的情况下就是要实例化的类有一个 public 的无参构造方法,那么就可以直接通过 newInstance 方法去实例化它,然后就引发出了两种情况,一种是该类没有无参构造方法、一种是该类的构造方法是 private 的。
1.先来看第一种情况–>该类没有无参构造函数,这种情况在学习 newInstance 方法的时候可能就遇到了,网上搜该方法的时候会遇到这样的一句话–>
newInstance() 已被弃用:从 Java 9 开始,newInstance() 方法已经被标记为弃用。推荐使用 Constructor.newInstance() 方法来创建对象,特别是当构造函数带有参数时。
上面提及到的 Constructor 就是要来获取类的构造器,其中关键是要用到这个方法–>getConstructor()
,很简单然后就去跟一下这个 ProcessBuilder 这个类,平时也会去用里面的 start()方法去执行命令。为啥要选这个类,因为它的构造方法都是带参数的,详情见下图–>
and
第一个构造器的参数是一个列表,第二个构造器的参数是一个可变长参数(也就是字符串数组),针对第一个构造器来写一个命令执行的方法,代码如下–>
import java.util.Arrays;
import java.util.List;
public class EXP {
public static void main(String[] args) {
try{
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("ping","6h50h.cxsys.spacestabs.top")));
}catch (Exception e){
e.printStackTrace();
}
}
}
运行后触发命令执行,结果如下
相当于给 newInstance 这个方法增添了一双翅膀,让它有能力通过 Class 对象去实例化一个构造函数带参数的类。
那么再来看一下可变长参数如何去执行如上的 RCE,代码如下–>
public class EXP {
public static void main(String[] args) {
try{
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]ping));
}catch (Exception e){
e.printStackTrace();
}
}
}
运行后触发命令执行,结果如下–>
是二维数组的原因:去跟一下 newInstance 这个方法,它的参数也是一个变长参数,针对于数组这种类型,里面有一个默认解包的过程。
将上述关键代码改成newInstance(new String[]{"ping","08xak.cxsys.spacestabs.top"})
的话,首先会报一个这样的错误–>参数数量与预期的数量不一致
显然到这里就已经解包了,那如果改成newInstance(new String[]{"ping"})
这个样子,虽然可以过了这个数量的检测,但显然类型还是不一样的,一个是 string 类型,而一个是 string[]类型,于是乎就会出现如下的报错–>
更加简单一点的解释:ProcessBuilder 需要的参数是⼀个 String…类型的,也就是 String[] 类型的,然后后⾯的 [],表⽰ newInstance() 接收的参数是⼀个变⻓数组
2.再来看第二种情况–>该类的构造方法是 private 的该如何办?这里就用到了另外一个名词getDeclared
,这个系列的反射方法和getMethod
、 getConstructor
这种区别如下,对比着看吧。
这是一组:
-
getConstructor() 方法用于获取类的 公共 构造方法(包括从父类继承的构造方法),如果没有匹配的构造方法,则会抛出 NoSuchMethodException 异常。
-
getDeclaredConstructor() 方法用于获取类的 所有构造方法,包括 私有的、保护的、包私有的以及公共的构造方法。它不会从父类继承任何构造方法,只返回当前类声明的构造方法。
这是另外一组:
-
getMethods() 方法返回当前类及其父类(包括 Object 类)声明的 所有公共方法,包括从父类继承的公共方法。
-
getDeclaredMethods() 方法返回当前类中声明的 所有方法,包括 私有方法、保护方法、包私有方法和公共方法。但是,它 不包括父类的任何方法。
然后来实操一个 RCE 的例子,代码如下–>
import java.lang.reflect.Constructor;
public class Exp {
public static void main(String[] args) {
try{
Class<?> clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec",String.class).invoke(m.newInstance(),"ping wrij0.cxsys.spacestabs.top");
}catch (Exception e){
e.printStackTrace();
}
}
}
运行后触发命令执行,结果如下–>
Tips:上面这种方式貌似在高版本打不通,换成 Java 8 就好了
反射从 0 到 1,基本结束 🔚