JAVA反射机制学习

1
在我们了解或者分析JAVA的反序列化漏洞时,一定绕过不过一个知识点,那就是JAVA的反射调用,所以这次我们专门写一篇文章和大家学习和了解一下JAVA的反射调用。	

基本介绍

为什么要引入反射机制?

​ 我们在编写程序时会有两种情况,第一种是我们明确知道编译时要使用的类和需要调用的方法的具体信息,这种情况下我们可以使用new xxx()来创建对象并使用。第二种是我们在编译的过程种不知道类或者对象的具体情况,只能通过程序运行时通过动态加载来判断。 比如类的名称和需要调用的属性放在配置文件中,这种配置方式降低了耦合性,我们在写JAVA WEB的过程中经常会遇到。

​ 对于第二种方式,我们就无法在编译时得知我们要使用的类的类型和调用的方法,所以引入了反射机制。

什么是JAVA的反射机制?

​ 通过JAVA的反射机制,我们可以在运行时动态的获取到需要调用的类的属性和方法,对于任意对象,也能调用其相应的方法和设置相应的属性,这种动态获取信息和调用方法的属性叫做JAVA的反射机制。

​ 通过JAVA的反射机制,我们可以做到如下功能:

1
2
3
4
在运行时判断任意一个对象所属的类;
在运行时构造任意一个类的对象;
在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
在运行时调用任意一个对象的方法

如何使用JAVA的反射机制?

JAVA类的加载机制

​ 要理解JAVA的反射机制,我们肯定避不开JAVA类的一个加载机制。

​ 我们知道如果我们需要使用JAVA开发的程序,就需要安装JDK,也就是说如果没有JDK,我们使用的WINDOQWS默认是无法运行JAVA生成的CLASS文件的,其中JDK就默认带有JAVA虚拟机(JVM),这个JVM就是充当我们我们的JAVA程序和WINDOWS操作系统中间的角色,将我们编译的JAVA程序解释给WINDOWS操作系统运行。

​ 当我们通过JAVA命令执行某个程序,该命令将会启动一个JVM,这个程序的所有线程、变量都会放在同一个JVM中运行。

​ 当我们的程序需要使用某个类时,如果这个类还没有被加载到内存中,JVM虚拟机会将CLASS文件读入到内存,并对数据进行校验转换解析初始化,最终形成可被虚拟机直接使用的Java类型的过程。

Java 执行流程

​ 一般类的加载分为3个阶段:加载、连接、初始化。

类的加载器

类加载器的加载过程

  • 通过一个类的全限定名称来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口

类的加载器的加载方式

  • 从本地文件系统加载CLASS文件

  • 从JAR包中加载CLASS文件

  • 通过网络加载CLASS文件

  • 通过JAVA源文件动态编译加载执行

    JVM自带的类加载器通常分为如下三种:

BootStrap ClassLoader :启动类加载器,是顶层加载器。

Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。

System ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。

​ 那么者三种类型的加载器之间的继承关系是怎样的?可以写个代码简单测试一下

1
2
3
4
5
6
7
8
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
System.out.println(loader);
System.out.println(loader.getParent());
System.out.println(loader.getParent().getParent());
}
}

image-20201202163758388

​ 通过上面的结果,我们可以看出 APPClassLoader的父类型是ExtClassLoader,但是ExtClassLoader的父类型空,因为BootStrap ClassLoader是用C++写的。

JVM的类加载机制

全盘负责,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

类加载方式

​ 类的加载方式有三种:

  • 命令行启动JAVA程序时由JVM加载

  • 通过Class.forName()方法加载

  • 通过ClassLoader.loadClass()方法动态加载

    我们可以写个demo测试一下这几种加载方式有何不不同。

    首先测试loadClass方式,我们在loadClass处下断点,查看其具体的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassLoaderTest{
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = test666.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("test666");
//使用Class.forName()来加载类,默认会执行初始化块
//Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
//Class.forName("Test2", false, loader);

}
}

​ 在loadClass中,通过调用findLoadedClass来获取Class对象,再将Class对象返回,这个过程中并不会去调用获取到的Class类的static静态代码块的内容,也不会调用其对应的构造方法。

image-20201202170613299

​ 当我们通过获取的Class对象再调用newInstance方法时,则会先调用static静态代码块,再去调用构造方法。

image-20201202171313057

​ newInstance调用时,先调用静态代码块,如下所示:

