Java-反射及原生序列化和反序列化
Java-反射及原生序列化和反序列化
Java 反射
核心就是
1 | 用字符串决定 |
比如说
1 | public void execute(String Dog, String bark) throws Exception { |
几个在反射里里极为重要的方法:
获取类的⽅方法: forName
实例例化类对象的方法: newInstance
获取函数的方法: getMethod
执行函数的方法: invoke
基本上,这几个方法包揽了了Java安全里各种和反射有关的Payload。
另外newInstance() 在 Java 9 之后已经不推荐使用。
1 | a.getDeclaredConstructor().newInstance(); |
1 | Class a = Class.forName("Dog"); |
如果攻击者能控制 Java 代码执行:
1 | Runtime.getRuntime().exec("命令") |
就可以进行命令执行了
除了 Runtime,还有一个类ProcessBuilder
1 | ProcessBuilder pb = new ProcessBuilder("calc"); |
常见的还是runtime
获取类的其他方式
obj.getClass()
前提是已经有一个对象
1 | Dog d = new Dog(); |
之后就可以
1 | Class c = d.getClass(); |
1 | d.getClass() = Dog.class |
类名.class
前提是已经知道这个类了
1 | Class c = Dog.class; |
直接获取 Dog
类的 Class 对象
不过这种方法不属于反射 因为你编译的时候就已经知道类了
Class.forName()
只有字符串类名
1 | Class c = Class.forName("Dog"); |
这是真正的反射常用方法
类名是动态的
函数重载
Java 的 Class.forName 有两个函数重载(就是同名函数但参数不一样):
1 | Class<?> forName(String name) |
- 第一个:我们最常用,直接写类名就行。
- 第二个:更底层、更灵活,可以控制 是否初始化 和 用哪个 ClassLoader 来加载。
为什么第一个可以理解为第二个的封装
第一个内部大概
1 | Class.forName(className, true, 当前ClassLoader) |
className→ 你想要的类名字,比如“java.lang.Runtime”
true → 表示 要初始化类
当前ClassLoader → JVM 默认的类加载器
参数解释
第一个是类名
第二个参数决定了类是否被初始化
初始化指的是:
- 静态变量赋值
- 静态代码块执行
eg
1 | class Test { |
1 | Class.forName("Test", true, loader); // 输出: Test类初始化了! |
其实是加载类后要不要初始化类而不是要不要加载类
第三个参数是一个加载器
告诉 JVM 去哪里找这个类
Java 默认的 ClassLoader 会去 classpath(Java类路径)找
高级用法可以传自己写的 ClassLoader,比如从网络加载类、从 jar 文件动态加载等等
安全研究里,很多漏洞就是利用 自定义 ClassLoader + forName 动态加载一些隐藏类
1 | public class TrainPrint { |
这三个不同的初始化
| 类型 | 写法 | 什么时候执行 |
|---|---|---|
| 静态代码块 | static {} |
类初始化时执行 |
| 普通代码块 | {} |
每次创建对象时执行 |
| 构造函数 | public TrainPrint() |
创建对象时执行 |
首先调⽤用的是static {},其次是{},最后是构造函数。
如果我们是正常的调用一个类 那么
1 | import java.lang.Runtime; |
然后才能写:
1 | Runtime r = Runtime.getRuntime(); |
反射不需要 import
只要你知道 类的完整名字:包名+类名 就可以去加载
为什么源码里会出现 $
很多 Java 类名里会看到 $
a$b大致意思就是b是在a里面class的
因此可以这样加载:
1 | Class.forName("C1$C2"); |
$ 在安全研究里的意义
有些框架会过滤.
那么这时候就可以用$来进行绕过
所以像 Fastjson 在安全检查时会:
把 $ 替换成 .
避免绕过。
newInstance()函数
1 | clazz.newInstance(); |
用来调用类的无参构造函数 等价于
1 | new ClassName(); |
比如说
1 | class Test { |
就可以
1 | Class.forName("Test").newInstance(); |
有时候在写漏洞利用方法的时候,我们会发现使用 newInstance 总是不成功,这时候原因可能是:
你使用的类没有无参构造函数
1 | class Test { |
Test类定义了一个带参数的构造方法 public Test(int a) {},但没有显式定义无参构造方法 `Test()
当你定义了带参构造方法后,Java 编译器不会再自动生成默认的无参构造方法。而 newInstance() 方法在创建对象时,会强制调用类的无参构造方法。此时因为找不到这个方法,就会抛出异常,导致实例化失败
反射通过 newInstance() 创建对象时,严格要求类必须有 public 的无参构造方法;否则会因 “找不到可用的构造方法” 而失败。
你使用的类构造函数是私有的
这个比较好理解
1 | class Test { |
为什么 Runtime 不能 newInstance()
在java的源码里 runtimed的构造方法是private
所以说只有在这个类的内部才能使用这个方法
尝试反射调用时候没有办法进入类内部
当你用反射尝试创建实例时:
1 | Class clazz = Class.forName("java.lang.Runtime"); |
newInstance()会尝试调用类的无参构造方法,但Runtime的无参构造是private的。- 反射默认无法访问
private构造,所以会抛出异常。
官方设计是这样获取的
1 | Runtime.getRuntime() |
1 | Runtime r = Runtime.getRuntime(); |
如果用反射 那么就是
1 | Class clazz = Class.forName("java.lang.Runtime"); |
简单来说就是
必须:
1 | Runtime.getRuntime() |
不能:
1 | new Runtime() |
然后来看这个
1 | Class clazz = Class.forName("java.lang.Runtime"); |
其实这个代码最后执行的就是
1 | Runtime.getRuntime().exec("calc.exe"); |
对于getmethod

漫谈里这段介绍比较容易懂
对于invoke()
作用就是去执行方法
1 | // 1. 获取 Runtime 类的 Class 对象(反射的“入口”) |
没有无参构造函数如何实例化类
newInstance() 只能调用无参构造方法
如果类 没有无参构造方法就会失败
那么我们就可以使用getconstructor来获取构造方法
1 | Class clazz = Class.forName("java.lang.ProcessBuilder"); |
这里表示获取:
1 | ProcessBuilder(List<String>) |
newInstance 调用构造方法
获取构造方法后:
1 | Constructor.newInstance() |
就等价于:
1 | new ProcessBuilder(...) |
代码:
1 | clazz.getConstructor(List.class) |
等价:
1 | new ProcessBuilder(Arrays.asList("calc.exe")) |
RMI
RMI是 Java 原生的远程方法调用机制,核心目标是:让运行在 JVM A 上的对象,像调用本地方法一样,调用运行在 JVM B 上的对象的方法。
RMI的通信分为三个阶段

RMI 的所有数据传输(方法名、参数、返回值)都依赖 Java 序列化:
- 客户端:把要调用的方法名、参数序列化成字节流,通过网络发给服务端。
- 服务端:反序列化字节流,执行方法,再把结果序列化返回。
RMIServer.java(服务端)
1 | package org.vulhub.RMI; |
UnicastRemoteObject 是 RMI 的核心类,作用:
自动生成 Stub(客户端代理)和 Skeleton(服务端代理);
处理 TCP 网络通信(默认随机端口);
实现对象的远程导出(让对象能被远程访问)
| 远程接口 | IRemoteHelloWorld extends Remote |
定义客户端能调用的远程方法,是「客户端和服务端的协议」 | 必须继承java.rmi.Remote,所有方法抛RemoteException(网络异常) |
|---|---|---|---|
| 接口实现类 | RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld |
实现远程方法的真实业务逻辑,运行在服务端 | 必须继承UnicastRemoteObject(自动生成通信代理),构造方法抛RemoteException |
| 主启动类 | start()方法 +main() |
启动 Registry、创建实现类实例、绑定对象到 Registry | 用LocateRegistry.createRegistry(1099)启动注册中心,Naming.rebind()完成绑定 |
TrainMain.java(客户端)
1 | package org.vulhub.Train; |





