java安全基础_反射篇

Java安全基础——反射篇

Java反射

什么是JAVA反射

Java反射机制是在运行状态时,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。

简单的说,就是通过java反射机制,我们可以获取任意类的成员方法、变量这些,同时,可以创建JAVA类实例,调用任意类方法。

还有就是想说一下这里为什么称为反射,在JAVA中,我们可以这样理解:通过new一个对象而调用其中方法的,称为”正射”,而不通过new一个对象取得其中方法的,称为”反射”。

反射的动态机制

反射机制是java实现动态语言的关键,也就是通过反射机制来实现类动态加载

静态加载:编译时加载相关的类,没有就会报错

就比如,

1
2
3
4
5
6
public class Main{
public static void main(String[] args) throws Exception{
Dog dog = new Dog();
dog.cry();
}
}

如果 Dog 类没有定义,编译器会直接报错 “cannot find symbol: class Dog”,因为 Java 采用静态加载机制,所有使用的类必须在编译时就能找到。

1
2
3
4
5
6
7
8
public class Main{
public static void main(String[] args) throws Exception{
Class cls = Class.forName("Person");
Object o = cls.newInstance();
Method m = cls.getMethod("hi");
m.invoke(o);
}
}

Class.forName("Person")反射机制,属于动态加载,只有在运行时才会去查找 Person 类。

如果 Person 类不存在,编译时不会报错(因为反射不需要在编译时确定类),但运行时会抛出ClassNotFoundException

反射机制允许程序在执行期借助于ReflectionAPI取得任何类的内部信息(比如成员变量、构造器、成员方法等等),并能操作对象的属性及方法。加载完类之后,就会在堆中生成一个Class类型的对象(一个类只有一个Class对象),这个对象就包含了类的完整结构信息。这里借用大佬的反射流程图

image-20250405145839570

ClassLoader 就是⼀个“加载器”,告诉Java虚拟机如何加载这个类。

那么java的反射机制会完成:

  • 运行时判断任意一个对象所属的类

使用 Object.getClass() 方法可以获取对象的 Class 对象,从而得知该对象所属的类。

1
2
3
4
5
6
7
8
9
public class Main {
public static void main(String[] args) {
String s = "Hello";
Class<?> clazz = s.getClass();
System.out.println("类名: " + clazz.getName());
}
}

//输出:类名: java.lang.String
  • 运行时构造任意一个类的对象

使用Class.forName()newInstance()可以创建类的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person{
public Person(){
System.out.println("Person 构造方法被调用");
}
}

public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Person");
Object obj = clazz.newInstance();
}
}

//输出:Person 构造方法被调用
  • 在运行时得到一个类的所具有的成员变量和方法

使用Class类的方法getFields(),getDeclaredFields(),getMethods(),getDeclaredMethods()可以获取字段和方法信息

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
29
30
31
32
33
import java.lang.reflect.*;

class Person{
public String name;
public int age;

public Person(){
System.out.println("Person 构造方法被调用");
}
public void sayhello(){
System.out.println("hello~");
}
}

public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("Person"); //这个就是运行时通过字符串动态加载
//Class<?> clazz = Person.class; 这样的方法属于静态编译
//已经定义的方法,但不包括构造方法
for(Method m : clazz.getDeclaredMethods()){
System.out.println(m.getName());
}
//已经定义的变量
for(Field f : clazz.getDeclaredFields()){
System.out.println(f.getName());
}
}
}

//输出:
sayhello
name
age

Class.forNamePerson.class总结对比:

特性 Class.forName("Person") Person.class
加载方式 运行时通过字符串动态加载 编译时已知
是否安全 不安全,可能类不存在会抛异常 安全,编译时检查
是否需要全限定名 是,必须包含包名
是否能用于泛型等 可以,但更适合反射用途 适合泛型、注解、静态反射等
用途 框架/工具/反射 正常开发、泛型等
  • 在运行时调用任意一个对象的成员变量和方法

通过反射可以访问私有变量、调用私有或公有方法。

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
29
30
31
32
import java.lang.reflect.*;

class Person {
private String name = "张三";

private void sayHello(String msg) {
System.out.println("Hello, " + msg);
}
}