image-20201202171335684

​ 再调用构造函数,如下图所示:

image-20201202171418171

​ 我们再测试一下Class.forName(“test666”)是如何工作的,由于forName内部的实现都是native层的,我这里跟踪不到,就不具体分析了,我们只了解一下它的执行结果。在调用forName后,会自动调用对应类的静态代码块,但不会执行构造方法。如果需要调用构造方法,则也需要调用newInstance方法。

image-20201202172214675

​ 我们之前的测试中加载的类是由无参数的构造方法的,如果没有无参数的构造方法,那么我们在调用newInstance的过程中,是否会去调用有参的构造方法。当我将加载类的构造方法的无参构造方法去掉时,调用newInstance将不会执行任何操作。

​ 关于类的加载先了解这么多,我们接下来主要了解下当获取到Class对象后如何通过反射调用来获取类的信息。

反射调用获取类的信息

​ 通过之前的学习我们了解了如何获取Class对象,获取了这个对象后我们如何获取类的其他信息?我们接下来将一起学习这部分的内容。

获取构造器

​ 之前我们直接通过获取到的Class调用newInstance方法,只会调用访问权限为Public的无参构造器,如果我们想获取其他构造器该怎么办?JAVA为我们提供了下面几种获取构造器的方法。

1
2
3
4
Constructor<T> getConstructor(Class<?> parameterTypes)  获取带指定参数类型的public构造器
Constructor<?>[] getConstructors() 返回这个类的所有public类型的构造器
Constructor<T> getDeclaredConstructor(Class<?> parameterTypes) 获取带指定参数类型的无视访问权限的构造器
Constructor<?>[] getDeclaredConstructor() 获取Class对象的所有构造器无视访问权限的构造器

​ 我这里做了一个测试,我们尝试获取public访问权限的带参构造器,代码如下:

1
2
3
4
Class test666=Class.forName("test666");
Constructor con=test666.getConstructor(String.class);
Object obj = con.newInstance("test666");
System.out.println(obj);

​ 成功访问到对应的带参构造器,如下图所示:

image-20201202180512409

​ 尝试访问priivate 权限的带参构造器,代码如下

1
2
3
4
Class test666=Class.forName("test666");
Constructor con=test666.getDeclaredConstructor(String.class);
Object obj = con.newInstance("test666");
System.out.println(obj);

​ 经过测试,也仅仅只能getDeclaredConstructor获取private类型的构造器,通过newInstance来调用private的构造方法还是会报错。

image-20201202181052265

image-20201202181100882

获取方法

​ 下面我们一起学习一下如何获取对应的方法

1
2
public Method getDeclaredMethod(String name, Class<?>... parameterTypes) // 得到该类所有的方法无视方法的访问权限
public Method getMethod(String name, Class<?>... parameterTypes) // 得到指定类的public方法

​ 我们做一个测试,查看如何通过getMethod获取对应的方法。

1
2
3
Class test666=Class.forName("test666");
Method methods =test666.getMethod("xxx",String.class);
System.out.println(methods);

image-20201202223349526

​ 上面是当我们反射调用的test666类存在xxx方法时调用的结果,如果test666类不存在我们要调用的xxx方法,而test666的父类test888存在我们要调用的方法,那么我们通过getMethod是否能获取test888对应的方法呢?

​ 答案是当前通过反射调用getMethod的类如果没有我们想要调用的方法,则会通过反射调用父类对应的方法

image-20201202223717517

​ 当我们将需要反射调用的方法改为private的访问权限,通过getDeclaredMethod仍然可以找到对应的方法。

image-20201202224039107

获取变量

​ 获取变量的信息可以通过下面的两个方法:

1
2
getFiled:访问公有的成员变量
getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量

​ 为了方便大家理解,我们同样写一个DEMO进行测试

1
2
3
4
Class test666=Class.forName("test666");
Object o=test666.newInstance();
Field field =test666.getField("cmd");
System.out.println(field.get(o));

​ 在test666这个类中,有一个cmd参数,我们测试能否通过反射调用来获取cmd这个变量。

image-20201203092022639

​ 我们打开debug进行调试,可以看到当调用newInstance来创建test666这个类的实例时,会对变量进行初始化。

image-20201203111226737

​ 通过getFiled获取到cmd变量,最后通过filed.get获取实例化对象o对应的变量cmd的内容进行输出。

image-20201203111441672

