java安全-反射

一、什么是反射?

一段代码,改变其中的变量量,将会导致这段代码产⽣生功能性的变化,我称之为动态特性。

比如说

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

在你不知道传入的参数值的时候,你是不知道他的作⽤是干什么的

在安全研究中,我们使⽤用反射的⼀⼤⽬的,就是绕过某些沙盒。比如,上下文中如果只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许可以这样:

1
1.getClass().forName("java.lang.Runtime")

二、反射常用函数

1.forName

Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤用 forName来获取。

forname方法有多个方法重载,根据参数可分为

1
2
3
Class<?> forName(String name)
Class<?> forName(String name, boolean initialize, ClassLoader loader)
第一个实现等价于`Class.forName(className, true, currentClassLoader);`
  • 参数说明:

    • name:类名,如 "java.lang.Integer"

    • initialize:

      • true:加载 + 链接 + 初始化(执行 static 块)
      • false:只加载,不执行 static 块
    • loader:指定类加载器

​ 类加载 ≠ 创建对象;只有创建对象才会调用构造方法,此时未调用构造方法。

  • 类的三种初始化方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class TrainPrint {
    { // ① 实例初始化块(Instance Initializer Block)
    System.out.printf("Empty block initial %s\n", this.getClass());
    }

    static { // ② 静态初始化块(Static Initializer Block)
    System.out.printf("Static initial %s\n", TrainPrint.class);
    }

    public TrainPrint() { // ③ 构造方法(Constructor)
    System.out.printf("Initial %s\n", this.getClass());
    }
    }

    使用forname只会调用static即静态初始化块,其在类被加载(初始化)时执行

    因此可以在恶意类中static加入恶意代码,从而执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    例如:
    import java.lang.Runtime;
    import java.lang.Process;
    public class TouchFile {
    static {
    try {
    Runtime rt = Runtime.getRuntime();
    String[] commands = {"touch", "/tmp/success"};
    Process pc = rt.exec(commands);
    pc.waitFor();
    } catch (Exception e) {
    // do nothing
    }
    }
    }
  • 实现沙箱绕过

    1.getClass().forName("java.lang.Runtime")详解

    本质就是class类中有静态方法forname,所以先通过getClass获取integer类对象,进而获取Runtime对象

​ 在SPEL注入中常遇到

  • 内部类加载

    静态内部类不依赖外部类实例,可以直接通过 Class.forName

    1
    2
    Class<?> clazz = Class.forName("C1$C2");        //加载类
    Object obj = clazz.getDeclaredConstructor().newInstance(); //c

2.getMethodinvoke

getMethod 的作用是通过反射获取一个类的某个特定的公有方法。

参数为方法参数列表类,Method m = cls.getMethod("greet", String.class, int.class);

可以加入declared进行获取私有方法

