文章目录
介绍
RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
RMI的一般要用到的组件:
- Remote Interface:远程接口
需要定义一个接口,继承自 java.rmi.Remote
,表明可以被远程对象调用的方法。
远程调用可能发生网络异常 , 所以每个方法都必须显式抛出 RemoteException
- Remote Object Implementation:远程接口的具体实现
一般需要继承UnicastRemoteObject
类, 将对象导出成一个 可以通过 TCP 调用的远程对象
- Server:服务端,注册远程对象到 RMI 注册中心。
- Client:客户端,查找远程对象并调用其方法。
- Registry:注册端提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
实现
服务端 Server
定义远程接口
package RMI.Server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
public String sayHello(String name) throws RemoteException;
}
远程接口的实现
package RMI.Server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws RemoteException {
super(); //也可以什么都不写,隐式调用
//如果没有继承UnicastRemoteObject,就需要手动导出: UnicastRemoteObject.exportObject(this, 0);
}
@Override
public String sayHello(String name) throws RemoteException {
return "Hello " + name;
}
}
服务端
主要是创建 RMI 注册表(使用默认端口 1099),创建服务实现类的实例,将远程对象绑定到注册表中
package RMI.Server;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) {
try{
//实例化远程对象
HelloImpl obj = new HelloImpl();
//启动本地的RMI注册服务(一般默认 1099 端口),创建注册中心
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry();
//绑定远程对象
registry.bind("HelloImpl", obj);
//或者import java.rmi.Naming;
//Naming.bind("rmi://127.0.0.1:1099/HelloImpl", obj);
}catch (Exception e){
e.printStackTrace();
}
}
}
客户端 Client
连接到本地(localhost)的 RMI 注册表然后查找相应名字的远程对象,最后调用远程方法,传入相应参数
package RMI.Client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import RMI.Server.Hello; // 导入服务器端的远程接口
public class RMIClient {
public static void main(String[] args) throws Exception {
//连接到服务器
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
//通过名字查找远程对象
Hello hello = (Hello) registry.lookup("HelloImpl");
//调用远程对象上面的方法
String response = hello.sayHello("xpw");
System.out.println("response :"+response);
}
}
先运行服务端, 再运行客户端, 在客户端就可以看到调用了远程对象的方法了
通信过程
很多复制粘贴的来自其他师傅的博客,了解了一下内部通信的知识,还没有动手去尝试抓包
数据端与注册中心(1099 端口)建立通讯
- 客户端查询需要调用的函数的远程引用,注册中心返回远程引用和提供该服务的服务端 IP 与端口。
客户端与注册中心(1099 端口)建立通讯完成后,客户端 向注册中心发送了⼀个 “Call” 消息,注册中心回复了⼀个 “ReturnData” 消息,然后客户端新建了⼀个 TCP 连接,连到服务端的 33769 端⼝
AC ED 00 05
是常见的 Java 反序列化 16 进制特征
注意以上两个关键步骤都是使用序列化语句
客户端与服务端建立 TCP 通讯
客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用
同样使用序列化的传输形式
以上两个过程对应的代码是这两句
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj"); // 查找远程对象
这里会返回一个 Proxy 类型函数,这个 Proxy 类型函数会在我们后续的攻击中用到。
客户端序列化传输 调用函数的输入参数至服务端
- 这一步的同时:服务端返回序列化的执行结果至客户端
以上调用通讯过程对应的代码是这一句
remoteObj.sayHello("hello");
可以看出所有的数据流都是使用序列化传输的,那必然在客户端和服务带都存在反序列化的语句。
总结
整个过程进⾏了两次TCP握⼿,也就是我们实际建⽴了两次 TCP连接。
第⼀次建⽴TCP连接是连接远端 ip 的1099端⼝,这也是我们在代码⾥看到的端⼝,⼆ 者进⾏沟通后,我向远端发送了⼀个“Call”消息,远端回复了⼀个“ReturnData”消息,然后我新建了⼀ 个TCP连接,连到远端的33769端⼝。
之所以是33769端口, 因为在“ReturnData”这个包中,返回了⽬标的IP地址,其后跟的⼀个字节 \x00\x00\x83\xE9
,刚好就是整数 33769 的网络序列
所以捋一下整个的过程: 首先客户端连接Registry,并在其中寻找Name是HelloImpl的对象,这个对应数据流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=HelloImpl的对象,这个对应数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 127.0.0.1:33769 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程 ⽅法调⽤,也就是 HelloImpl()
各个元素之间的关系
RMI Registry就像⼀个⽹关,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。
参考文章
代码审计社区 Java安全漫谈
https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/#Java-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8B-RMI-%E4%B8%93%E9%A2%98-01-RMI-%E5%9F%BA%E7%A1%80
https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/