什么是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)