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;
public class Fanshe { public static void main(String[] args) { Student stu1 = new Student(); Class stuClass = stu1.getClass(); System.out.println(stuClass.getName()); Class stuClass2 = Student.class; System.out.println(stuClass == stuClass2); try { Class stuClass3 = Class.forName("fanshe.Student"); System.out.println(stuClass3 == stuClass2); } 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);
|
创建一个类
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<?> c1 = Class.forName("reflect.Student"); Constructor<?> constructor = c1.getDeclaredConstructor(int.class); constructor.setAccessible(true); Student student = (Student) constructor.newInstance(100); 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<?> c1 = Class.forName("reflect.Student"); Field field = c1.getDeclaredField("id"); field.setAccessible(true); Student student = (Student) c1.newInstance(); field.set(student,500); 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<?> c1 = Class.forName("reflect.Student"); Method method = c1.getDeclaredMethod("func2",String.class); method.setAccessible(true); Student student = (Student) c1.newInstance(); 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 总是不成功,这时候原因可能是:
你使用的类没有无参构造函数
你使用的类构造函数是私有的
所以如果我们这样执行命令是不行的:(这本来是一个有公共的无参构造的执行方法)
Class clazz = Class.forName("java.lang.Runtime"); clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
|
会报错:
原因是 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); 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有两个构造函数:
我上面用到了第一个形式的构造函数,所以我在 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;
public class Main { public static void main(String[] args) { try { Class clazz = Class.forName("fanshe.main.Student"); Method methodMain = clazz.getMethod("main", String[].class); methodMain.invoke(null, (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 stuClass = Class.forName(getValue("className")); Method m = stuClass.getMethod(getValue("methodName")); m.invoke(stuClass.getConstructor().newInstance()); } 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); } }
|
此时控制台会输出:
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;
public class Demo { public static void main(String[] args) throws Exception{ ArrayList<String> strList = new ArrayList<>(); strList.add("aaa"); strList.add("bbb"); Class listClass = strList.getClass(); Method m = listClass.getMethod("add", Object.class); m.invoke(strList, 100); for(Object obj : strList){ System.out.println(obj); } } }
|
控制台输出:
aaa
bbb
100