**invoke**的作用是执行函数。

  • 参数详解,method.invoke(obj, 123, "abc");

    • obj:要调用的方法所属对象实例
      • 对于 实例方法:必须传该类的一个对象实例
      • 对于 静态方法:可以传 null或任意实例
    • args:参数列表(必须与方法参数类型一一对应

    返回值:

    • 返回该方法的返回结果,类型为 Object
    • 方法返回 void 时结果是 null

使用该方法进行Runtime反射

  • 1
    2
    3
    4
    5
    Class clazz = Class.forName("java.lang.Runtime");
    Method execMethod = clazz.getMethod("exec",String.class);
    Method getRuntimeMethod = clazz.getMethod("getRuntime");
    Runtime runtime = (Runtime) getRuntimeMethod.invoke(null);
    execMethod.invoke(runtime,"calc");

3.getConstructornewInstance()

**getConstructor的作用是从类对象中获取一个公有的(public)**构造函数对象(Constructor)。

参数详解:代表对应构造器参数列表,例如:clazz.getConstructor(String.class, int.class)

**newInstance()**的作用执行这个构造函数,在内存中创建出该类的实例(也就是 new 了一个对象)。

参数详解:它接收的参数是 可变参数(Object…),这里传入的是真正的值,而不是类型,需要与构造器一一对应。

  • Class中newInstance与Reflect包下的newinstance

    Class中直接调用newinstance的已经弃用,大概缺点如下:

    image-20251212180352614

  • 绕过类型转换

    原始

    1
    2
    3
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    ((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc"))).start();
    很多表达式语言(或者构造的利用链工具,如 Transformer 链)**语法是阉割版或者受限的**。它们可能:
    1. 根本没有设计 (Type) 这种强转语法
    2. 或者是在构造一个对象链(Chain),而不是写代码,链条里每一个环节只能传递 Object。

    绕过

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

    相当于找到 ProcessBuilder 类里的 start 方法。然后,直接把这个 start 方法应用在 obj 对象上执行!

  • newinstance中自动解包造成的问题

    在反射创建对象使用ProcessBuilder中可变参数传参时,会导致newInstance与构造方法发生冲突

    image-20251212182434978

    所以我们进行二维数组正确写法

    1
    2
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    clazz.getMethod("start").invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc"}}));

4.getDeclared 系列

getDeclared 系列 (getDeclaredMethod, getDeclaredField, getDeclaredConstructor)

  • 权限: 获取该类声明的所有成员(Public, Protected, Default, Private 全部都能拿到)。
  • 范围: 只看当前类自己写的不包括从父类继承下来的任何东西。

setAccessible(true)

1
2
3
Constructor c = Runtime.class.getDeclaredConstructor();
c.setAccessible(true);
Runtime r = (Runtime) c.newInstance();

三、java反射中一些小知识

1.Class

在 Java 中,万物皆对象。所以,“类”本身就是对象

  • 当我们编写一个 Person.java 文件并编译成Person.class字节码文件后。
  • JVM(Java虚拟机)启动并加载这个 .class 文件时,JVM 会在内存中创建一个对应的 java.lang.Class 对象
  • 这个对象包含了Person类的所有元信息(类名、包名、有哪些方法、有哪些字段、构造函数是什么等等)。

Person 是我们定义的一个类,用来创建 p1, p2 实例。
而 Class 类是用来描述 Person 这个类的。它是“类的类”。

获取 Class 对象的三种方式

1
2
3
4
5
6
7
8
Object.getClass()
String str = "Hello";
Class<?> clazz = str.getClass()
.class 静态语法
Class<?> clazz = String.class;
Class<?> intClass = int.class; // 基本数据类型也有 Class 对象
Class.forName("全限定类名")
Class<?> clazz = Class.forName("java.net.URL");

2.反射修改static、final修饰的字段

  • static

    1. 参数传 nullfield.set(null, val)
    2. 权限控制:如果字段是 private,必须调用 setAccessible(true)
    3. 影响范围:修改后,整个 JVM 进程中,所有使用该类的地方获取到的值都会改变(因为是全局共享的)
  • final

    1. 必须 setAccessible(true):哪怕是 public final,想改值也必须加这一行,否则会报错。
    2. 常量折叠(内联):如果 final 字段是基本数据类型或 String 字面量,且在声明时赋值,反射修改虽然成功修改了内存,但对已有代码可能不可见

    有效场景

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class InDirectPerson {  
    private final StringBuilder sex = new StringBuilder("male");
    // 经过逻辑判断产生的变量赋值
    public final int age = (null!=null?18:18);
    private final String name;
    public InDirectPerson(){
    name = "Drunkbaby";
    }
    }
  • static + final

    对于 static final 字段,Java 在设计上是不允许修改的

    1. 反射检查(Runtime Check):
      当你尝试调用 field.set(null, newValue) 修改一个同时被 static 和 final 修饰的字段时,JDK 的反射实现会显式检查:如果不去除 final 修饰符,它会直接抛出 IllegalAccessException
    2. 编译器内联(Compile-time Inlining):
      如果字段是编译期常量,编译器会将引用该字段的地方直接替换为具体的值。

    绕过修改方法

    1. 获取字段的 Field 对象。
    2. 修改 Field 对象自身的 modifiers 属性,把 FINAL 的标志位抹去。使其看起来像是一个普通的 static 字段。
    3. 重新赋值。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class MagicReflection {
    // 场景 B:间接赋值(运行期确定)- 对象、方法返回值、构造块
    private static final String INDIRECT_VAL = new String("Old Value");

    public static void main(String[] args) throws Exception {
    System.out.println("修改前 Indirect: " + INDIRECT_VAL);
    modifyStaticFinalField("INDIRECT_VAL", "New Value");

    System.out.println("修改后 Indirect: " + INDIRECT_VAL);
    }

    private static void modifyStaticFinalField(String fieldName, Object newValue) throws Exception {
    // 1. 获取目标字段
    Field targetField = MagicReflection.class.getDeclaredField(fieldName);
    targetField.setAccessible(true);

    // 2. 获取 Field 类中的 modifiers 字段
    // 这一步是为了去除 targetField 的 final 属性
    Field modifiersField = Field.class.getDeclaredField("modifiers");
    modifiersField.setAccessible(true);

    // 3. 去掉 final 修饰符 (使用位运算 & ~Modifier.FINAL)
    modifiersField.setInt(targetField, targetField.getModifiers() & ~Modifier.FINAL);

    // 4. 设置新值
    targetField.set(null, newValue);
    }
    }