public class Main {
public static void main(String[] args) throws Exception {
Class<?> clazz = Person.class;
Object obj = clazz.getDeclaredConstructor().newInstance();

// 修改私有字段
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(obj, "李四");
System.out.println("修改后的name: " + nameField.get(obj));

// 调用私有方法
Method method = clazz.getDeclaredMethod("sayHello", String.class);
method.setAccessible(true);
method.invoke(obj, "Java");
}
}


//输出:
修改后的name: 李四
Hello, Java
  • 生成动态代理

Java 提供 Proxy 类用于创建动态代理对象,可以在运行时为某个接口生成代理,实现横切逻辑(如日志记录、权限校验等)。

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
29
30
31
32
33
34
35
36
37
38
39
40
import java.lang.reflect.*;

interface Hello {
void sayHello(String name);
}

class HelloImpl implements Hello {
@Override
public void sayHello(String name) {
System.out.println("hello "+name);
}
}

class MyInvocationHandler implements InvocationHandler {
private Object target;

public MyInvocationHandler(Object target) {
this.target = target;
}

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("执行前 "+method);
Object result = method.invoke(target, args);
System.out.println("执行后 "+method);
return result;
}
}

public class Main {
public static void main(String[] args) throws Exception {
Hello hello = new HelloImpl();
Hello proxy = (Hello) Proxy.newProxyInstance(
hello.getClass().getClassLoader(),
hello.getClass().getInterfaces(),
new MyInvocationHandler(hello)
);
proxy.sayHello("proxy");
}
}

反射相关的主要类如下:

1
2
3
4
1、Java.long.Class:代表一个类,Class对象表示某个类加载后在堆中的对象
2、Java.lang.reflect.Method:代表类的方法
3、Java.lang.reflect.Field:代表类的成员变量
4、Java.lang.reflect.Constructor:代表类的构造方法

上面的每个步骤已经演示。

Class类的分析

Class类对象不是new出来的,而是系统创建的。

Class类是由loadClass()方法完成类加载,生成了某个类对应的Class类对象。

前提是你的类没有加载过,才会进入到loadClass()方法,加载过的话,就直接返回了,就比如:

1
Class<?> aclass = Class.forName("java.lang.Object");

因为Object类肯定加载过了,他是主类嘛

我这里使用的是java.lang.Runtime

image-20250405213631294

打断点调就可以了

对于某个类的Class类对象,在内存中只有一份,因为类只加载一次

下面即可证明

image-20250408140335610

常用方法

字段获取和修改的方法

方法 描述
getFields() 获取类所有的字段,只能获取public类型的字段,可以获取父类的字段。
getDeclaredField(String name) 根据字段名获取对应的字段,可以获取public、protected和private类型的字段,不能获取父类的字段。
getDeclaredFields() 获取类所有的字段,包括public、protected和private。不能获取父类的字段。
getField(String name) 根据字段名获取对应的字段,只能获取public类型的字段,可以获取父类的字段。

在进行私有属性获取时,有两个注意事项。第一,必须使用getDeclaredField()函数来获取。第二,在对字段进行操作之前,必须field.setAccessible(true);这个表示设置允许对字段进行操作(上面也已经演示过)。

方法 描述
field.set(Obj,field_value) 设置Obj对象的field字段值为field_value
field.get(null) 获取static修饰的静态字段field的值
field.set(null,field_value) 设置static修饰的静态字段field的值为field_value
field.get(Obj) 获取Obj对象的field字段

方法获取和访问

方法 描述
getMethods() 通过反射获取一个类的某个特定的公有方法,只能获取public类型的字段,可以获取父类的方法。
getDeclaredMethod(String name, Class… parameterTypes) 根据方法名获取对应的方法,可以获取public、protected和private类型的方法,不能获取父类的方法。
getDeclaredMethods() 获取类所有的方法,包括public、protected和private。不能获取父类的方法。
getMethod(String name, Class… parameterTypes) 根据方法名和参数类型获取对应的方法,只能获取public类型的方法,可以获取父类的方法。

invoke——反射执行方法的函数

