# 简介

PS:这里主要是我对 RMI 流程的跟踪,还有一些理解吧。这玩意越学越烦,艹。这篇文章不涉及漏洞利用,想略过原理直接看利用的看下一篇。

RMI (Remote Method Invocation) 即 Java 远程方法调用,RMI 用于构建分布式应用程序,类似于 RPC(Remote Procedure Call Protocol) 远程过程调用协议,RMI 实现了 Java 程序之间跨 JVM 的远程通信,使得一个 JMV 上的对象可以调用另一个 JMV 上的方法(方法在远程 JVM 上执行,只是返回运行结果)。这两个 JVM 可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

PS:RPC 可以理解成是一种思想,而 RMI 是这种思想在 java 中的体现,jrmp 就是 rmi 的具体实现协议。

# 参考链接

https://blog.csdn.net/weixin_43610673/article/details/124138537

(必看)b 站搜 白日梦组长

# 简单使用

RMI 最终实现的目的还是远程方法调用,也就是说我们可以调用远程对象的方法。

所以我们这里可以做一个场景:远程调用目标服务器上的一个对象的指定方法。

这里我们具体的测试方案就是 服务端存在一个 User 类,这个 User 类存在两个方法,一个用来为 User 这个对象赋值,一个是对 username 这个成员变量进行大写转换。

我们要实现的目标就是客户端远程调用这个 User 类的这两个方法。

①因为最终目标客户端上需要获取服务端的某个对象,并且客户端要能够操作这个对象的某些方法。所以客户端和服务段上都应该要有一个同样的接口。但客户端并不需要对这个接口进行实现。

②客户端和服务端的接口需要在相同的包名才能正常序列化和反序列化。(PS: 传输的对象其实是需要实现序列化接口的,但因为我们继承的那个 UnicastRemoteObject 类已经实现了序列化接口,所以我们是不需要特别设置了)

# Server

# 创建接口

首先,我们针对 User 这个类做一个接口,然后里面定义上这个接口存在的方法。

服务段接口处必须继承 Remote 类,并且里面的方法需要抛出 RemoteException。

image-20221020145219979

# 实现接口

接着就需要对这个接口进行一个实现了,只有实现了才能当作对象来传输给客户端。

要传输给客户端的实现类必须继承 UnicastRemoteObject 类(不继承也行,得在构造函数调用这个类的 exportObject 方法)

如下图,我们这个 UserImpl 类就实现了这个接口,完成了两个方法的填充。接着服务端就做完了。

image-20221020145245849

# 注册中心

前面其实就已经完成了 Server 的搭建,我们的实现类就是一个服务端(只是还没创建起来)
接着只需要把注册中心给搭建起来,这边就完成了。

PS:其实注册中心和 Server 基本都在一块的,这里当我们创建一个对象实例时,其实服务器已经开始监听一个随机的高位端口了,也就是说服务端已经跑起来了。

image-20221020145444639

# Client

前面我们讲了,在客户端中也要有这个类的一个接口(没有接口的话,编译器不知道获取的对象有什么方法,而且调用远程对象时 methodhash 会错误)。并且我们需要保证客户端和服务端的接口需要在相同的包名下。

如图:客户端只需简单的创建一个接口就行,里面的实现不用管。

image-20221020145508459

接着我们尝试去进行远程方法调用了。

image-20221020145544504

最终输出结果:

image-20221020145551955

# 简单分析下

这里简单的说下我的理解:

服务端就是提供远程服务的,它本质上就是一个类,一个对象,也就是说一个对象就是一个服务端。当一个继承了 UnicastRemoteObject 类的类被实例化成对象时,这个对象就变成了一个服务端,并且这个对象会被发布到本地的一个网络端口上,对外提供服务。

而注册中心就是管理这些服务端的,比如说你创建了 10 个对象,那这些对象都是单独的服务端,都有一个随机的高位端口。这种时候客户端想调用就很麻烦,而注册中心就是把这些服务端的网络位置信息给放到一个 hashMap 中存起来,这样客户端只需和注册中心交互,就能知道对应的服务端在哪里了。

客户端,没什么好解释的。客户端先是从注册中心拿到它想要的远程对象(服务端)的网络信息,然后再和这个对象(服务端)进行连接获取资源,操作对象。

结合下面两张图食用。

image-20221020145654270

image-20221020145702182

# 深入分析流程

如果只是了解 RMI 是什么,怎么利用和绕过的话,这里就没必要学了。这部分主要是介绍 RMI 的各个部分在干什么,为什么会导致反序列化漏洞而已。而且巨难理解,我反正没吃透。

