ShokaX

Java RMI(远程方法调用)漏洞分析

发布于 字数统计 12.3k 字 阅读时长 42 分钟

Java RMI(远程方法调用)漏洞分析

发布于 字数统计 12,343 阅读时长 62 分钟

简介

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是怎么包装对象和发布的。

因为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