Java反序列化-RMI流程分析

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主程序运行,可以看见程序开始监听等待连接
image.png
然后这个时候运行RMIClient 主程序
image.png
这个时候可以看见服务端,成功调用了实现类的方法
image.png

RMI流程

image.png
RMI主要有三部分:

  • 服务端
  • 注册中心
  • 客户端

然后漏洞是产生在 两两通信之间的。

服务端创建远程服务

在这里进行断点调试
image.png
然后来到这里,这是一个静态赋值
image.png
继续按F7,我们来到了这里,远程对象实现类的构造方法
image.png
继续按 F7 就会来到父类的构造函数
image.png
然后接着按F7 我们来到了这里,这个部分会把远程对象发布到一个随机的端口上。可以看见如果我们如果传0,它会发布到一个随机的端口
image.png
然后 按F8来到这里,可以看见调用了这个 exportObject 方法,按F7看一下
image.png
这个方法的大概意思是,导出对象或者发布对象这个意思,很明显它就是一个核心的方法。因为 RemoteObjlmpl 类的构造方法,可以不用继承 UnicastRemoteObject ,也可以直接调用这个 exportObject 静态方法。
image.png
在这里会有处理网络请求的逻辑
image.png
按F7,看一下,点击进到这个方法里
image.png
可以看见它这里又创建了一个 LiveRef 类
image.png
按F7来到这里,点击这个方法名
image.png
按F7,点击 this,看一下构造方法
image.png
可以看见主要有三个参数
image.png
主要看一下 getLocalEndpoint 这个方法,可以看见这个 TCPEndpoint 的意思是处理网络请求大概意思
image.png
可以看见 TCPEndpoint的 构造方法,两个参数,一个 host 、一个怕port。也就是说只要给它一个 ip 一个端口它就可以处理后面的这些网络请求。
使用 LiveRef方法,放的主要是这些
image.png
这个时候返回 LiveRef 方法,看一下它的构造方法
image.png
可以看见有三个参数,真正有意义的是这个ip和端口 进行了封装
image.png
然后按F8来到这里,这里调用了父类的构造方法
image.png
按F7,可以看见它的父类构造方法,这里只是进行了一个赋值,并不是建立了一个新的,还是 liveRef
image.png
然后我们按 F8出来到这里,这里返回了一个方法
image.png
按F7继续调用就来到了这里,sref下有这个 LiveRef
image.png
远程对象还是用 sref 进行了赋值,然后下一步调用了 exportObject 这个方法
image.png
继续按 F7看一下这个方法,可以看见 stub,stub是客户端操作的一个代理。

  • 服务端先创建好了,然后把它放到注册中心上,然后客户端去注册中心拿,拿到再去操作它
  • 通过它stub操作另外一个代理,然后才能真正调用服务端的远程对象

image.png
按F8加F7来到了这里,然后点击。看一下这个方法是怎么创建的
image.png
implClass参数是一个远程对象的实现类,我们自定义的类
image.png
然后这个 clientRef是一个封装的 LiveRef
image.png
按F8来到这里,如果这个 stubClassExists 方法为真
image.png
可以看见 如果 远程对象实现类如果有这个 _Stub结尾的话,结果会返回为真
image.png
F8来到这里,这里是一个创建动态代理的流程
image.png
这个是调用处理器,我们进去看一下
image.png
进去 super方法
image.png
是一个 ref,ref里面还是封装着 LiveRef
image.png
继续按F8可以看见,就已经把动态代理创建好了
image.png
F8来到这里,可以发现是一个总封装,按F7,然后点击 Target
image.png
F8来到这里,重点是这个ID,它和 LiveRef的ID是一样的,总的来说就是把这些东西都放在里面了
image.png
一直按F8来到这里,可以看见 exportObject方法把 target 发布出去
image.png
按F7跟到调用的部分,来到了 TCPTransport类里
image.png

发布远程对象

Listen 实际上我们知道是监听的意思,监听远程对象的某个端口
image.png
这里按F7跟进去,然后按F8来到了这里。可以看见它创建了一个新的sockert,这是一个服务端的socket
image.png
这里开启了一个新的线程,然后等待客户端连接
image.png
继续按F8出去代码逻辑来到这里,可以看见一开始 liveRef的默认端口是0,实际上这里已经随机分配了一个端口了
image.png
查找调用 listen方法,然后找到这里,可以看见 如果 端口为0,它会随机给它一个值,然后返回服务端,实际上服务端已经把这个远程对象发布出去了,但是它把它发送在一个随机的端口上,所以客户端默认是不知道的。
image.png

服务端记录发布

最后服务端还需要记录一下,F8来到这里,然后按F7
image.png
来到这里,继续按F7
image.png
然后就来到了这里,这里是一个简单的赋值,不用管,继续按F8
image.png
然后发现ObjectTable调用了一个 putTarget的方法
image.png
按F7进去,然后一直按F8来到这里,可以看见有两个方法
image.png
这两个方法会把信息保存到这两个 table
image.png
后面一直按F8可以看见已经开始监听网络的线程了,等待客户端进行连接
image.png

服务端创建注册中心

image.png
创建 RegistryImpl 对象,可以看见创建注册中心的默认端口为1099
image.png
来到了注册中心的实现类
image.png
我们来到sref的调用方法

  • uref 是来自 UnicastServerRef类的
  • 也就是说调用 UnicastServerRef类的 exportObject方法