这里就分析了服务端和注册中心,客户端是没分析的,主要是流程差不多,懒得追了。

# Server 的创建深入分析

PS: 对 Server 和 registry 分析的时候我也有点云里雾里,没太懂,就大致搞清楚了流程而已,问题不大。

这里我们的目的是去分析 server 是怎么包装对象和发布的。

因为 rmi 这个模块是在 sun 包中的,sun 包是没源码的,所以在分析前,先把 sun 包的源码给引入到 SDK 中,不然调试不过去。具体操作就不多说了,看 B 站白日梦组长讲 CC 链的那个视频,他有讲怎么做。

# 入口

这里我们注释掉注册中心的代码,给服务端打上断点,进行跟踪分析。

因为这个 UserImpl 继承了 UnicastRemoteObject 类,并且 UserImpl 的构造方法没任何内容,所以它会隐式调用父类的构造方法。

image-20221020150041215

# 当前是 UnicastRemoteObject 类

ok,跟着走,因为我们这个类它是继承了 UnicastRemoteObject 这个类的,所以会调用它的构造方法。进而走到这个 exportObject 方法。这个方法简单理解为暴露对象。

image-20221020150052432

继续跟进发现又调用了 exportObject,但此时给的参数不同了,也就是说调用的方法是另一个,见下图,此时看不太懂这个 exportObject 做了什么,先不管,先按照执行流程走。

这里我们简单看一下,这个 new UnicastServerRef 做了什么,可以看到它的参数是一个 port,所以可以大胆猜测这个类就是做网络请求的。

因此这个 exportObject 的功能我们也可以大胆猜测:就是把对象和网络关联起来,也就是把这个对象绑定到某个端口上,以便后续调用。

image-20221020150108772

image-20221020150113810

# 此时换到了 UnicastServerRef 类

接着我们分析下前面猜测的那个处理网络请求的 UnicastServerRef 类。

跟进去发现,它调用了父类的构造方法,传参是一个叫 liveRef 的类,这个类的参数还是一个端口。此时感觉这个 UnicastServerRef 其实还是没什么用,就单纯的套娃,简单看了下构造器,发现确实是真的没什么用,就是把 liveRef 赋值给另一个参数而已。真正处理网络逻辑的应该是 LiveRef (port).

image-20221020150127230

image-20221020150130195

# 又换到了另一个类 - LiveRef 类

跟进 LiveRef,我套它个猴子,又开始套娃了。这个 ObjID 简单看了下没什么用就生成一个随机 ID 的。

image-20221020150143686

此时跟进它的两参构造器,发现又又又套娃,老样子,先看看它的构造器,发现也没操作什么对象,和前面一样就是对一些值进行了赋予而已。所以真正处理网络的是 TCPEndpoint.getLocalEndpoint (port) 这个对象。

image-20221020150154765

# 此时跳到了 TCPEndpoint 类

跟进这个对象,发现它又在调自己,不管他,往下看,发现它获取了 ip,获取了本机的 ip。但很可惜,这个方法走完都没看到端口在变的。只是拿到了 ip 而已。

image-20221020150210580

image-20221020150216069

到这里就没有更深入的其他调用了,就是慢慢的解套,一直跟,最后回到我们前面没分析明白的 exportObject (Remote obj, UnicastServerRef sref) 这个两参构造器了。

# 此时回到了 UnicastRemoteObject 这个类

这里回到了我们前面 UnicastRemoteObject 这个类的两参 expoetObject 方法。

但这个方法中间也只是做了一些赋值的操作。

但在最后 return 时,我们发现它又又又调用了一个类的 exportObject 方法(当前是 UnicastRemoteObject 类,这里是跳到了 UnicastRemoteRef 类)。

image-20221020150237616

# 来到另一个没去过的类 UnicastRemoteRef 类

ok,继续跟进。此时来到 UnicastRemoteRef 类的三参 exportObject 方法。这里我跳快了,不知道什么情况,它就拿到了端口。而且这里我们发现,它 return 了一个叫 stub 的玩意,这个玩意不就是给客户端的东西么。

image-20221020150303916

重新打上断点,重新调试一次,看看这个端口在哪偷偷摸摸拿到的。最后发现是在 LiveRef 的 exportObject 上拿到的。这里需要注意的是它传了一个 target 进去,这个 target 由很多东西组成,我们后续再分析。

image-20221020150316681

