反射
- 什么是反射?反射的作用是什么?
定义:JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法,这种动态获取、调用对象方法的功能称为java语言的反射机制,程序在运行期可以拿到一个对象的所有信息。
作用:它允许程序在运行时检查和修改类、方法、字段和接口。这意味着你可以在程序运行时获取类的信息,创建对象,调用方法,甚至修改字段值,这一切都可以动态地进行。
反射是⼤多数语⾔⾥都必不可少的组成部分,对象可以通过反射获取他的类,类可以通过反射拿到所有 ⽅法(包括私有),拿到的⽅法可以调⽤,总之通过“反射”,我们可以将Java这种静态语⾔附加上"动态特性"。
动态特性:“⼀段代码,改变其中的变量,将会导致这段代码产⽣功能性的变化,称之为动态特性”。
想必学习JAVA安全之前,大家都把PHP的代码审计学差不多了,其实在PHP中也有反射的影子。在PHP中有一个类ReflectionClass便是用于反射的类。
echo new ReflectionClass(str 类名)
通过上面这种便能获取对象所属类的具体内容。JAVA中的反射也是异曲同工之妙。
与反射相关的类
java 反射主要依赖于 java.lang.reflect 包中的类,以下是常用的反射工具类:
java.lang.Class:表示类的字节码对象,可以用来加载类、获取类的信息。java.lang.reflect.Field:表示类的字段,可以获取和设置字段的值。java.lang.reflect.Method:表示类的方法,可以动态调用方法。java.lang.reflect.Constructor:表示类的构造函数,可以通过它创建对象。
Class对象
在 Java 中,Class 类和反射密切相关。Class 类是 Java 中每个类的模板,Java 中的每个类(包括用户定义的类)都有一个对应的 Class 对象,它是类元数据的载体。所以反射是通过Class对象,来知道某个类的所有属性和方法。也就是说通过反射我们可以获取构造器,对象,属性,方法。
所以想要利用反射,首要的步骤便是先获取目标类的class对象。主要有三种方式可用获取Class对象。
获取Class对象
用于测试我们先创建一个Person类
class Person {
private String name;
private int age;
// 构造方法
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// 方法
public void introduce() {
System.out.println("My name is " + name + " and I am " + age + " years old.");
}
// 获取年龄
public int getAge() {
return age;
}
}
方式一、实例化对象的getClass()方法
如果上下文中已经存在了类的实例,那么我们便可以直接通过obj.getClass()来获取它的类。
例子:
public static void main(String[] args) throws Exception {
// 获取 Person 类的 Class 对象
Person person = new Person("erosion2020", 14);
Class<? extends Person> clazz = person.getClass();
}
在这个例子中,已经存在了一个Person类的实例person。那便可以直接通过person.getClass获取其Class对象。
方式二、使用类的 .class 属性
如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。
例子:
public static void main(String[] args) throws Exception {
// 获取 Person 类的 Class 对象
Class<?> clazz = Person.class;
}
由于我们已经定义完了Person类,所以我们可以直接通过类名.class获取其Class对象。
方式三、使用Class.forName(String className)静态方法
当你知道一个类的完整的类名,便可以通过静态方法Class.forname(类名)获取Class对象。
例子:
public static void main(String[] args) throws Exception {
Class cls = Class.forName("com.test.Person");
}
JAVA反射的使用方法
当我们成功获得了类的Class对象,便可以通过Class对象来获取类的成员属性,成员方法,构造函数以及创建实例,执行函数等等。
获取类的方法:forName
实例化类对象的方法:newInstance
获取函数的方法:getMethod
获取构造函数:getConstructor
执行函数的方法:invoke
获取成员属性
获取成员变量Field位于 java.lang.reflect.Field 包中
Field[] getFields() :获取所有 public 修饰的成员变量
Field[] getDeclaredFields() 获取所有的成员变量,不考虑修饰符
Field getField(String name) 获取指定名称的 public 修饰的成员变量
Field getDeclaredField(String name) 获取指定的成员变量
例子:
public static void main(String[] args) throws Exception {
Person person = new Person("erosion2020", 14);
Class<?> clazz = Person.class;
// 获取所有有访问权限的字段(public)
Field[] pubFields = clazz.getFields();
// 获取所有字段(包括 private)
Field[] fields = clazz.getDeclaredFields();
// 错误:getField() 无法获取 private 字段,注释掉这行
// Field name = clazz.getField("name");
// 正确:用 getDeclaredField() 获取 private 字段
Field name = clazz.getDeclaredField("name");
// 允许访问私有字段
name.setAccessible(true);
// 修改字段值
name.set(person, "AcidEtch");
// 可以加一行打印验证是否修改成功
person.introduce(); // 输出:My name is AcidEtch and I am 14 years old.
}
由于Person类里的属性都是私有属性,即使我们获取了也无法读取与修改它。
所以需要设置允许访问私有字段
name.setAccessible(true);
获取成员方法并通过invoke调用
Class类提供了以下几个方法来获取Method:
Method getMethod(name, Class...):获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)Method[] getMethods():获取所有public的Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
例子:
public static void main(String[] args) throws Exception {
Class<?> clazz = Person.class;
// 获取所有包含了访问权限的方法
Method[] pubMethods = clazz.getMethods();
// 获取所有方法,然后输出(即便这个方法是被禁止访问的
Method[] methods = clazz.getDeclaredMethods();
// 获取introduce方法
Method My_intro = clazz.getMethod("introduce");
//调用方法
My_intro.invoke(new Person("ljl",18));
}

