一. RMI介绍
RMI
(Remote Method Invocation) 远程方法调用,顾名思义,是一种调用远程位置的对象来执行方法的思想。
在 Java 中,我们通常传递一个完整的对象,这个对象既包含数据,也包含数据和操作数据的方法,Java 中如果想完整的在网络中向远程位置传输一个对象,我们通常使用的方法是 Java 原生反序列化,并且可以结合动态类加载和安全管理器来安全的传输一个 Java 类
为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的
而具体的实现思想就是让我们获取远程主机上对象的引用,我们调用这个引用对象,但实际方法的执行在远程位置上
举例:客户端要调用服务端的A对象的A方法,客户端会生成A对象的代理对象,代理对象里通过用Socket与服务端建立联系,然后将A方法以及调用A方法是要传入的参数序列化好通过socket传输给服务端,服务端接受反序列化接受到的数据,然后通过反射调用A对象的A方法并将参数传入,最终将执行结果返回给客户端,给人一种客户端在本地调用了服务端的A对象的A方法的错觉。
RMI结构
RMI分为三部分
- RMI Registry 注册中心,存放着远程对象的位置(ip、端口、标识符)
- RMI Server 服务端,提供远程的对象
- RMI Client 客户端,调用远程的对象
数据传输过程
这里使用Geekby大佬的一张图
整个过程中,Server到RMI Registry(步骤2)、Client与RMI Registry的双向通信(步骤3、4)和Client与Server的双向通信(步骤6、9),这三个过程数据传输都要经过序列化/反序列化
的操作
RMI通信流程:服务端首先需要创建远程对象并在 Registry进行注册,客户端客户端连接Registry,并在其中寻找Name是hello的对象。然后Registry返回一个序列化的数据(Hello对象的初始引用)。这个hello对象是由一个动态代理生成的类,包含与Server通信的IP和端口。Client与该地址进行连接,在该连接中才真正调用远程⽅法。
RMI使用
使用 RMI ,首先要定义一个我们期望能够远程调用的接口,这个接口必须扩展 java.rmi.Remote
接口,用来远程调用的对象作为这个接口的实例,也将实现这个接口,为这个接口生成的代理(Stub)也是如此。这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException
异常,例如:
package org.gk0d;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}
然后创建这个远程接口的实现类,这个类中是真正的执行逻辑代码,并且通常会扩展java.rmi.server.UnicastRemoteObject
类,扩展此类后,RMI 会自动将这个类 export
给远程想要调用它的 Client
端,同时还提供了一些基础的 equals/hashcode/toString
方法。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException
。
在 export 时,会随机绑定一个端口,监听客户端的请求,所以即使不注册,直接请求这个端口也可以通信,
如果不想让远程对象成为 UnicastRemoteObject
的子类,后面就需要主动的使用其静态方法 exportObject
来手动 export 对象
package org.gk0d;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}
现在可以被远程调用的对象被创建好了,接下来如何调用呢?Java RMI 设计了一个 Registry
的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个 RMI 电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。
这种电话本的思想,由 java.rmi.registry.Registry
和 java.rmi.Naming
来实现。这里分别来说说这两个东西。
java.rmi.Naming
,这是一个 final 类,提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,这个类提供的每个方法都有一个 URL 格式的参数,格式如下: //host:port/name
- host 表示注册表所在的主机
- port 表示注册表接受调用的端口号,默认为 1099
- name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字
Naming 提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)用来对注册表进行操作。也就是说,Naming 是一个用来对注册表进行操作的类。而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的。
java.rmi.registry.Registry
接口,这个接口在 RMI 下有两个实现类,分别是 RegistryImpl
以及 RegistryImpl_Stub
我们通常使用 LocateRegistry#createRegistry()
方法来创建注册中心
package org.gk0d;
import java.rmi.registry.LocateRegistry;
public class Registry {
public static void main(String args[]) {
try {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后将待调用的类进行绑定:
攻击面
客户端攻击服务端
服务端攻击客户端
客户端攻击注册中心
服务端攻击注册中心
注册中心攻击客户端(或服务端)
JEP290绕过
攻击方式后边再写
标签:调用,Java,利用,Registry,RMI,远程,服务端,客户端 From: https://www.cnblogs.com/gk0d/p/16839758.htmlhttps://myzxcg.com/2021/10/Java-RMI分析与利用/
https://paper.seebug.org/1194/#_8
https://halfblue.github.io/2021/11/02/RMI反序列化漏洞之三顾茅庐-攻击实现/#more