# 追踪回到 LiveRef 类和 TCPEndpoint 类最后到 TCPTransport 类

又跳到 TCPEndpoint 类的 exportObject

image-20221020150334073

最后是跟进到了 TCPTransport 类中,发现它是最后一步了,这里直接开始监听端口。到这里网络层的东西我们就搞清楚了。

image-20221020150343657

可以看到,这里是开启了一个 socket 端口,然后后面是开启了线程等待客户端的连接。

image-20221020150351713

跟进这个 newServerSocket,就能看到它的端口是怎么获取的了,再往下就没必要分析了。

image-20221020150400094

但别忘记,前面我们还传入了 target 这个东西,target 由很多东西组成,我们也简单分析看下它到底做了什么(主要是感觉我也不太懂这个 target 到底做了什么,能看懂的就是被塞到了两个表里)。

这里跟踪过程略过。

image-20221020150407939

最后确实塞到了两个静态表里,应该是用来对应网络发布的,就前面那个 socket 网络端口对应的数据是这个 target。

image-20221020150417009

# UnicastServerRef 类 - Stub(存根)成分分析

接着我们回过头来分析 Stub 的成分。看这个函数,感觉 stub 就是一个代理对象。

image-20221020150430548

跟进去到 Util 类,很明显的动态代理创建,然后接口是 User 接口。也就是说客户端拿到的也是个代理对象,然后通过代理对象再操作返回结果给客户端。

而且这里的 handler 可以看到就是一些网络处理的类,所以这个代理的意义就是不言而喻了。

image-20221020150439145

而且在这段代码中,不只是创建了存根,骨架也一并创建了。

image-20221020150446227

# 小结

到这里,差不多就搞完了 Server 的分析,虽然仍有疑问,但大致流程还是能够梳理出来的,Server 先是

通过一系列手段占用一个高位端口,进行监听,然后把这个端口的信息 LiveRef 绑定到 User 类的代理类 Stub 上。

最后得到的对象就是一个含 LiveRef 信息的 User 实例(其实就是存根)。

其中存根 Stub 是建立 socket 连接,并向 Skeleton 发请求的。通过 Skeleton 调用 User 这个类的相应的方法,最后接收返回的结果。

而骨架 Skeleton 类用到了线程,它长驻在后台运行,随时接收 client 发过来的 request。并根据发送过来的 key 去调用相应的对象的 method。

# Registrt 的创建分析

接着我们来看注册中心它到底做了什么呢?

从代码来看它就两行很短,简单的看就是开启监听器了一个端口,然后用字典把存根给存了起来。对外提供服务。

image-20221020150527302

# Registry 对象的创建

跟进代码发现,没能步入 try,进入到的是 else 语句,这里就两行代码,第一行我们老懂了,就是和 server 一样,创建一个网络端口而已,返回的是这个网络引用的信息。

image-20221020150542942

然后我们分析第二个代码,先不管 setup 方法,看 UnicastServerRef 这个类,它很明显就是把注册中心这个对象给绑定到了我们生成的 LiveRef 类中,也就是说把注册中心这个对象给绑定成了远程对象,让它能够被远程访问调用。

然后再看 setup 方法。一个 ref.exportObject,就是把这个远程对象给发布出去了。简单跟进去一看,其实就是和 Server 中的差不多,创建存根什么什么的。也就是说客户端和注册中心访问也是依赖存根的 emm。毕竟这个存根存储了注册中心这个对象和它的网络引用。

image-20221020150557484

# 绑定远程对象

这个 bind 就一个函数,没什么奇奇怪怪的调用,进来就是先判断 bindings 这个 hash 表是否有重名的 key 了,有就抛出错误,否则就把我们的远程对象给绑定到 hash 表中,提供服务。

image-20221020150610967

可以看到这个 registry 对象,它有两个内容,一个就是 bindings 这个 hash 表,另一个就是 ref,它自己的存根。

image-20221020150622621

# 小结

简单来看,这个 Registry 对象的创建其实是和 Server 创建远程对象一样的,只是这里绑定的是固定端口罢了。并且这个对象还用一个 hash 表去存储了服务对象提供的对象的存根。没什么难度 ok。

# 各个角色受到的威胁

在 rmi 中所有在网络上传输的数据都是序列化数据。服务端、注册中心、客户端都会受到其他两者的威胁。也就是说 RMI 在涉及之初就没考虑过安全。所有角色都会受到其他两者的威胁。

