Java反射

反射有关的类方法

首先给出一个例子

  1. public void execute(String className, String methodName) throws Exception {
  2. Class clazz = Class.forName(className);
  3. clazz.getMethod(methodName).invoke(clazz.newInstance());
  4. }

在上面的例子中,有几个反射里极为重要的⽅法

  • 获取类的方法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的⽅式,其实可以理解为第⼆种⽅式的⼀个封装:

  1. Class.forName(className)
  2. // 等于
  3. Class.forName(className, true, currentLoader)

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

ClassLoader是一个加载器,告诉Java虚拟机如何加载这个类。这个类名是类完整路径,比如

java.lang.Runtime

第二个参数initialize其实不是初始化构造函数,即使它为True。构造函数并不会执行。

这个初始化可以理解为类的初始化。

demo

  1. import java.io.IOException;
  2. public class reflect {
  3. public void execute(String className, String methodName) throws Exception {
  4. Class clazz = Class.forName(className);
  5. clazz.getMethod(methodName).invoke(clazz.newInstance());
  6. }
  7. public static class TrainPrint {
  8. {
  9. System.out.printf("Empty block initial %s\n", this.getClass());
  10. }
  11. static {
  12. System.out.printf("Static initial %s\n", TrainPrint.class);
  13. }
  14. public TrainPrint() {
  15. System.out.printf("Initial %s\n", this.getClass());
  16. }
  17. }
  18. public static void main(String[] args) {
  19. TrainPrint trainPrint=new TrainPrint();
  20. }
  21. }

输出的时候我们发现最先输出的是static{},然后是{},最后才是构造方法。Java反射 - 图1

其中,static{}是在“类初始化”的时候调用的。所以说,forName中的initialize=true

其实就是告诉Java虚拟机是否执⾏”类初始化“。

那么,假如有如下函数,name可控。

  1. public static void ref(String name) throws Exception {
  2. Class.forName(name);
  3. }

我们就可以编写一个恶意类,把恶意代码放在static{}中,从而执行。

  1. import java.lang.Runtime;
  2. import java.lang.Process;
  3. public class Eval {
  4. static {
  5. try {
  6. Runtime rt = Runtime.getRuntime();
  7. String[] commands = {"calc"};
  8. Process pc = rt.exec(commands);
  9. pc.waitFor();
  10. } catch (Exception e) {
  11. // do nothing
  12. }
  13. }
  14. }

Java反射 - 图2

当然,如何将这个恶意类带入机器,之后我们再研究

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 总是不成功,这时候原因可能是:

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

最最最常见的情况就是 java.lang.Runtime ,这个类在我们构造命令执行Payload的时候很常见,但我们不能直接这样来执行命令:

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

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

Runtime类就是单例模式,我们只能通过 Runtime.getRuntime() 来获取到 Runtime 对象。我们将上述Payload进行修改即可正常执行命令了:

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

这里用到了getMethodinvoke方法。

getMethod的作用是通过反射获取一个类的某个特定的公有方法。而学过Java的同学应该清楚,Java中 支持类的重载,我们不能仅通过函数名来确定一个函数。所以,在调用 getMethod 的时候,我们需要 传给他你需要获取的函数的参数类型列表。

比如这里的 Runtime.exec 方法有6个重载:

Java反射 - 图3

我们使用最简单的,也就是第一个,它只有一个参数,类型是String,所以我们使用 getMethod("exec", String.class) 来获取Runtime.exec方法。

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

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

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

这也比较好理解了,我们正常执行方法是 [1].method([2], [3], [4]...) ,其实在反射里就是 method.invoke([1], [2], [3], [4]...)

所以我们将上述命令执行的Payload分解一下就是:

  1. Class clazz = Class.forName("java.lang.Runtime");
  2. Method execMethod = clazz.getMethod("exec", String.class);
  3. Method getRuntimeMethod = clazz.getMethod("getRuntime");
  4. Object runtime = getRuntimeMethod.invoke(clazz);
  5. execMethod.invoke(runtime, "calc.exe");
  • 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,我们怎样通过反射实例化该类呢?

  • 如果一个方法或构造方法是私有方法,我们是否能执行它呢?

getConstructor

我们需要用到一个新的反射方法 getConstructor 。 和getMethod类似, getConstructor接收的参数是构造函数列表类型,因为构造函数也支持重载, 所以必须用参数列表类型才能唯一确定一个构造函数。

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

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

ProcessBuilder有两个构造函数:

  • public ProcessBuilder(List command)

  • public ProcessBuilder(String… command)

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

但是,我们看到,前面这个Payload用到了Java里的强制类型转换,有时候我们利用漏洞的时候(在表达式上下文中)是没有这种语法的。所以,我们仍需利用反射来完成这一步。其实用的就是前面讲过的知识:

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

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

getDeclared

与普通的 getMethodgetConstructor 区别是

  • getMethod系列方法获取的是当前类中的所有公共方法,包括从父类继承的方法。

  • getDeclaredMethod 系列方法获取的是当前类中“声明”的方法,是实在写在这个类里的,包括私 有的方法,但从父类里继承来的就不包含了

getDeclaredMethod 的具体用法和 getMethod 类似, getDeclaredConstructor的具体用法和 getConstructor类似,我就不再赘述。

举个例子,前文我们说过Runtime这个类的构造函数是私有的,我们需要用 Runtime.getRuntime()来 获取对象。其实现在我们也可以直接用getDeclaredConstructor来获取这个私有的构造方法来实例 化对象,进而执行命令:

  1. Class clazz = Class.forName("java.lang.Runtime");
  2. Constructor m = clazz.getDeclaredConstructor();
  3. m.setAccessible(true);
  4. clazz.getMethod("exec", String.class).invoke(m.newInstance(), "calc.exe");

可见,这里使用了一个方法 setAccessible ,这个是必须的。我们在获取到一个私有方法后,必须用 setAccessible 修改它的作用域,否则仍然不能调用。