java安全-反射
java安全-反射
一、什么是反射?
一段代码,改变其中的变量量,将会导致这段代码产⽣生功能性的变化,我称之为动态特性。
比如说
1 | public void execute(String className, String methodName) throws Exception { |
在你不知道传入的参数值的时候,你是不知道他的作⽤是干什么的
在安全研究中,我们使⽤用反射的⼀⼤⽬的,就是绕过某些沙盒。比如,上下文中如果只有Integer类型的数字,我们如何获取到可以执行命令的Runtime类呢?也许可以这样:
1 | 1.getClass().forName("java.lang.Runtime") |
二、反射常用函数
1.forName
Class.forName 如果你知道某个类的名字,想获取到这个类,就可以使⽤用 forName来获取。
forname方法有多个方法重载,根据参数可分为
1 | Class<?> forName(String name) |
参数说明:
name:类名,如
"java.lang.Integer"initialize:
true:加载 + 链接 + 初始化(执行 static 块)false:只加载,不执行 static 块
loader:指定类加载器
类加载 ≠ 创建对象;只有创建对象才会调用构造方法,此时未调用构造方法。
类的三种初始化方式
1
2
3
4
5
6
7
8
9
10
11
12
13public 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
2Class<?> clazz = Class.forName("C1$C2"); //加载类
Object obj = clazz.getDeclaredConstructor().newInstance(); //c
2.getMethod与invoke
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
5Class 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.getConstructor与newInstance()
**getConstructor的作用是从类对象中获取一个公有的(public)**构造函数对象(Constructor)。
参数详解:代表对应构造器参数列表,例如:clazz.getConstructor(String.class, int.class)
**newInstance()**的作用执行这个构造函数,在内存中创建出该类的实例(也就是 new 了一个对象)。
参数详解:它接收的参数是 可变参数(Object…),这里传入的是真正的值,而不是类型,需要与构造器一一对应。
Class中newInstance与Reflect包下的newinstance
Class中直接调用newinstance的已经弃用,大概缺点如下:

绕过类型转换
原始
1
2
3Class clazz = Class.forName("java.lang.ProcessBuilder");
((ProcessBuilder)clazz.getConstructor(List.class).newInstance(Arrays.asList("calc"))).start();
很多表达式语言(或者构造的利用链工具,如 Transformer 链)**语法是阉割版或者受限的**。它们可能:- 根本没有设计 (Type) 这种强转语法。
- 或者是在构造一个对象链(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与构造方法发生冲突
所以我们进行二维数组正确写法
1
2Class 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 | Constructor c = Runtime.class.getDeclaredConstructor(); |
三、java反射中一些小知识
1.Class类
在 Java 中,万物皆对象。所以,“类”本身就是对象
- 当我们编写一个
Person.java文件并编译成Person.class字节码文件后。 - 当
JVM(Java虚拟机)启动并加载这个 .class 文件时,JVM会在内存中创建一个对应的java.lang.Class对象。 - 这个对象包含了
Person类的所有元信息(类名、包名、有哪些方法、有哪些字段、构造函数是什么等等)。
Person 是我们定义的一个类,用来创建 p1, p2 实例。
而 Class 类是用来描述 Person 这个类的。它是“类的类”。
获取 Class 对象的三种方式
1 | Object.getClass() |
2.反射修改static、final修饰的字段
static
- 参数传 null:
field.set(null, val)。 - 权限控制:如果字段是
private,必须调用setAccessible(true)。 - 影响范围:修改后,整个
JVM进程中,所有使用该类的地方获取到的值都会改变(因为是全局共享的)
- 参数传 null:
final
- 必须
setAccessible(true):哪怕是public final,想改值也必须加这一行,否则会报错。 - 常量折叠(内联):如果 final 字段是基本数据类型或 String 字面量,且在声明时赋值,反射修改虽然成功修改了内存,但对已有代码可能不可见。
有效场景
1
2
3
4
5
6
7
8
9public 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 在设计上是不允许修改的- 反射检查(
Runtime Check):
当你尝试调用field.set(null, newValue)修改一个同时被static 和 final修饰的字段时,JDK的反射实现会显式检查:如果不去除 final 修饰符,它会直接抛出IllegalAccessException。 - 编译器内联(
Compile-time Inlining):
如果字段是编译期常量,编译器会将引用该字段的地方直接替换为具体的值。
绕过修改方法
- 获取字段的 Field 对象。
- 修改 Field 对象自身的 modifiers 属性,把 FINAL 的标志位抹去。使其看起来像是一个普通的 static 字段。
- 重新赋值。
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
28public 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);
}
}- 反射检查(