要使用通过反射获取的方法,需要使用invoke函数。关于invoke方法的使用如下:

方法 描述
invoke(Object obj, Object[] args) 执行通过反射获取的方法,如果这个方法是普通方法,obj代表要执行方法的类对象,如果是一个静态方法,那么obj是类,args代表方法的参数。

我们正常执行方法是[1].method([2],[3],[4]...),反射上就是method.invoke([1],[2],[3],[4]...)

构造函数获取和使用

方法 描述
getConstructors() 获取类所有的构造函数,只能获取public类型的字段,不能获取父类的构造函数。和 getMethod 类似, getConstructor 接收的参数是构造函数列表类型,因为构造函数也支持重载,所以必须用参数列表类型才能唯一确定一个构造函数。
getDeclaredConstructor (Class… parameterTypes) 所以必须用参数列表类型才能唯一确定一个构造函数。根据参数类型获取对应的构造函数,可以获取public、protected和private类型的构造函数,不能获取父类的构造函数。
getDeclaredConstructors() 获取类所有的构造函数,包括public、protected和private。不能获取父类的构造函数。
getConstructor(Class… parameterTypes) 根据参数类型获取对应的构造函数,只能获取public类型的构造函数,不能获取父类的构造函数。

newInstance——实例化类对象的方法

在获取到构造函数之后,需要通过newInstance函数来生成类对象。关于newInstance的使用如下所示:

方法 使用
newInstance(Object … initargs) newInstance函数接受可变的参数个数,构造函数实际有几个传输,这里就传递几个参数值。newInstance返回的数据类型是Object。

class.newInstance() 的作用就是调用这个类的无参构造函数,这个比较好理解。不过,我们有时候

在写漏洞利用方法的时候,会发现使用 newInstance 总是不成功,这时候原因可能是:

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

  2. 你使用的类构造函数是私有的

forName()——获取类的方法

forName有两个函数重载:

Class<?> forName(String name)

Class<?> forName(String name, \*\*boolean** initialize, ClassLoader loader)

forName方法可以获取到类的Class对象。当然获取到Class对象的不止forName()这一种方式可以获取这个Class对象,getClass()class也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main {
public static void main(String[] args) throws Exception {
ClassLoader currentLoader = Thread.currentThread().getContextClassLoader();
Class a = Class.forName("java.lang.Runtime");
Class a1 = Class.forName("java.lang.Runtime", true, currentLoader);
System.out.println(a);
System.out.println(a1);
}
}

输出:
class java.lang.Runtime
class java.lang.Runtime

在正常情况下,如果我们想要拿到一个类,得import才能调用,但是用forName就不需要

就比如java.lang.Runtime,我们就不能直接这样执行命令

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

因为Runtime类的构造方法是私有的。也就是说,这个类是单例模式,里面有静态的执行命令的方法。我们只能通过Runtime.getRuntime()来获取到Runtime对象。

工厂模式(Factory Pattern)
定义:

工厂模式用于封装对象的创建过程。当你不想直接用 new 去创建对象时,而是通过一个“工厂”方法来完成对象的创建,这就是工厂模式。

目的:

将创建对象的过程从使用者中分离出来,使得代码更易于维护、扩展和解耦。

单例模式(Singleton Pattern)
定义:

单例模式用于确保某一个类只有一个实例,并提供一个全局访问点来获取它。

目的:

控制实例的创建数量,通常用于管理共享资源,如数据库连接、配置管理器、线程池等。

特点:
  • 只有一个实例
  • 自行创建这个实例
  • 提供一个访问它的静态方法
比较点 工厂模式 单例模式
目的 负责对象的“创建逻辑” 保证类的“唯一实例”
应用场景 创建多个相似对象,解耦客户端与对象创建 创建一个全局唯一的对象
实例数量 可以创建多个不同实例 只会有一个实例
是否涉及继承 通常涉及继承和接口实现 不一定
1
2
3
4
5
6
public class Main {
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),"calc.exe");
}
}

image-20250410191316824

如果一个类没有无参构造方法,也没有单例的静态方法,我们就需要getConstructor这个方法,拿到构造函数后使用newInstance来执行,这里用ProcessBuilder来执行命令进行演示。