​ 但是使用getFiled获取不到函数中定义的变量,即使是构造函数中的变量也无法获得,当我们尝试获取非public权限的变量,会获取失败,如下图所示:

image-20201203112359310

image-20201203112439206

​ 我们将test666这个类中的变量cmd访问权限修改为private,setAccessible修改访问权限后,通过反射调用获取变量。

image-20201203113340739

1
2
3
4
5
Class test666=Class.forName("test666");
Object o=test666.newInstance();
Field field =test666.getDeclaredField("cmd");
field.setAccessible(true);
System.out.println(field.get(o));

image-20201203113529161

调用方法

​ 获取到方法后,我们可以通过invoke来调用方法,并传递参数,测试代码如下:

1
2
3
4
5
Class test666=Class.forName("test666");
Object o=test666.newInstance();
Method method = test666.getMethod("test123", String.class);
Object result = method.invoke(o,"hello world");
System.out.println(result);

​ 在test666类中的test123方法如下:

1
2
3
4
public String test123(String aaa){
String x=aaa;
return x;
}

​ 通过invoke反射调用,执行test123方法。

image-20201203114541607

​ 如果是private的方法,我们也可以通过getDeclaredMethod来获取并进行调用,不过在调用之前需要调用setAccessible方法设置属性。

image-20201203120405405

修改私有变量

​ 我们之前了解了一些获取变量的方法,那么这些变量我们该如何进行修改呢?下面是我的测试代码:

1
2
3
4
5
6
Class test666=Class.forName("test666");
Object o=test666.newInstance();
Field privateField = test666.getDeclaredField("cmd");
privateField.setAccessible(true);
privateField.set(o, "hello");
System.out.println("xxx");

​ test666类中cmd的值是xxx

image-20201203132448457

​ 运行程序后,cmd变量的值成功被修改。

image-20201203132532134

反射调用执行系统命令

​ 我们平时遇到的JAVA命令执行,大多数是通过反射调用Process.builder执行系统命令而很少使用Runtime.exec来执行命令,这是为什么?能否通过Runtime.exec来执行命令呢?

Runtime

​ 首先测试一下Runtime能否通过反射调用exec方法来进行命令执行,测试代码如下:

1
2
3
4
5
Class clazz = Class.forName("java.lang.Runtime");
Constructor con=clazz.getDeclaredConstructor();
Object o= con.newInstance();
Method methods =clazz.getDeclaredMethod("exec",String.class);
methods.invoke(o,"calc.exe");

​ 我在测试过程中发现,当执行到newInstance会报错

image-20201203142145567

​ 我们查看runtime的源码,可以看到Runtime只有一个private类型的构造函数,因此直接调用这个构造函数会因为访问权限不足而报错。

image-20201203142209061

​ 但是结合我们之前讲过的方法,我们可以使用setAccessible来设置访问权限,我尝试修改这个构造方法的访问权限,最终可以通过反射来调用Runtime.exec来执行命令。

image-20201203142503837

ProcessBuilder

​ 我们再试试通过ProcessBuilder来执行系统命令,测试代码如下:

1
2
3
4
5
6
Class test666=Class.forName("java.lang.ProcessBuilder");
Constructor con = test666.getConstructor(List.class);
Object o=con.newInstance(Arrays.asList("calc.exe"));
Method method = test666.getMethod("start");
Object result = method.invoke(o);
System.out.println(result);

​ 这里需要注意,由于ProcessBuilder没有无参构造器,所以在调用构造方法的时候需要传递需要的参数类型,创建实例的时候也需要传入参数,但是调用start方法的时候无需传入参数,由于ProcessBuilder的构造方法是public类型,因此无需设置访问权限。

image-20201203144932128

​ 当然ProcessBuilder的构造方法不止这一个,还有一个重载的方法

image-20201203145832898

​ 下面我们学习一下如何通过反射调用这个方法,这里面使用了的参数是变长参数,对于边长参数,我们也可以当数组来处理,如下所示:

image-20201203152347974

​ 所以我们获取这个构造方法时可以这样getConstructor(String[].class)

当我们通过newInstance来创建实例时,由于newInstance这个函数也是可变参数,所以可以使用两层数组来引用new String[][]{{"calc.exe"}}

image-20201203152629453

image-20201203152723799

​ 由于newInstance接收的可变参数是Object类型,因此可以通过(Object)new String[]{"calc.exe"}来创建实例。

image-20201203153631825