java安全-RMI调用逻辑
java安全-RMI调用逻辑
一、RMI
1.简单了解
**RMI(Remote Method Invocation,远程方法调用)**是 Java 提供的一种核心机制,它允许运行在一个 Java 虚拟机(JVM)上的对象,调用运行在另一个 JVM 上的对象的方法,就像调用本地对象一样
简单来说,它的目的是让分布式 Java 应用之间的通信变得对开发者透明
RMI 的核心运行机制
RMI 的底层通信依赖于网络(通常是 TCP/IP),并且重度依赖 **Java 序列化(Serialization)**机制来在网络中传输对象。它的标准工作流程如下:
- RMI Registry(注册中心): 服务端启动时,会将自己提供的远程对象注册到一个叫做 RMI Registry 的地方,并绑定一个名字(类似电话簿)
- Stub(客户端存根): 客户端通过名字去注册中心查找,拿到一个 Stub 对象。这个 Stub 就像是服务端对象在客户端的“代理”
- 网络传输: 当客户端调用 Stub 的方法时,Stub 会把调用的方法名、参数等信息进行序列化,通过网络发送给服务端
- Skeleton(服务端骨架): 服务端接收到网络请求后,Skeleton(在较新的 Java 版本中已弱化为内部机制)会反序列化这些数据,找到真正的服务端对象并执行方法
- 返回结果: 执行完毕后,结果再次被序列化,原路返回给客户端的 Stub,最后解包交给客户端代码
2.创建demo
服务端
chuan/rmi/RMIServer.java
1 | public class RMIServer { |
chuan/rmi/RemoteObj.java
1 | public interface RemoteObj extends Remote { |
chuan/rmi/RemoteObjImpl.java
1 | public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj { |
客户端
chuan/rmi/RemoteObj.java
1 | public interface RemoteObj extends Remote { |
chuan/rmi/RMIClient.java
1 | public class RMIClient { |
3.通信原理
第一次 TCP 连接:客户端与 Registry 通信
- 客户端发起连接: 客户端连接到
RMI Registry(默认端口 1099) - 发送查询: 客户端发送
Call消息(调用lookup方法),寻找 Name 为hello的绑定关系 - 返回存根: Registry 找到对应的绑定关系,将一个包含 RMI Server 地址和随机端口(例如你提到的
172.17.88.209:24429)的 Stub(存根)对象序列化后,通过ReturnData消息发回给客户端
第二次 TCP 连接:客户端与 RMI Server 通信
- 客户端解析存根: 客户端反序列化拿到的 Stub 对象,提取出真正的 RMI Server 的 IP 和端口(
172.17.88.209:24429) - 客户端发起新连接: 客户端根据提取出的地址,主动与 RMI Server 建立第二次 TCP 连接
- 发送调用请求: 在这个新连接中,客户端向服务端发送
Call消息,告诉服务端:“我要执行sayHello()方法,这是我的参数” - 服务端执行并返回: 服务端接收到
Call消息,反序列化参数,真正执行本地的sayHello()方法,最后将结果序列化,通过ReturnData返回给客户端

二、rmi服务端
1.创建远程服务对象
src/chuan/rmi/RMIServer.java
1 | RemoteObj remoteObj = new RemoteObjImpl(); |
接着调用RemoteObjImpl构造函数
src/chuan/rmi/RemoteObjImpl.java
1 | public RemoteObjImpl() throws RemoteException { |
由于其继承UnicastRemoteObject类,所以接着调用父类无参构造器
java/rmi/server/UnicastRemoteObject.java
1 | protected UnicastRemoteObject() throws RemoteException |
1.1、一层exportObject
调用无参构造器默认port参数为0,在之后这个值为任意数,也就是默认发布到一个随机的端口
接着调用exportObject方法
java/rmi/server/UnicastRemoteObject.java
1 | public static Remote exportObject(Remote obj, int port) //这里的传来的obj,指的是当前实例化的对象chuan/rmi/RemoteObjImpl.java |
先创建了UnicastServerRef类,构造器如下
sun/rmi/server/UnicastServerRef.java
1 | public UnicastServerRef(int port) { |
依旧创建一个类LiveRef
1 | public LiveRef(int port) { |
链式创建出LiveRef类具体如下图
接着赋值回UnicastServerRef类的ref属性,最终就是返回一个属性ref为上面创建出来的LiveRef类的UnicastServerRef类

1.2、二层exportObject
接着之后return调用本类的同名方法
java/rmi/server/UnicastRemoteObject.java
1 | private static Remote exportObject(Remote obj, UnicastServerRef sref) |
((UnicastRemoteObject) obj).ref = sref;这里将我们要实例化的RemoteObjImpl转为父类UnicastRemoteObject
这里父类又有很多继承关系
1 | public class UnicastRemoteObject extends RemoteServer { |
RemoteObject中存在ref属性,本质上就是将RemoteObjImpl中ref赋值为我们之前的LiveRef
1.3、三层exportObject
接着调用sref.exportObject,这里的sref便是我们之前提到的UnicastServerRef,此时他的ref属性为LiveRef
1 | public Remote exportObject(Remote impl, Object data, |
这里的主要逻辑就是创建了stub也就是客户端的代理对象,具体创建过程如下
1 | stub = Util.createProxy(implClass, getClientRef(), forceStubUse); |
implClass这个是RemoteObjImpl类,getClientRef()获取的就是一个包装ref的对象,forceStubUse为false
1 | protected RemoteRef getClientRef() { |
应该相同,指的是同一个类,我这里只不过是重新启动了调试
之后具体处理流程如图
**方式1:**判断条件为stubClassExists(remoteClass)为真时成立,具体逻辑如下
1 | private static boolean stubClassExists(Class<?> remoteClass) { |
判断本机是否存在类名+_Stub后缀文件,存在即真
**方式2:**反射创建代理对象,handler构造还用LiveRef
1 | final InvocationHandler handler = |
clientRef的ref属性为LiveRef
stub创建完成之后
第一步之后用到,不考虑
第二步便是构造一个总的Target对象,impl为RemoteObjImpl,this为UnicastServerRef,stub为代理对象,ref.getObjID(), permanent为之前定好的
并将其存入发布到LiveRef,返回代理对象
这里创建stub
1 | 1. 先在服务端创建一个 Stub |
1.4、LiveRef#exportObject
这里的返回又有说法
1 | ref.exportObject(target); |
ref依旧是LiveRef
1 | sun/rmi/transport/LiveRef.java |
listen方法详解
同时,这里的newServerSocket方法会处理端口问题
super.exportObject(target);详解
1 | super.exportObject(target); |
sun/rmi/transport/Transport.java
1 | public void exportObject(Target target) throws RemoteException { |
sun/rmi/transport/ObjectTable.java
1 | static void putTarget(Target target) throws ExportException { |
意思为将我们每次创建的target放入俩个静态的表objTable,implTable,以方便服务端获取识别
由此创建完毕,将信息返回即可
2.创建注册中心
src/chuan/rmi/RMIServer.java
1 | Registry registry = LocateRegistry.createRegistry(1099); |
java/rmi/registry/LocateRegistry.java
1 | public static Registry createRegistry(int port) throws RemoteException { |
sun/rmi/registry/RegistryImpl.java
1 | public RegistryImpl(int port) |
这里和创建远程服务时逻辑相同,不过这里先利用1099默认端口创建了LiveRef,将其作为参数传入UnicastServerRef,并且将其属性ref设置为LiveRef
接着将创建出来的UnicastServerRef作为参数传入RegistryImpl#setup方法
sun/rmi/registry/RegistryImpl.java
1 | private void setup(UnicastServerRef uref) |
这里就是调用了 UnicastServerRef 的 exportObject 方法,本质上就是创建远程对象时的第二层 exportObject 方法
不同的是,传入的permanent参数这回为true

接着进入创建远程服务对象的第三层exportObject逻辑
2.1、创建stub
创建stub并返回
注册中心与远程服务端对象主要区别就是在创建stub的过程
stubClassExists具体逻辑如下
本地是有这个静态文件的,所以判断为true,也就走到了sun/rmi/server/Util.java#createStub

sun/rmi/server/Util.java
1 | private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref) |
这里代理stub本质上也就是创建了一个参数ref为我们的关键对象LiveRef的类
2.2、创建Skel
接着
sun/rmi/server/UnicastServerRef#setSkeleton
1 | public void setSkeleton(Remote impl) throws RemoteException { |
这一步,将sun/rmi/server/UnicastServerRef.java中skel字段创建并且赋值
此时
1 | UnicastServerRef |
2.3、创建target
与服务端不同的是,此时impl中存在属性skel

2.4、加入静态表
sun/rmi/server/UnicastServerRef#exportObject
1 | ref.exportObject(target); |
sun/rmi/transport/LiveRef#exportObject
1 | public void exportObject(Target target) throws RemoteException { |
sun/rmi/transport/tcp/TCPEndpoint.java
1 | public void exportObject(Target target) throws RemoteException { |
sun/rmi/transport/tcp/TCPTransport.java
1 | public void exportObject(Target target) throws RemoteException { |
sun/rmi/transport/Transport.java
1 | public void exportObject(Target target) throws RemoteException { |

可以在最终的objTable静态列表完成终极对比
3.注册中心绑定
chuan/rmi/RMIServer.java
1 | registry.bind("remoteObj", remoteObj); |
sun/rmi/registry/RegistryImpl.java
1 | public void bind(String name, Remote obj) |
checkAccess("Registry.bind");
必要安全检查
1 | Remote curr = bindings.get(name); |
检查是否绑定过此远程对象
1 | bindings.put(name, obj); |
本质上就是向静态hash表写入我们的代理对象

三、rmi客户端
1.请求注册中心-客户端
1.1、获取RegistryImpl_Stub
1 | Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); |
调用
java/rmi/registry/LocateRegistry.java
1 | public static Registry getRegistry(String host, int port) |
调用
java/rmi/registry/LocateRegistry.java
1 | public static Registry getRegistry(String host, int port, |
最后通过Util.createProxy这个方法创建了一个和注册中心创建的一样的stub
1.2、获取远程对象
1 | RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); |
调用
sun\rmi\registry\RegistryImpl_Stub.class中lookup方法
一共有三个注意点
第一个点是将字符串序列化传入
第三个点是指,反序列化获取读取到的的动态代理对象
第二个方法调用
sun/rmi/server/UnicastRef.java
1 | public void invoke(RemoteCall call) throws Exception { |
接着调用
sun/rmi/transport/StreamRemoteCall.java
1 | public void executeCall() throws Exception { |
这一块的代码是主要的通信加载方法,同时在代码下方逻辑处依旧会有问题
而且这个点更为隐蔽,甚至更加危险通用,只要是存在 invoke 方法,就有可能调用这里
方法最终获取到stub代理对象

2.请求服务端-客户端
1 | remoteObj.sayHello("hello"); |
remoteObj是一个动态代理类,所以会走到invoke方法
java/rmi/server/RemoteObjectInvocationHandler.java
1 | public Object invoke(Object proxy, Method method, Object[] args) |
接着调用invokeRemoteMethod
进一步调用invoke,此时ref为sun/rmi/server/UnicastRef.java
这个invoke中逻辑较多


这里通过判断,如果有返回值的话,会将返回值反序列化

这里这个 executeCall 方法,就是处理通常提到的 JRMP 协议

3.请求注册中心-注册中心
接第一层,创建注册中心时调用
1 | ref.exportObject(target); |
这里最终会调用到sun/rmi/transport/tcp/TCPTransport.java的listen方法
listen方法会开启socket监听,并且创建一个新的线程
进一步查看线程处理情况
1 | AcceptLoop(ServerSocket serverSocket) { |
启动线程时,是启动线程的run方法,这里继续深入
sun/rmi/transport/tcp/TCPTransport#executeAcceptLoop

sun/rmi/transport/tcp/TCPTransport$ConnectionHandler#run
1 | public void run() { |
接着调用sun/rmi/transport/tcp/TCPTransport$ConnectionHandler#run0

此方法接着调用handleMessages

3.1、具体的serviceCall方法
默认的TransportConstants会调用serviceCall
接着sun/rmi/server/UnicastServerRef#dispatch
因为是注册中心,当然对应的sekl不为空,调用oldDispatch方法
D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\registry\RegistryImpl_Skel.class
这里主要用到了 case2 就是将传入的参数反序列化,因此这里便是一个反序列化漏洞点
4.请求服务端-服务端
前方网络依旧相同,这里到了serviceCall出现分歧
这里获取的target是我们的代理类
进一步的,后面调用的dispatch方法,也会产生分歧




5.dgc垃圾回收机制
5.1、创建
服务端创建出target对象,将其放入静态表之前时,会进行dgc target加载
就是在第四层listen方法结束之后,将target放入静态表前exportObject加载时处理
1 | if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) { |
这里是调用DGCImpl类的静态变量dgcLog,调用静态变量时会默认加载类的static代码块
这里静态代码块会利用 Util.createProxy 动态加载 sun.rmi.transport.DGCImpl_Stub 这个类

并将其DGCImpl中UnicastServerRef属性stub设置为其
最后在静态表中将最终的 target 对象放入
5.2、调用
客户端D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\transport\DGCImpl_Stub.class
方法dirty
方法clean
所有的客户端stub都会被JRMP攻击
服务端D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\sun\rmi\transport\DGCImpl_Skel.class
随着 JDK 安全机制的不断完善(特别是 8u121 把 useCodebaseOnly 默认设为 true,以及后续 8u191 对 JNDI trustURLCodebase 的严格限制),早期那种直接利用 RMI 组件互相投毒、依赖远程加载 codebase 的攻击手法,在现代环境下的门槛已经变得极高
现在 RMI 在安全攻防里的生态位,已经完全变成了**”最佳辅助”**