Java安全之反射
反射(一)
反射中一些极为重要的方法:
- 获取类的方法:
forName
- 实例化类对象的方法:
newInstance
- 获取函数的方法:
getMethod
- 执行函数的方法:
invoke
forName
并不是获取“类”的唯⼀途径,获取类一共有三种方法,这个我在上节开头有说。
forName()
forname()函数
1 | Class.forName(className) |
看着三个参数
第一个className
,即要加载的类,第二个是是否初始化类,第三个是ClassLoader
.
类初始化的顺序
1 | package Safe; |
初始化顺序依次是(静态变量、静态初始化块)>(变量、初始化块)>构造器。
修改下上面代码
1 | package Safe; |
那么也就是说,当我forName()
变量可控时,我们就可以调用static
中的恶意方法。从而进行攻击。
1 | //Main.java |
通过编写一个恶意的类(弹计算器),然后把恶意的方法放在static
里面,当我们用forName
执行的时候,类第一次被加载,初始化,触发恶意的方法,从而执行代码(弹出计算器)
反射(二)
$
的作用是寻找内部类
1 | package Safe; |
编译这个java文件,会得到下面这两个文件
可以看到,内部类的的名称是外部类$内部类名
,如果我们要加载这个内部类,也可以使用forName(“Main$Test”);加载它。
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
Class.newInstance()
可以调用一个类的无参构造函数。但是有时候会出现报错
1 | //TuNan1.java |
可以得到如下错误
说的是TuNan2中的无参构造函数时private
修饰的,所以报错了。这也是第一种常见错误。
同时TuNan3也会报错,因为根本就没有无参构造函数,自然也没法调用了。这是第二种错误。
为什么有的类的构造函数是私有的,这就涉及了java中的单例模式。这种模式,最显著的特点就是,构造函数时私有的,保证该类只有一个实例。
例如Runtime
类,我们只能通过Runtime.getRuntime()
来获取这个类的对象。
那我们应该如何调用这种类的方法呢
1 | package Safe; |
关于invoke
:
- 如果这个方法是一个普通方法,那么第一个参数是对象
- 如果这个方法是一个静态方法,那么第一个参数是类
由于是单例模式,我们进去看一下源码
getRuntime
其实返回了一个currentRuntime
,再往上看,这其实就是Runtime
的实例。
我们先通过这个方法获取了对象,然后由于invoke
第二个参数是方法参数也就是我们之前获取的exec
方法的参数,传入,执行该方法,弹出计算器。
反射(三)
p牛在上一节提了两个问题
- 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
- 如果一个方法或构造方法是私有方法,我们是否能执行它呢?
这其实也不是什么问题,如果之前有看过反射教程的话,应该就可以很快解决这俩个问题。
getConstructor(Class...)
:获取某个public
的Constructor
;getDeclaredConstructor(Class...)
:获取某个Constructor
;getConstructors()
:获取所有public
的Constructor
;getDeclaredConstructors()
:获取所有Constructor
。- 调用非
public
的Constructor
时,必须首先通过setAccessible(true)
设置允许访问。setAccessible(true)
可能会失败。
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
有时候我们需要进行强制转化,来实现相应的功能,但是强制转换也不是唯一的路,其实我们也可以纯使用反射来做到。
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
先使用forname获取到类,然后通过getMethod获取方法,在使用invoke执行。
一个技巧
对于反射来说,如果要获取的目标函数里包含可变长参数,那么我们认为它是数组就行了。