0x00 前言
懒狗很久都没有动手写过文章了,实在是懒。这次懒狗先是玩了几小时的饥荒,然后挤牙膏似的找了几篇关于Java中的RMI的文章,并且粗略的理解了一下,所以我也不敢保证自己下面写的都是正确的😋。
本篇文章包含以下元素:
- 什么是RMI
- RMI的运用
第二天,阿里云的饥荒服务器就嗝屁了,麻了,一天没玩到游戏,更加颓废了。
结果这篇文章,现在才差不多算写完,而且还留下了疑问 —2021/08/01
0x01 什么是RMI
释意
RMI是Remote Method Invocation的缩写,翻译过来的意思就是远程方法调用。在有些文章里会将他与RPC(Remote Procedure Call 远程过程调用)进行对比,这里因为我是个懒狗和菜狗,都还搞不懂什么是RPC,所以这里我就不进行对比了。
作用
远程方法调用,即可以让一个JVM中运行的程序调用另一个JVM中程序的方法,并且这两个JVM可以是分别属于两台不同的主机(也可以相同主机)。
组成
一个RMI往往包含以下三部分:
- 注册中心 Register
- 服务端 Server
- 客户端 Client
通信原理
RMI为了让客户端和服务端更加专注的处理自己的事物,而不用去操心客户端和服务端之间的通信问题,采用了类似代理的模式,并且在客户端和服务端各自有一个代理,客户端的叫Stub(存根) ,服务端的叫Skeleton(骨架),据此,我们给出如下的通信方式:
客户端和服务端各自通过调用自己的代理来与对方通信,并且代理最后也会将取得的结果返回给端,在这里我省略了代理间通信的细节(感兴趣可以自己看参考文章了解细节)。
在JDK 1.2版本(1998)之后,骨架skeleton不再被需要, 由Java的UnicastServerRef#dispatch替代;在JDK 5 (大家常说的1.5)之后,不再需要手动利用rmic命令生成静态Stub,而是会由Java自动地动态生成。这个动态生成也是我们后面JNDI注入的关键
三部分之间的联系
以上我们给出了RMI底层的通信模型,然而客户端是如何获取到自己的Stub的呢?
RMI中采用注册表的方式,服务端会将远程对象注册,然后在注册表中留下相关信息,当客户端需要使用远程代理对象(Stub)时,将在注册表中查找。也就是说注册中心 Register实际上就是用来给服务器提供Stub的。
将这一部分与上面的通信原理图相结合,我们可以得到下面的原理图:
数据传输格式
服务端和客户端的通信,包括服务端、客户端与注册中心的通信都是使用的序列化内容,然后在各自这里进行反序列化。我们之所以能够利用RMI来攻击,就是基于此。
0x02 一个简单的RMI
远程对象接口
要实现远程对象的调用,显然,客户端和服务端需要有一个同样的远程对象接口。这样子,虽然客户端并没有远程对象的具体实现,但是我们依然可以先声明一个相同的类。(😋,听不懂就算了,多看几遍源码就懂了。)
下面是一个远程对象接口的例子:
package edu.myRMI.share;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteInterface extends Remote {
String sayHello(String name) throws RemoteException;
}
Java中规定远程对象接口必须继承Remote类,当客户端和服务端不在同一个包时,类必须使用public来声明,且远程接口中的方法必须抛出RemoteException的错误。
远程对象接口实现
远程接口的实现可以显式的继承UnicastRemoteObject类,这便于之后直接注册为远程对象(待会细🔒),这里先给出这样写出来的代码:
package edu.myRMI.server;
import edu.myRMI.share.RemoteInterface;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteImpl extends UnicastRemoteObject implements RemoteInterface{
protected RemoteImpl() throws RemoteException {
//看网上的博客说,因为父类的构造器有抛出RemoteException,所以这里也必须写
}
@Override
public String sayHello(String name) throws RemoteException {
return "hi"+ name +"chenlvtang_is_Fool";
}
}
之所以上面说显式的继承可以便于直接注册为远程对象,在于UnicastRemoteObject类的构造方法:
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}
protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}
它的第一个无参构造方法中的this(0)其实就是调用了下面的有参构造方法,并传参为0。而exportObject方法,我们只需要知道,它会帮我们将类导出为远程对象服务(Exports the remote object to make it available to receive incoming calls, using the particular supplied port.),能够接收调用(很浅显的说法,如果有兴趣,可以自己细究,其实它有好几种功能, 并且也有函数多态)。所以当我们直接继承UnicastRemoteObject类时,待会实例化的时候,就会自动帮我调用exportObject方法,从而更方便的导出为远程对象服务。
因为Java不支持多继承,所以有时候我们想让远程对象继承别的类的时候,就不得不放弃直接继承UnicastRemoteObject,这时候我们可以在之后手动的调用exportObject方法,具体写法将在下面服务端给出。
上面的UnicastRemoteObject继承了RemoteServer,而RemoteServer继承了RemoteObject,其实现了接口Remote, java.io.Serializable,这也符合上面我们所说的客户端、服务端与注册中心间是通过序列化进行通信;然而这里我有个疑问,当我们的远程对象类没有显式的继承UnicastRemoteObject时,为什么还可以用于通信呢?或许是我目前对这三者通信的理解有什么盲点,又或者可能是我对Java中的序列化了解的不够完善。所以我这里就先写下疑问,等以后再来探讨。– 2021/07/31
关于这个问题,答案其实是:Java并不是把这个远程对象类序列化后发送过去给客户端调用,而是在收到调用请求后,先调用方法,然后把方法的返回值进行序列化。所以,只要我们方法中的返回值是“基本类型”或者是能够进行序列化的类即可,这里我们的返回值就是基本类型中的String – 2021/08/07
服务端
在写服务端代码时,我们可以选择将启动注册中心的代码写入其中:
package edu.myRMI.server;
import edu.myRMI.share.RemoteInterface;
import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class myServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
//实例化一个远程对象,注意这里的RemoteImpl是有继承UnicastRemoteObject的
RemoteInterface myRemote = new RemoteImpl();
//将注册中心创建在1099端口,可以再加一个参数IP
Registry myRegistry = LocateRegistry.createRegistry(1099);
//向注册中心中注册Stub,第一个参数为名字,要求客户端服务端相同
myRegistry.rebind("myRMI", myRemote);
}
}
上面我们说到过,远程对象接口的实现类如果有直接继承UnicastRemoteObject,则在实例化的时候,就会自动调用exportObject,导出为远程对象服务。如果没有继承,那我们可以使用下面的代码手动导出:
RemoteInterface myRemote = new RemoteImpl();
UnicastRemoteObject.exportObject(myRemote, 0);
这里也可以不用代码启动注册中心,可以选择使用命令启动:
rmiregistry 5566 //可以指定端口创建,未指定端口时,默认在1099端口创建
或者让其在后台运行start rmiregistry
,这将单独创建一个运行窗口
客户端
package edu.myRMI.client;
import edu.myRMI.share.RemoteInterface;
import java.net.MalformedURLException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class myClient {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
//连接到注册中心,这里因为是本地创建的,所以IP写的是localhost
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
//像注册中心查询服务,这里也就体现了为什么要和服务端共用一个接口
RemoteInterface myRemote = (RemoteInterface) myRegistry.lookup("myRMI");
String result = myRemote.sayHello("chenlvtang");
System.out.println(result);
}
}
启动
分别启动客户端和服务端,查看调用结果
0x03 参考文章
java RMI原理详解 - yehx - 博客园 (cnblogs.com)
深入理解JNDI注入与Java反序列化漏洞利用 – KINGX
Java 安全-RMI-学习总结 - 知乎 (zhihu.com)
JAVA RMI 反序列化知识详解 (seebug.org)