反射
开始学习java安全,反射肯定是避不开的,虽然很早之前写过一点记录,但还是再学一遍
Java反射是啥
Java
反射就是说,对于任意的一个类,我们都可以通过反射获取这个类中所有的属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制
通过Class
类对象来获取Student
和Teacher
类中的成员变量和成员方法,而不是直接通过创建Student
和Teacher
类对象,这就叫反射
利用反射创建类对象
获取了Class对象,现在可以通过反射来生成实例化对象,一般我们使用Class对象的newInstance()方法来进行创建类对象。
使用的方式也特别简单,只需要通过forname方法获取到的class对象中进行newInstance方法创建即可。
1 | Class c = Class.forName("com.reflect.MethodTest"); // 创建Class对象 |
利用反射机制创建类并执行方法
1 | public class ReflectTest { |
在正常情况下,除了系统类,如果我们想拿到一个类,需要先导入才能使用。而使用forName就不需要,这样对于攻击者来说就十分有利,我们可以加载任意类
同时,forName()还可以获取内部类
我们通过forName获得类之后用newInstance()来调用这个类的无参构造函数,但有时这个方法会失败,原因可能是:
1 | 你使用的类没有无参构造函数 |
常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
因为Runtime类的构造方法是私有的。
但还是有方法获取到这个类的,这涉及到单例模式。
比如,对于Web应用来说,数据库连接只需要建立一次,而不是每次用到数据库的时候再新建立一个连接,就可以将数据库连接使用的类的构造函数设置为私有,然后编写一个静态方法来获取:
1 | public class TrainDB { |
之后获取这个类的方法为getInstance。
Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime对象。将上述Payload进行修改即可正常执行命令了:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
Runtime.exec有6个重载,第一个重载,它只有一个参数,类型是String,所以我们使用 getMethod(“exec”, String.class) 来获取 Runtime.exec 方法。
invoke 的作用是执行方法,它的第一个参数是:
1.如果这个方法是一个普通方法,那么第一个参数是类对象
2.如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]…) ,其实在反射里就是 method.invoke([1], [2], [3], [4]…) 。
所以我们将上述命令执行的Payload分解一下就是:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
解决两个问题
1.如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
2.如果一个方法或构造方法是私有方法,我们是否能执行它呢?
那么我们可以尝试获取构造器来实例化类
第一个问题
这里要引入新的反射方法 getConstructor
和 getMethod 类似,getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。
获取到构造函数后,我们使用 newInstance 来执行。
ProcessBuilder有两个构造函数:
1 | 1.public ProcessBuilder(List<String> command) |
比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start() 来执行命令
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
上面用到了第一个形式的构造函数,所以我在 getConstructor 的时候传入的是 List.class 。
但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表 达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。
其实用的就是前面讲过的知识:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
通过 getMethod(“start”) 获取到start方法,然后 invoke 执行, invoke 的第一个参数就是 ProcessBuilder Object了。
那么,如果我们要使用 public ProcessBuilder(String… command) 这个构造函数,需要怎样用反 射执行呢?
这又涉及到Java里的可变长参数了。正如其他语言一样,Java也支持可变长参数,就是当你 定义函数的时候不确定参数数量的时候,可以使用 … 这样的语法来表示“这个函数的参数个数是可变的”。
对于可变长参数,Java其实在编译的时候会编译成一个数组,也就是说,如下这两种写法在底层是等价 的(也就不能重载):
1 | public void hello(String[] names) {} |
那么对于反射来说,如果要获取的目标函数里包含可变长参数,其实我们认为它是数组就行了。
所以,我们将字符串数组的类 String[].class 传给 getConstructor ,获取 ProcessBuilder 的第二种构造函数:
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
在调用 newInstance 的时候,因为这个函数本身接收的是一个可变长参数,我们传给 ProcessBuilder 的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
第二个问题
这就涉及到 getDeclared 系列的反射了,与普通的 getMethod 、 getConstructor 区别是:
1.getMethod 系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
2.getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私有的方法,但从父类里继承来的就不包含了
举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime() 来 获取对象。其实现在我们也可以直接用 getDeclaredConstructor 来获取这个私有的构造方法来实例 化对象,进而执行命令:
1 | Class clazz = Class.forName("java.lang.Runtime"); |