以下我们还是以分析流程来找有问题的点。只做分析,具体攻击利用看下篇。

# 客户端受到的威胁

# 来自注册中心的威胁

# 创建本地注册中心对象

这里并没有去真正连接目标注册中心,只是在本地又创建了一个注册中心的 stub 对象,跟进代码,代码量不大,没什么套娃,而且调用的方法都是我们之前常调用的那几个。无非就是创建 LiveRef,然后绑定到 stub 中,返回而已。

image-20221020151120900

image-20221020151129447

# 调用 lookup 真正连接注册中心获取远程对象 stub

这里代码也不是很复杂,很明显了,客户端和服务端的数据传输是通过 socket 来完成的并且在传输过程中对象都是经过了序列化和反序列化的,这就意味着如果注册中心是恶意的,我们的客户端就有可能受到威胁。

image-20221020151147051

当然前面只是其中一个反序列化点,其实还存在另一个点,就是 ref.invoke(call)这个,我们可以尝试跟进去,

image-20221020151158153

跟进来只后,跳到了 UnicastRef 这个类的 invoke,可以看到它调用了 call 的 executeCall 方法。

image-20221020151205022

我们继续追踪,来到了 StreamRemoteCall 类的 executeCall 方法。可以看到,这里存在着一个处理异常的代码出现了 readObject 操作。这里代码设置本意可能是如果输入流是个异常类,它就可以通过反序列化来获取得更多详细信息。这就导致如果注册中心返回一个恶意的流的话,客户端就会在这里进行反序列化。而几乎每个涉及连接注册中心的方法都会用这个 invoke。

image-20221020151212607

# 来自服务端的威胁

接着我们来分析客户端请求服务段的流程了,首先,进入到的是 RemoteObjectInvocationHandler 这个处理器的 invoke 方法(理所当然的,调用代理对象的任意方法都会进入处理器的 hander 方法)。

然后追着追着,它就跳到了 UnicastRef 的 invoke 方法了,在这里就很关键了。(那个 marshaValue 方法是先判断参数是否为基础类型,不是就序列化)

image-20221020151313525

这里还是这个 invoke 方法,可以看到,我们就算没那个 call.execuetCall 来执行反序列化,在下面的地方他一样亲自进行了反序列化。从这里我们也可以看出,这个处理器的存在就导致着,客户端在 RMI 中,时时刻刻都有可能受到注册中心和服务段的反序列漏洞攻击。

image-20221020151323561

# 注册中心受到的威胁

因为注册中心和服务端在高版本 JDK 下,只能在同一台机子,所以这里就不讨论注册中心和服务端之间的互相威胁了,只讨论客户端是怎么威胁注册中心和服务端的。

首先客户端和注册中心的交互主要有连接注册中心和获取注册中心中注册的 stub。

从上面的交互图中,我们得知,客户端操作的是 stub 来访问我们的骨架 skel,所以在注册中心,我们的断点也是下在 stub 那边。也就是 RegistrImpl_Skel 中(对应的路径是 rt.jar/sun/rmi/registry/RegistryImpl_Skel)。

这里我们进去之间就发现 dispatch 这个方法很有可能是处理客户端调用逻辑的。先简单分析,可以看到下面有个 switch 选择,其中分了很多 case,并且不同 case 还给了注释,这下子我们就很清晰了,不同的 case 对应的是不同的方法调用。但多个 case 里面都涉及到了 readObject 的调用,所以就很明显存在反序列化了。

image-20221020151355527

接着我们下断点来分析哈,可以看到,客户端传进来的 String 类型 user 就是它的参数,如果此次传的是恶意对象就有可能威胁到注册中心。

image-20221020151409032

# 服务端受到的威胁

这里我们继续分析客户端在调用远程方法时,服务端是怎么去处理的。

这里断点应该在哪呢?老样子,一般都是 Dispatcher 这个调度器来操作的。所以先找这个类。这里在 sun.rmi.server 目录下只找到了它的接口,那就继续寻找这个接口的实现,发现 UnicastServerRef 这个类比较像是处理数据的类。追踪里面的 dispatcher 方法。打上几个断点看看传进来的是什么东西。

可以看到,这里 parms 参数就是客户端搞过来的传参,然后后面就是对这个参数进行反序列化了。并且它上面的注释也写的很明显了,反序列化参数。

image-20221020151430113

image-20221020151434820

同样的,下边的代码处,也是对返回结果进行了序列化给客户端的,所以说客户端也是会受到服务端的威胁的。

image-20221020151443110