ProcessBuilder 是 Java 中一个非常实用的类,用于创建并控制外部进程(系统命令)。也就是说,它可以让你在 Java 程序里调用像 pinglscmdpython 等系统命令或者外部程序。

ProcessBuilder 是 Java 提供的一个类,位于 java.lang 包下,用来启动和管理新的操作系统进程。

1
ProcessBuilder pb = new ProcessBuilder("命令", "参数1", "参数2", ...);
方法 说明
start() 启动新进程
command() 设置或获取命令及其参数
directory(File dir) 设置工作目录
redirectOutput() 将输出重定向到文件或父进程
redirectErrorStream(true) 将错误输出合并到标准输出

ProcessBuilder有两个构造函数:

public ProcessBuilder(List<String> command)

public ProcessBuilder(String... command)

利用反射执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.ProcessBuilder");
clazz.getMethod("start") .invoke(clazz.getConstructor(String[].class).newInstance(new String[][]{{"calc.exe"}}));
//或者下面这个
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
Object instance = clazz.getConstructor(String[].class).newInstance((Object) new String[]{"calc.exe"});
clazz.getMethod("start").invoke(instance);
//或者使用List来触发,也是P牛用的那个
Class<?> clazz = Class.forName("java.lang.ProcessBuilder");
// 2. 构造函数参数是 List<String>,所以要用 Arrays.asList("calc.exe")
java.util.List<String> command = java.util.Arrays.asList("calc.exe");
// 3. 通过构造方法创建对象
Object pb = clazz.getConstructor(java.util.List.class).newInstance(command);
// 4. 调用 start 方法
clazz.getMethod("start").invoke(pb);
}
}

image-20250410210912002

image-20250410211745917

image-20250410212027165

那如果是一个方法或者构造方法是私有方法,使用getDeclared便可以解决。

getDeclared 系列的反射了,与普通的 getMethod 、 getConstructor 区别是:

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

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

方法 能访问谁? 是否能访问私有成员? 是否包含继承来的?
getMethod(String name, Class... parameterTypes) 只能访问 public 方法(包括继承来的) ❌ 否 ✅ 是
getDeclaredMethod(String name, Class... parameterTypes) 访问当前类中声明的所有方法(不管访问修饰符) ✅ 是 ❌ 否
方法 能访问谁? 是否能访问私有构造器? 是否包含父类的构造器?
getConstructor(...) 只能访问 public 构造方法 ❌ 否 ✅ (但构造器本身不继承)
getDeclaredConstructor(...) 可访问当前类声明的任意构造方法(包括 private) ✅ 是 ❌ 否

AI给了个顺口溜,哈哈哈,挺有意思

getXxx():只管 public,啥都要“见得光”。

getDeclaredXxx():不管三七二十一,自己类里的全都能看。

想用 private?你还得加 setAccessible(true)

我们拿Runtime来举个例子,直接用getDeclaredConstructor来获取私有的构造方法

1
2
3
4
5
6
7
8
9
10
import java.lang.reflect.Constructor;

public class Main {
public static void main(String[] args) throws Exception {
Class clazz = Class.forName("java.lang.Runtime");
Constructor a = clazz.getDeclaredConstructor();
a.setAccessible(true); //获取到一个私有方法后,必须用setAccessible 修改它的作用域,否则仍然不能调用。
clazz.getMethod("exec",String.class).invoke(a.newInstance(),"calc.exe");
}
}

image-20250410214045141

总结一个

目的 方法
获取 public 方法/构造器 getMethod() / getConstructor()
获取 private 或 protected getDeclaredMethod() / getDeclaredConstructor() + setAccessible(true)
调用静态方法 invoke(null, ...)
调用实例方法 invoke(对象, ...)

参考

Java安全第一篇 | 反射看这一篇就够了

java安全漫谈-01-03

告别脚本小子系列丨JAVA安全(3)——JAVA反射机制


java安全基础_反射篇
https://eznp.github.io/2025/04/14/java安全基础-反射篇/
作者
Zer0
发布于
2025年4月14日
许可协议