什么是JAVA RMI
RMI ( Remote Method Invocation , 远程方法调用 ) 能够让在某个 Java虚拟机 上的对象像调用本地对象一样调用另一个Java虚拟机 中的对象上的方法 , 这两个 Java虚拟机 可以是运行在同一台计算机上的不同进程, 也可以是运行在网络中不同的计算机上
为什么要使用RMI?
- 一般我们想要在本地调用一个 - object的方法的时候,会采用- object.method()的方式来调用,但如果提供- object的并不是本地的- JAVA虚拟机,而是远程的- JAVA虚拟机呢?我们本地并没有这个对象,那么我们就需要用到RMI。
- 当我们服务器拥有一系列服务,想要把它们提供给客户端,但问题是,服务器只想提供它想要提供给客户端的方法,它不可能把自己所有的对象和方法都发送给客户端,让客户端去调用一个对象的所有方法,这是极其不安全的。 
- 假设客户端(JVMA)想要获取服务端(JVMB)上的某个对象 - ObjectA,但下次程序修改后,实例对象的名称可能会发生改变,这时候客户端(JVMA)并不知道实例对象的名称是什么,也就无法获取到了,又失去了与服务端(JVMB)的联系。
- 假设客户端找到了想要的资源,但数据传输又成了问题。这时候获取到的是一个完整的对象,在远程调用的时候,如果先把对象分解成基本类型的数据传输给客户端后,再把这些基本类型的数据拼接成对象,这样会大大增加代码复杂度。 
- 另外,如果程序需要频繁的远程调用,开发人员不可能为每一次调用都设计一套调用方法,所以一个统一而规范的接口十分重要。 
RMI工作流程

上面这个流程图说的很清楚。
RMI有三个角色参与,分别是Client、Registry、Server。Server向Client提供服务,但它不愿意将所有的对象、方法都交给Client去调用,所以它需要一个“中间人” - Registry,Server把它想要提供的服务告诉Registry,让Registry和Client去交流(获取、发送请求),所以它相当于是服务器的代理,负责帮Server办事。
那么Registry怎么向Client提供服务呢?Registry上会绑定需要提供给客户端的服务,绑定的时候,会生成对应的stub(存根),所以Registry里有各种服务的stub。这时候Client如果想要获取服务器上的某个服务,它可以直接在Registry中查找,如果找到了对应的stub,就返回这个stub的拷贝,这样他就可以直接把stub当作一个object,然后调用里面的方法了。所以,stub相当于远程对象在客户端的代理。
Registry除了会生成stub,还会生成skeleton(相当于server的代理),Skeleton用于处理stub发过来的请求,然后去调用服务端的方法,再返回给stub。但在jdk1.2以后,反射API替代了skeleton的作用,它可以直接把请求发给服务端,所以就不再用skeleton了。
客户端调用stub的方法后,stub会将客户端想要调用的方法名及其参数序列化,利用远程引用层和传输层(Socket通信)发给Server,Server那边的远程引用层接收到数据后发给Skeleton,反序列化后在Server执行被调用的方法,然后将方法的返回值或异常打包(序列化后)返回给客户端,客户端再以同样的方法反序列化解析返回数据。
这里需要注意的是,远程方法的调用最后都是在Server执行的,最后只是返回序列化后的结果而已。
案例讲解
定义远程接口
Server想要提供服务,那么就需要为这些提供的服务定义一个接口,让Client可以访问到它。该接口必须继承Remote,这样该接口才能成为一个远程对象,才可以被JAVA虚拟机所调用。
| 1 | import java.rmi.Remote; | 
定义Service Implementation Class
定义好了远程接口后,需要写出具体的方法内容即实现类。该类需要继承UnicastRemoteObject,而这个父类会生成stub和skeleton。
| 1 | import java.rmi.RemoteException; | 
Registry Class
写好了服务接口,那么我们就需要一个中间代理人 - Registry。
我们需要创建并启动RMIService,并把我们提供的实现类绑定在上面。
| 1 | import server.HelloService; | 
Client
直接使用Naming.lookup()可以找到我们想要的stub。
| 1 | import server.HelloService; | 
Client上的接口
即使我们在服务端上设计了一套接口HelloService,但是Client是不知道的,所以我们需要在Client端设定一模一样的接口,这样Client才知道哪些方法能够被他们所调用。
运行测试
先运行RMIRegister

可以看到,RMIService成功绑定。
再运行Client:

成功收到远程服务器上的方法返回值。
RMI的安全问题
根本原因
RMI在数据传输过程中,涉及到序列化和反序列化,这就有构造恶意对象的风险。
伪造LocateRegister攻击client
我们客户端获取stub的方式是通过**Naming.lookup()**来查找指定url
例如
| 1 | /*Client.java*/ | 
如果该lookup()方法中的rmi地址(也就是LocateRegister)是可控的,让Client获取到的service是我们构造的恶意service(攻击者搭建的RMI server和实现的恶意对象),我们就能执行恶意helloService中的hello()了
RMI的利用方法远远不止如此,更多相关内容可见Java反序列化漏洞之利用链分析集合(4)