image.png
在 exportObject的方法可以看见参数 permanent的意思为永久,意思是我们创建注册中心这个对象为永久对象
image.png
创建 RegistryImpl_Stub 代理对象,在流程图可以知道它是用作于客户端的代理
image.png
image.png
进去 createStub 方法可以看见,类名的名字改变了,return 返回了加载的初始化 ref
image.png
创建 建 RegistryImpl_Skel 代理对象代理对象。Skeleton 在流程图可以知道它是作于服务端的代理
image.png
image.png
image.png
跟踪方法
image.png
然后f8来到这里,可以发现static中的数据的 objTarget的第二个Target对象的Value的值有一个 DGCImpl_Stub。它是分布式垃圾回收的一个对象,并不是我们创建的,而且这里有三个Target后面会说到。
image.png
至此服务端创建注册中心分析到这里

服务端远程对象绑定创建的注册中心

在这里下断点,可以看见 调用了 Registrylmpl类的方法
image.png
跟进到这里
image.png
实际上 这个 bindings 就是 Hastable表
image.png
至此绑定就分析到这了

注册中心接受并处理服务端的绑定请求

在服务端主程序中进行 DEBUG 调试,然后在这里下一个断点。

  • 注册中心 通过 TCPTransport#handleMessages 处理相关的网络请求

image.png
调用这个方法
image.png
是注册中心的代理,所以走到这个方法里
image.png
再调用这个方法
image.png
可以看见,如果传恶意的 Remote对象,就会存在漏洞。因为需要执行反序列化
image.png
至此,注册中心接受并处理服务端的绑定请求,到这了

客户端获取注册中心代理对象

这里下一个断点
image.png
调用了 getRegistry 方法
image.png
跟踪方法来到这里
image.png
调试到这里
image.png
返回加载创建好的Stub代理对象
image.png
至此客户端获取注册中心代理对象就到这里了

客户端通过注册中心查找远程对象

在这里下断点进行调试
image.png
image.png
image.png
来到invoke方法,再跟踪executeCall方法
image.png
executeCall 主要是处理网络请求的,这个方法中也使用了反序列化方法,也就是说调用invoke,都有可能执行反序列化,如果注册中心是恶意的
image.png

注册中心收到查询请求并返回远程对象代理

这里需要服务端/客户端之间的交互,在服务端主程序进行 DEBUG操作,然后断点在如图的地方。照顾时候运行客户端主程序,由于客户端执行了lookup方法,所以能够进行断点调试。
image.png
然后追踪调试到这里 Transport#disp.disppath
image.png
这里的skel只有注册中心才有,当判断是注册中心就会调用 oldDispath方法,显然这里满足条件了
image.png
进行追踪调试,调用 skel.dispatch 方法
image.png
总的来说是 RegistryImpl_Skel类调用了dispath方法,然后lookup方法中有一个反序列化的点,这里是存在漏洞的
image.png
最后服务端本地调用 RegiistryImpl.lookup(name),获取返回的远程对象,最后远程对象序列化,然后还给客户端,让它进行反序列化读取image.png
至此,注册中心收到查询请求并返回远程对象代理,就到这了。

客户端调用远程对象的方法并获取返回结果

首先服务端运行主程序
image.png
然后客户端在这一行进行下一个断点进行调试
image.png
因为客户端获取的是远程对象动态代理 stub,也就是说它调用任意方法都会走到invoke里
image.png
跟进到重载的invoke()方法里面
image.png
重载的invoke方法里面有一个 marshalValue方法
image.png
这个方法进行了序列化的操作
image.png
实际上 call.executeCall() 方法我们知道执行这个方法是存在漏洞的,客户端如果遇到了恶意的注册中心。
image.png
跟进 unmarshalValue 方法,可以看见最后进行了反序列化的操作
image.png
可以看见之后返回了一个 HELLO的值,成功反序列化的值
image.png
到这一行,返回调用方法执行的结果,至此客户端调用远程对象方法结束。
image.png

服务端接受调用函数请求并返回执行结果

服务端在这里进行下一个断点,然后开启debug进行调试
image.png
这个时候在客户端运行主程序代码
就可以按F8来到这里
image.png
按f9直到 skel为 null的时候按F7步入调试
image.png
image.png
继续往下走
image.png
主要有以下三个关键的点
image.png

第一个关键点

先看第一个 unmarshalValue 方法,最后反序列化客户端序列化的内容
image.png
因为要反序列化数据的类型是String,所以它绕过了前面的判断
image.png
可以看见反序列化参数成功了
image.png

第二个关键点

再看第二个关键点,当服务端进行反射调用后,可以看见方法执行成功并且返回了值
image.png

第三个关键点

跟进到 marshalValue方法,可以看见它是进行序列化返回值的操作
image.png
至此可以看见客户端进程直接运行完毕了,因为它收到了来自服务端发送的返回值。
image.png
服务端完成接受客户端的调用、执行本地函数、返回执行结果的过程就是这样了。

客户端请求服务端-dgc

DGC代理的产生

在这里下断点进行调试
image.png
F8来到这里,可以发现stub 是 dgc代理
image.png
来到DGCImpl的实现类进行断点调试
image.png
至此DGC_Stub的创建就完成了,DGC是一个自动创建的过程,用于清理内存

DGC实现类Stub

DGCImpl_Stub 的类下有两个方法,一个是clean(强清除)、一个是dirty(弱清除)。
image.png
在clean方法中存在反序列化的漏洞点

DGC实现类Skel

在DGCImpl_Skel类中的dispath方法中,存在反序列化漏洞的入口
image.png

总结

漏洞点在客户端与服务端都存在,因为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/
  • 19
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cike_y

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值