java反射

java反射

Java安全可以从反序列化漏洞开始说起,反序列化漏洞又可以从反射开始说起。

定义

对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制

简单的说就是可以任意访问和使用任意类

java反射是通过获取字节码文件来获取其对应的Class类型的对象的

反射的使用

反射相关类:

Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量/字段
Method类 代表类的方法
Constructor类 代表类的构造方法

获取字节码文件(获取Class对象)

有三种方法

  • obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过obj.getClass() 来获取它的类

  • Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接拿它的 class 属性即可。这个⽅法其实不属于反射。

  • Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取 主要forName后面跟的是全类名,也就是包名+类名

其中forName是最常用的

这样说还是不太容易理解 上代码!

package fanshe;
/**
* 获取Class对象的三种方式
* 1 Object ——> getClass();
* 2 任何数据类型(包括基本数据类型)都有一个“静态”的class属性
* 3 通过Class类的静态方法:forName(String className)(常用)
*
*/
public class Fanshe {
public static void main(String[] args) {
//第一种方式获取Class对象
Student stu1 = new Student();//这一new 产生一个Student对象,一个Class对象。
Class stuClass = stu1.getClass();//获取Class对象
System.out.println(stuClass.getName());

//第二种方式获取Class对象
Class stuClass2 = Student.class;
System.out.println(stuClass == stuClass2);//判断第一种方式获取的Class对象和第二种方式获取的是否是同一个 结果为true

//第三种方式获取Class对象
try {
Class stuClass3 = Class.forName("fanshe.Student");//注意此字符串必须是真实路径,就是带包名的类路径,包名.类名
System.out.println(stuClass3 == stuClass2);//判断三种方式是否获取的是同一个Class对象
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

}
}

在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。⽐如,上下⽂中如果只有Integer类型的

数字,我们如何获取到可以执⾏命令的Runtime类呢?也许可以这样(伪代

码): getClass().forName("java.lang.Runtime")

forName有两个函数重载:

Class<?> forName(String name)

Class<?> forName(String name, **boolean** initialize, ClassLoader loader)

第⼀个就是我们最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:

默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就

是 ClassLoader 。

ClassLoader 是什么呢?它就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。Java默认的 ClassLoader 就是根据类名来加载类,这个类名是类完整路径,如 java.lang.Runtime

第二个参数initialize

其实在 forName 的时候,构造函数并不会执⾏,即使我们设置 initialize=true 。

那么这个初始化究竟指什么呢?

可以将这个“初始化”理解为类的初始化。我们先来看看如下这个类:

public class TrainPrint {
{
System.out.printf("Empty block initial %s\n", this.getClass());
}
static {
System.out.printf("Static initial %s\n", TrainPrint.class);
}
public TrainPrint() {
System.out.printf("Initial %s\n", this.getClass());
}
}

三个“初始化”⽅法有什么区别,调⽤顺序是什么?

其实你运⾏⼀下就知道了,⾸先调⽤的是 static {} ,其次是 {} ,最后是构造函数。

其中, static {} 就是在“类初始化”的时候调⽤的,⽽ {} 中的代码会放在构造函数的super() 后⾯,但在当前构造函数内容的前⾯。

所以说, forName 中的 initialize=true 其实就是告诉Java虚拟机是否执⾏”类初始化“。

Class类中常用获得类相关的方法:

getClassLoader() 获得类的加载器
getDeclaredClasses() 返回一个数组,数组中包含该类中所有类和接口类的对象
forName(String classRoad) 根据类的路径返回类的Class对象(该方法是静态成员方法)
newInstacne() 创建类的实例
getName() 获得类的完整路径名

获取构造方法并使用

想要获取一个类的构造方法 当然要先获取这个构造方法所在的类

然后再使用函数:

getConstructors() 这是获取所有公共的构造方法

getDeclaredConstructors() 这是获取所有的构造方法 包括私有和受保护的

clazz.getConstructor(xxxx) 这是获取指定的公共构造方法

getDeclaredConstructor(xxxx) 这是获取指定的构造方法

注意 构造方法是支持重载的 所以在获取指定的构造方法时 要写清楚后面的数据类型

//获取包
Class clazz = Class.forName("java.lang.Runtime1"); //随便乱写的一个包

//无参构造
Constructor con1 = clazz.getDeclaredConstructor();

//有参构造 参数一致 传的是字节码文件
Constructor con2 = clazz.getDeclaredConstructor(String.class);
// int的参就是int.class 其他同理


创建一个类

public class Student {
public String name;//公共的成员变量

private int id = 1;//私有的成员变量

public Student(){
System.out.println("公共的构造方法");
}

private Student(int id){
this.id = id;
System.out.println("私有的构造方法");
}

public void func(){
System.out.println("公共的成员方法");
}

private void func2(String s){
System.out.println(s);
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}

反射调用:

public class Test {
public static void reflectPrivateConstructor(){
try {
//获得Class对象
Class<?> c1 = Class.forName("reflect.Student");
//调用getDeclaredField(String name)方法
Constructor<?> constructor = c1.getDeclaredConstructor(int.class);
//调用setAccessible(boolean)后可修改访问权限
constructor.setAccessible(true);
//通过Constructor对象创建Student对象
Student student = (Student) constructor.newInstance(100);
//输出创建好的Student对象
System.out.println(student);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws ClassNotFoundException {
reflectPrivateConstructor();
}
}

获取成员变量

Field

跟获取构造方法一样 getFields()等

也是要获取字节码文件

方法 作用
getField(String name) 获得该类中某个公有的字段
getFields() 获得该类中所有公有的字段
getDeclaredField(String name) 获得该类中某个字段
getDeclaredFields() 获得该类中所有字段
public class Test {
public static void reflectPrivateField(){
try {
//获得Class对象
Class<?> c1 = Class.forName("reflect.Student");
//getDeclaredField(String name)方法
Field field = c1.getDeclaredField("id");
//调用setAccessible(boolean)后可修改访问权限
field.setAccessible(true);
//通过Class类反射一个Student对象
Student student = (Student) c1.newInstance();
//修改指定Student对象的静态成员变量:id = 500
field.set(student,500);
//输出Student对象
System.out.println(student);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
reflectPrivateField();
}
}

获取并调用成员方法

都差不多:

getMethod(String name,Class…parameterTypes) 获得该类中某个公有的方法 getMethods() 获得该类中所有公有的方法 getDeclaredMethod(String name,Class...parameterTypes) 获得该类中某个方法
getDeclaredMethods() 获得该类中所有方法

public class Test {
public static void reflectPrivateMethod(){
try {
//获得Class对象
Class<?> c1 = Class.forName("reflect.Student");
//调用getDeclaredMethod(String name,Class...<?>parameterTypes)方法
Method method = c1.getDeclaredMethod("func2",String.class);
//获取私有的属性或方法一般都要调用setAccessible(boolean)方法
method.setAccessible(true);
//通过Class类反射一个Student对象
Student student = (Student) c1.newInstance();
//Method对象调用invoke方法
//让指定的Student对象调用指定的私有方法并传参
method.invoke(student,"通过反射机制调用Student类的私有方法");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

public static void main(String[] args) {
reflectPrivateMethod();
}
}

获取与前面类似

调用的时候需要用到一个新函数:invoke

invoke 的作用是执行方法,它的第一个参数是:

  • 如果这个方法是一个普通方法,那么第一个参数是类对象

  • 如果这个方法是一个静态方法,那么第一个参数是类

实例化类对象的⽅法: newInstance

class.newInstance() 的作用就是调用这个类的无参构造函数

当发现使用 newInstance 总是不成功,这时候原因可能是:

  1. 你使用的类没有无参构造函数

  2. 你使用的类构造函数是私有的

所以如果我们这样执行命令是不行的:(这本来是一个有公共的无参构造的执行方法)

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

会报错:

image-20240515183638264

原因是 Runtime 类的构造方法是私有的

单例模式:类的构造方法是私有的

对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,此时作为开发者你就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来

获取:

public class TrainDB {
private static TrainDB instance = new TrainDB();
public static TrainDB getInstance() {
return instance;
}
private TrainDB() {
// 建立连接的代码...
}
}

这样,只有类初始化的时候会执行一次构造函数,后面只能通过 getInstance 获取这个对象,避免建

立多个数据库连接。

而Runtime类就是单例模式

我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象

所以我们将上述命令改为:

Class clazz = Class.forName("java.lang.Runtime");
clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");

也就是: 单例模式下的执行方法

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");

也可以直接使用:getDeclaredConstructor()

Class clazz = Class.forName("java.lang.Runtime");
Constructor m = clazz.getDeclaredConstructor();
m.setAccessible(true); //这个是必须的。我们在获取到一个私有方法后,必须用setAccessible 修改它的作用域,否则仍然不能调用。
clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

其实就是我们调用成员方法开始给到代码

也就是调用 getConstructor

因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。

获取到构造函数后,我们使用 newInstance 来执行。

将上述代码整合成一个payload

比如:ProcessBuilder–一个常见的命令执行方式,我们使用反射来获取其构造函数,然后调用start() 来执行命令:

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

(ProcessBuilder) 强制类型转换用于将 Object 类型(实际上是 Class.newInstance() 返回的实例)转换为 ProcessBuilder 类型,以便我们可以调用 ProcessBuilder 类的 start() 方法来启动一个进程。

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)

  • public ProcessBuilder(String… command)

我上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class

但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表

达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步

也就是:

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

通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是ProcessBuilder Object了。

那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反射执行呢?

可变长参数:就是当你定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变的”。

对于可变长参数,Java其实在编译的时候会编译成一个数组

也就是说 这两个事等价的:

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

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

我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:

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

在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下:

Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(String[].class).newInstance(newString[][]{{"calc.exe"}})).start();

反射的其他使用

反射main方法

其实也没什么好说的 直接上代码:

student类:

package fanshe.main;

public class Student {

public static void main(String[] args) {
System.out.println("main方法执行了。。。");
}
}

反射main方法:

package fanshe.main;

import java.lang.reflect.Method;

/**
* 获取Student类的main方法、不要与当前的main方法搞混了
*/
public class Main {

public static void main(String[] args) {
try {
//1、获取Student对象的字节码
Class clazz = Class.forName("fanshe.main.Student");

//2、获取main方法
Method methodMain = clazz.getMethod("main", String[].class);//第一个参数:方法名称,第二个参数:方法形参的类型,
//3、调用main方法
// methodMain.invoke(null, new String[]{"a","b","c"});
//第一个参数,对象类型,因为方法是static静态的,所以为null可以,第二个参数是String数组,这里要注意在jdk1.4时是数组,jdk1.5之后是可变参数
//这里拆的时候将 new String[]{"a","b","c"} 拆成3个对象。。。所以需要将它强转。
methodMain.invoke(null, (Object)new String[]{"a","b","c"});//方式一
// methodMain.invoke(null, new Object[]{new String[]{"a","b","c"}});//方式二

} catch (Exception e) {
e.printStackTrace();
}


}
}

结果事执行了main方法

通过反射运行配置文件内容

student类:

public class Student {
public void show(){
System.out.println("is show()");
}
}

配置文件(这里事txt文件)

className = cn.fanshe.Student
methodName = show

反射运行配置文件:

import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Properties;

/*
* 我们利用反射和配置文件,可以使:应用程序更新时,对源码无需进行任何修改
* 我们只需要将新类发送给客户端,并修改配置文件即可
*/
public class Demo {
public static void main(String[] args) throws Exception {
//通过反射获取Class对象
Class stuClass = Class.forName(getValue("className"));//"cn.fanshe.Student"
//2获取show()方法
Method m = stuClass.getMethod(getValue("methodName"));//show
//3.调用show()方法
m.invoke(stuClass.getConstructor().newInstance());

}

//此方法接收一个key,在配置文件中获取相应的value
public static String getValue(String key) throws IOException{
Properties pro = new Properties();//获取配置文件的对象
FileReader in = new FileReader("pro.txt");//获取输入流
pro.load(in);//将流加载到配置文件对象中
in.close();
return pro.getProperty(key);//返回根据key获取的value值
}
}

此时控制台会输出:

is show()

当我们升级这个系统时,不要Student类,而需要新写一个Student2的类时,这时只需要更改pro.txt的文件内容就可以了。代码就一点不用改动

student2:

public class Student2 {
public void show2(){
System.out.println("is show2()");
}
}

该配置文件:

className = cn.fanshe.Student2
methodName = show2

此时控制台会输出:

is show2();

通过反射越过泛型检查

泛型用在编译期,编译过后泛型擦除(消失掉)。所以是可以通过反射越过泛型检查的

import java.lang.reflect.Method;
import java.util.ArrayList;

/*
* 通过反射越过泛型检查
*
* 例如:有一个String泛型的集合,怎样能向这个集合中添加一个Integer类型的值?
*/
public class Demo {
public static void main(String[] args) throws Exception{
ArrayList<String> strList = new ArrayList<>();
strList.add("aaa");
strList.add("bbb");

// strList.add(100);
//获取ArrayList的Class对象,反向的调用add()方法,添加数据
Class listClass = strList.getClass(); //得到 strList 对象的字节码 对象
//获取add()方法
Method m = listClass.getMethod("add", Object.class);
//调用add()方法
m.invoke(strList, 100);

//遍历集合
for(Object obj : strList){
System.out.println(obj);
}
}
}

控制台输出:

aaa
bbb
100