RMI 在反序列化里漏洞里面是很常用的,它是一个分布式的思想。
RMI概述
RMI 通常包含两个独立的程序,一个服务端 和 一个客户端。服务端通过绑定这个远程对象类,它可以封装网络操作。客户端层面上只需要传递一个名字,还有地址。
- 服务端绑定远程对象开了一个动态端口,然后告诉 注册中心,注册中心也有一个端口(默认端口是1099)。
- 客户端只查找这个名字,注册中心就会告诉他,开了哪个端口,然后回过来找服务端
- 最后调用服务端绑定的那个名字的接口实现类的某个方法
官方文档:
https://docs.oracle.com/javase/tutorial/rmi/overview.html
代码演示
需要两个主程序,分别为:客户端和服务端。
- 服务端程序需要实现类和接口
- 客户端程序只需要接口就好了
服务端
接口类:
package example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { //客户端有一个接口就行了
//客户端要调用的方法
public String sayHello(String keywords) throws RemoteException;
}
接口实现类:
package example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//继承远程对象 UnicastRemoteObject
public class RemoteObjlmpl extends UnicastRemoteObject implements IRemoteObj {
protected RemoteObjlmpl() throws RemoteException {
}
@Override//转大写的功能
public String sayHello(String keywords) throws RemoteException {
String upperCase = keywords.toUpperCase();
System.out.println(upperCase);
return upperCase;
}
}
RMIServer 服务端主程序类:
package example;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
RemoteObjlmpl remoteObjlmpl = new RemoteObjlmpl(); //new一个实现类
Registry registry = LocateRegistry.createRegistry(1099); //创建注册中心,它的默认端口为1099
registry.bind("remoteObj",remoteObjlmpl); //绑定这个实现类的名字为 remoteObj
}
}
客户端
接口类
package example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote { //客户端有一个接口就行了
//客户端要调用的方法
public String sayHello(String keywords) throws RemoteException;
}
客户端主程序类:
package example;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); //远程获取注册中心的一个连接
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");//去查找注册中心的这个名字
remoteObj.sayHello("hello"); //查到了之后,这个接口类型直接调用接口实现类的方法
}
}
运行
首先再 RMIServer主程序运行,可以看见程序开始监听等待连接
然后这个时候运行RMIClient 主程序
这个时候可以看见服务端,成功调用了实现类的方法
RMI流程
RMI主要有三部分:
- 服务端
- 注册中心
- 客户端
然后漏洞是产生在 两两通信之间的。
服务端创建远程服务
在这里进行断点调试
然后来到这里,这是一个静态赋值
继续按F7,我们来到了这里,远程对象实现类的构造方法
继续按 F7 就会来到父类的构造函数
然后接着按F7 我们来到了这里,这个部分会把远程对象发布到一个随机的端口上。可以看见如果我们如果传0,它会发布到一个随机的端口
然后 按F8来到这里,可以看见调用了这个 exportObject 方法,按F7看一下
这个方法的大概意思是,导出对象或者发布对象这个意思,很明显它就是一个核心的方法。因为 RemoteObjlmpl 类的构造方法,可以不用继承 UnicastRemoteObject ,也可以直接调用这个 exportObject 静态方法。
在这里会有处理网络请求的逻辑
按F7,看一下,点击进到这个方法里
可以看见它这里又创建了一个 LiveRef 类
按F7来到这里,点击这个方法名
按F7,点击 this,看一下构造方法
可以看见主要有三个参数
主要看一下 getLocalEndpoint 这个方法,可以看见这个 TCPEndpoint 的意思是处理网络请求大概意思
可以看见 TCPEndpoint的 构造方法,两个参数,一个 host 、一个怕port。也就是说只要给它一个 ip 一个端口它就可以处理后面的这些网络请求。
使用 LiveRef方法,放的主要是这些
这个时候返回 LiveRef 方法,看一下它的构造方法
可以看见有三个参数,真正有意义的是这个ip和端口 进行了封装
然后按F8来到这里,这里调用了父类的构造方法
按F7,可以看见它的父类构造方法,这里只是进行了一个赋值,并不是建立了一个新的,还是 liveRef
然后我们按 F8出来到这里,这里返回了一个方法
按F7继续调用就来到了这里,sref下有这个 LiveRef
远程对象还是用 sref 进行了赋值,然后下一步调用了 exportObject 这个方法
继续按 F7看一下这个方法,可以看见 stub,stub是客户端操作的一个代理。
- 服务端先创建好了,然后把它放到注册中心上,然后客户端去注册中心拿,拿到再去操作它
- 通过它stub操作另外一个代理,然后才能真正调用服务端的远程对象
按F8加F7来到了这里,然后点击。看一下这个方法是怎么创建的
implClass参数是一个远程对象的实现类,我们自定义的类
然后这个 clientRef是一个封装的 LiveRef
按F8来到这里,如果这个 stubClassExists 方法为真
可以看见 如果 远程对象实现类如果有这个 _Stub结尾的话,结果会返回为真
F8来到这里,这里是一个创建动态代理的流程
这个是调用处理器,我们进去看一下
进去 super方法
是一个 ref,ref里面还是封装着 LiveRef
继续按F8可以看见,就已经把动态代理创建好了
F8来到这里,可以发现是一个总封装,按F7,然后点击 Target
F8来到这里,重点是这个ID,它和 LiveRef的ID是一样的,总的来说就是把这些东西都放在里面了
一直按F8来到这里,可以看见 exportObject方法把 target 发布出去
按F7跟到调用的部分,来到了 TCPTransport类里
发布远程对象
Listen 实际上我们知道是监听的意思,监听远程对象的某个端口
这里按F7跟进去,然后按F8来到了这里。可以看见它创建了一个新的sockert,这是一个服务端的socket
这里开启了一个新的线程,然后等待客户端连接
继续按F8出去代码逻辑来到这里,可以看见一开始 liveRef的默认端口是0,实际上这里已经随机分配了一个端口了
查找调用 listen方法,然后找到这里,可以看见 如果 端口为0,它会随机给它一个值,然后返回服务端,实际上服务端已经把这个远程对象发布出去了,但是它把它发送在一个随机的端口上,所以客户端默认是不知道的。
服务端记录发布
最后服务端还需要记录一下,F8来到这里,然后按F7
来到这里,继续按F7
然后就来到了这里,这里是一个简单的赋值,不用管,继续按F8
然后发现ObjectTable调用了一个 putTarget的方法
按F7进去,然后一直按F8来到这里,可以看见有两个方法
这两个方法会把信息保存到这两个 table
后面一直按F8可以看见已经开始监听网络的线程了,等待客户端进行连接
服务端创建注册中心
创建 RegistryImpl 对象,可以看见创建注册中心的默认端口为1099
来到了注册中心的实现类
我们来到sref的调用方法
- uref 是来自 UnicastServerRef类的
- 也就是说调用 UnicastServerRef类的 exportObject方法
在 exportObject的方法可以看见参数 permanent的意思为永久,意思是我们创建注册中心这个对象为永久对象
创建 RegistryImpl_Stub 代理对象,在流程图可以知道它是用作于客户端的代理
进去 createStub 方法可以看见,类名的名字改变了,return 返回了加载的初始化 ref
创建 建 RegistryImpl_Skel 代理对象代理对象。Skeleton 在流程图可以知道它是作于服务端的代理
跟踪方法
然后f8来到这里,可以发现static中的数据的 objTarget的第二个Target对象的Value的值有一个 DGCImpl_Stub。它是分布式垃圾回收的一个对象,并不是我们创建的,而且这里有三个Target后面会说到。
至此服务端创建注册中心分析到这里
服务端远程对象绑定创建的注册中心
在这里下断点,可以看见 调用了 Registrylmpl类的方法
跟进到这里
实际上 这个 bindings 就是 Hastable表
至此绑定就分析到这了
注册中心接受并处理服务端的绑定请求
在服务端主程序中进行 DEBUG 调试,然后在这里下一个断点。
- 注册中心 通过 TCPTransport#handleMessages 处理相关的网络请求
调用这个方法
是注册中心的代理,所以走到这个方法里
再调用这个方法
可以看见,如果传恶意的 Remote对象,就会存在漏洞。因为需要执行反序列化
至此,注册中心接受并处理服务端的绑定请求,到这了
客户端获取注册中心代理对象
这里下一个断点
调用了 getRegistry 方法
跟踪方法来到这里
调试到这里
返回加载创建好的Stub代理对象
至此客户端获取注册中心代理对象就到这里了
客户端通过注册中心查找远程对象
在这里下断点进行调试
来到invoke方法,再跟踪executeCall方法
executeCall 主要是处理网络请求的,这个方法中也使用了反序列化方法,也就是说调用invoke,都有可能执行反序列化,如果注册中心是恶意的
注册中心收到查询请求并返回远程对象代理
这里需要服务端/客户端之间的交互,在服务端主程序进行 DEBUG操作,然后断点在如图的地方。照顾时候运行客户端主程序,由于客户端执行了lookup方法,所以能够进行断点调试。
然后追踪调试到这里 Transport#disp.disppath
这里的skel只有注册中心才有,当判断是注册中心就会调用 oldDispath方法,显然这里满足条件了
进行追踪调试,调用 skel.dispatch 方法
总的来说是 RegistryImpl_Skel类调用了dispath方法,然后lookup方法中有一个反序列化的点,这里是存在漏洞的
最后服务端本地调用 RegiistryImpl.lookup(name),获取返回的远程对象,最后远程对象序列化,然后还给客户端,让它进行反序列化读取
至此,注册中心收到查询请求并返回远程对象代理,就到这了。
客户端调用远程对象的方法并获取返回结果
首先服务端运行主程序
然后客户端在这一行进行下一个断点进行调试
因为客户端获取的是远程对象动态代理 stub,也就是说它调用任意方法都会走到invoke里
跟进到重载的invoke()方法里面
重载的invoke方法里面有一个 marshalValue方法
这个方法进行了序列化的操作
实际上 call.executeCall() 方法我们知道执行这个方法是存在漏洞的,客户端如果遇到了恶意的注册中心。
跟进 unmarshalValue 方法,可以看见最后进行了反序列化的操作
可以看见之后返回了一个 HELLO的值,成功反序列化的值
到这一行,返回调用方法执行的结果,至此客户端调用远程对象方法结束。
服务端接受调用函数请求并返回执行结果
服务端在这里进行下一个断点,然后开启debug进行调试
这个时候在客户端运行主程序代码
就可以按F8来到这里
按f9直到 skel为 null的时候按F7步入调试
继续往下走
主要有以下三个关键的点
第一个关键点
先看第一个 unmarshalValue 方法,最后反序列化客户端序列化的内容
因为要反序列化数据的类型是String,所以它绕过了前面的判断
可以看见反序列化参数成功了
第二个关键点
再看第二个关键点,当服务端进行反射调用后,可以看见方法执行成功并且返回了值
第三个关键点
跟进到 marshalValue方法,可以看见它是进行序列化返回值的操作
至此可以看见客户端进程直接运行完毕了,因为它收到了来自服务端发送的返回值。
服务端完成接受客户端的调用、执行本地函数、返回执行结果的过程就是这样了。
客户端请求服务端-dgc
DGC代理的产生
在这里下断点进行调试
F8来到这里,可以发现stub 是 dgc代理
来到DGCImpl的实现类进行断点调试
至此DGC_Stub的创建就完成了,DGC是一个自动创建的过程,用于清理内存
DGC实现类Stub
DGCImpl_Stub 的类下有两个方法,一个是clean(强清除)、一个是dirty(弱清除)。
在clean方法中存在反序列化的漏洞点
DGC实现类Skel
在DGCImpl_Skel类中的dispath方法中,存在反序列化漏洞的入口
总结
漏洞点在客户端与服务端都存在,因为Skel代理是服务端,Stub代理是客户端。所以这就是JRMP所谓的绕过。
参考链接:
https://jaspersec.top/2023/12/24/0x0A%20RMI%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/#%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%94%BB%E5%87%BB%E6%9C%8D%E5%8A%A1%E7%AB%AF
https://www.bilibili.com/video/BV1L3411a7ax/?p=8&spm_id_from=pageDriver&vd_source=9f847c5239350d8425b1d2242ef00bbf
https://drun1baby.github.io/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/