Java反射
反射有关的类方法
首先给出一个例子
public void execute(String className, String methodName) throws Exception {Class clazz = Class.forName(className);clazz.getMethod(methodName).invoke(clazz.newInstance());}
在上面的例子中,有几个反射里极为重要的⽅法
获取类的方法
forName实例化类对象的方法
newInstance获取函数的方法
getMethod执行函数的方法
invoke
基本上,这⼏个⽅法包揽了Java安全⾥各种和反射有关的Payload。
获取类的方法
forName不是唯一获取”类”的唯一途径,通常来说我们有如下三种⽅式获取⼀个“类”,也就 是 java.lang.Class 对象:
- obj.getClass() 如果上下⽂中存在某个类的实例 obj ,那么我们可以直接通过 obj.getClass() 来获取它的类
- Test.class 如果你已经加载了某个类,只是想获取到它的 java.lang.Class 对象,那么就直接 拿它的 class 属性即可。这个⽅法其实不属于反射。
- Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤ forName 来获取
在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。⽐如,上下⽂中如果只有Integer类型的 数字,我们如何获取到可以执⾏命令的Runtime类呢?也许可以这样(伪代 码):1.getClass().forName("java.lang.Runtime")
forName
forName有两个函数重载:
- Class forName(String name)
- Class forName(String name, boolean initialize, ClassLoader loader)
第⼀个就是我们最常⻅的获取class的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:
Class.forName(className)// 等于Class.forName(className, true, currentLoader)
默认情况下, forName 的第⼀个参数是类名;第⼆个参数表示是否初始化;第三个参数就是ClassLoader 。
ClassLoader是一个加载器,告诉Java虚拟机如何加载这个类。这个类名是类完整路径,比如
java.lang.Runtime
第二个参数initialize其实不是初始化构造函数,即使它为True。构造函数并不会执行。
这个初始化可以理解为类的初始化。
demo
import java.io.IOException;public class reflect {public void execute(String className, String methodName) throws Exception {Class clazz = Class.forName(className);clazz.getMethod(methodName).invoke(clazz.newInstance());}public static 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());}}public static void main(String[] args) {TrainPrint trainPrint=new TrainPrint();}}
输出的时候我们发现最先输出的是static{},然后是{},最后才是构造方法。
其中,static{}是在“类初始化”的时候调用的。所以说,forName中的initialize=true
其实就是告诉Java虚拟机是否执⾏”类初始化“。
那么,假如有如下函数,name可控。
public static void ref(String name) throws Exception {Class.forName(name);}
我们就可以编写一个恶意类,把恶意代码放在static{}中,从而执行。
import java.lang.Runtime;import java.lang.Process;public class Eval {static {try {Runtime rt = Runtime.getRuntime();String[] commands = {"calc"};Process pc = rt.exec(commands);pc.waitFor();} catch (Exception e) {// do nothing}}}

当然,如何将这个恶意类带入机器,之后我们再研究
invoke,getMethod
在正常情况下,除了系统类,如果我们想拿到一个类,需要先 import 才能使用。而使用forName就不 需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。
另外,我们经常在一些源码里看到,类名的部分包含 $ 符号,比如fastjson在 checkAutoType 时候就会 先将 $ 替换成 .:https://github.com/alibaba/fastjson/blob/fcc9c2a/src/main/java/com/alibaba/fa stjson/parser/ParserConfig.java#L1038。 $ 的作用是查找内部类。
Java的普通类 C1 中支持编写内部类 C2 ,而在编译的时候,会生成两个文件: C1.class 和 C1$C2.class ,我们可以把他们看作两个无关的类,通过 Class.forName("C1$C2") 即可加载这个内部类。
获得类以后,我们可以继续使用反射来获取这个类中的属性、方法,也可以实例化这个类,并调用方法。
class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候 在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:
- 你使用的类没有无参构造函数
- 你使用的类构造函数是私有的
最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:
Class clazz = Class.forName("java.lang.Runtime");clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
原因就是Runtime类的构造方法是私有的。
Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。我们将上述Payload进行修改即可正常执行命令了:
Class clazz = Class.forName("java.lang.Runtime");clazz.getMethod("exec",String.class).invoke(clazz.getMethod("getRuntime").invoke(clazz),"calc.exe");
这里用到了getMethod和invoke方法。
getMethod的作用是通过反射获取一个类的某个特定的公有方法。而学过Java的同学应该清楚,Java中 支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要 传给他你需要获取的函数的参数类型列表。
比如这里的 Runtime.exec 方法有6个重载:

我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用 getMethod("exec", String.class) 来获取Runtime.exec方法。
invoke 的作用是执行方法,它的第一个参数是:
如果这个方法是一个普通方法,那么第一个参数是类对象
如果这个方法是一个静态方法,那么第一个参数是类
这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...) 。
所以我们将上述命令执行的Payload分解一下就是:
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");
如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?
如果一个方法或构造方法是私有方法,我们是否能执行它呢?
getConstructor
我们需要用到一个新的反射方法 getConstructor 。 和getMethod类似, getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。
比如,我们常用的另一种执行命令的方式ProcessBuilder,我们使用反射来获取其构造函数,然后调用 start()来执行命令:
Class clazz = Class.forName("java.lang.ProcessBuilder");((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc.exe"))).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了。
getDeclared
与普通的 getMethod 、 getConstructor 区别是
getMethod系列方法获取的是当前类中的所有公共方法,包括从父类继承的方法。getDeclaredMethod系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了
getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor的具体用法和 getConstructor类似,我就不再赘述。
举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime()来 获取对象。其实现在我们也可以直接用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");
可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。