注意到这里调用方法的方式是通过invoke
invoke 的作用是执行方法,它的第一个参数是:
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是null
获取构造函数Constructor
Constructor<?>[] getConstructors() :只返回public构造函数
Constructor<?>[] getDeclaredConstructors() :返回所有构造函数
Constructor<> getConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的public构造函数
Constructor<> getDeclaredConstructor(类<?>... parameterTypes) : 匹配和参数配型相符的构造函数
我们在Person类中添加一个无参构造函数测试
public Person(){
System.out.println("hello");
}
对应代码
public static void main(String[] args) throws Exception{
Class c1 = Person.class;
Constructor[] constructors1 = c1.getDeclaredConstructors();
Constructor[] constructors2 = c1.getConstructors();
for (Constructor c : constructors1){
System.out.println(c);
}
System.out.println("-------分割线---------");
for (Constructor c : constructors2){
System.out.println(c);
}
System.out.println("-------分割线---------");
Constructor constructors3 = c1.getConstructor(String.class, int.class);
System.out.println(constructors3);
System.out.println("-------分割线---------");
Constructor constructors4 = c1.getDeclaredConstructor(String.class);
}

创建实例(对象)
反射创建对象,也叫做反射之后实例化对象,这里用到的是 newInstance() 方法
public static void main(String[] args) throws Exception {
Class<?> clazz = Person.class;
// 获取指定参数的构造方法
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 通过反射来创建对象
Object personInstance = constructor.newInstance("John", 30);
Method introduceMethod = clazz.getMethod("introduce");
// 通过反射调用打印方法
introduceMethod.invoke(personInstance);
}
也可以直接通过Class对象创建
Object obj = clazz.newInstance();

利用反射命令执行
在JAVA中要进行命令执行,就必须导入java.lang.Runtime类。
一般情况下JAVA中命令执行的代码如下
// 获取Runtime实例(单例,通过静态方法getRuntime()获取)
Runtime runtime = Runtime.getRuntime();
runtime.exec("calc");
那么利用反射应该怎么命令执行呢?我们不能直接按下面这种方式来执行命令。
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "calc");
出现如下报错

报错原因是 Runtime 类的构造方法是私有的。
至于为什么连构造函数都要设置为私有的,p神在JAVA安全漫谈中提到这是“单例模式”。比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连 接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取。
回归正题,Runtime类就有这样一个静态方法getRuntime,调用这个方法的返回值便是一个Runtime实例(对象)。所以正确的反射命令执行代码应该像下面这样写。
Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec", String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz), "calc.exe");
执行流程:首先先获取Class对象,然后获取exec方法,但是想要invoke执行需要有实例作为参数,这里便是通过获取getRuntime方法并执行获取Runtime对象来作为exec执行的参数。

写成这样更容易理解
Class clazz = Class.forName("java.lang.Runtime");
Method execMethod = clazz.getMethod("exec", String.class);
Method getRuntimeMethod = clazz.getMethod("getRuntime");
Object runtime = getRuntimeMethod.invoke(clazz);
execMethod.invoke(runtime, "calc.exe");
依旧在p神的JAVA安全漫谈里的反射篇里提到了两个问题。
-
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
-
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
先解答第一个问题:
如果我们不使用无参构造以及静态方法,也就是getRuntime也不能用了。这时候想要命令执行便要用到getConstructor,但是Runtime类又只有一个无参构造函数,所以便要用到另一个命令执行的方式ProcessBuilder类。
我们使用反射来获取其构造函数,然后调用start() 来执行命令:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).start();
ProcessBuilder有两个构造函数:
public ProcessBuilder(List<String> command)
public ProcessBuilder(String... command)
这里用的是第一个构造函数,所以传入的是List.class。并且这里使用了(ProcessBuilder)强制类型转换,所以可以直接start(),调用方法。
使用前注意要导入三个类
import java.util.Arrays;
import java.util.List;
import java.lang.ProcessBuilder;

当然我们也可以写成我们熟悉的形式
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start").invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe")));
获取start方法,然后通过invoke执行。
那如果使用的是第二种构造函数呢?
public ProcessBuilder(String... command)
这就涉及到了可变长参数了,其实很多语言支持它。当你定义函数的时候不确定参数的数量,那么便可以...代表参数个数是可变的。
对于可变长参数,Java其实在编译的时候会编译成一个数组。
public void hello(String...names) {}
等价
public void hello(String[] names) {}
也由此,如果我们有一个数组,想传给hello函数,只需直接传即可。
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。 所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二 种构造函数。在调用 newInstance 的时候,又因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:
Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}})).start();

接下来解答第二个问题:
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
答案是肯定能的,这个时候我们就要用到getDeclared了,在前面介绍JAVA反射的使用方法的时候介绍到了,如果使用的方法是包含Declared的话是可以获取到私有的属性或方法的。但是获取了以后依然不能使用,但是我们通过setAccessible(true)修改作用域后便能使用了。
payload如下:
Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true);
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

小总
其实反射学到最后发现也就那点东西,先获取类,并进行实例化对象,然后获取类里面的属性,调用类里面的方法,就没了。
但是这也算JAVA安全入学第一课了,我也是学习了挺久才基本掌握。
这里引用p神的一句话
Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。
p神的JAVA安全漫谈我真得狠狠推荐一手了,介绍的很全面。但是其实讲解的其实并不是特别细和通俗易懂,所以我在参考了很多师傅的博客后总结出了这些内容,至少我自己看觉得已经够细了。