
我看后面挺多漏洞和这个RMI和JNDI有关,现在先学习一下
RMI介绍
RMI 全称 Remote Method Invocation(远程方法调用),在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。
RMI三个部分
- 服务端:提供服务
- 客户端:调用服务端端服务
- 注册端:提供服务注册与服务获取,客户端可以在这里查询要调用的方法的引用
交互过程(图源 https://fynch3r.github.io/Java-RMI%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AE%9E%E6%88%98/)
在Java RMI里
- 远程服务器实现具体的Java方法并提供接口
- 客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法
- 其中对象是通过序列化方式进行编码传输的
- RMI全部的宗旨就是尽可能简化远程接口对象的使用
RMI实现
先写一个接口
1 | import java.rmi.Remote; |
写一个类继承这个接口
1 | import java.rmi.RemoteException; |
服务端代码
客户端代码
然后就能运行起来了
这里有个特别坑的地方,在server的代码里面,绑定registry之前要设置好服务器ip,不然client连接的时候会报错,本身不是很熟悉这个东西,拷打gpt半天都没结局,最后换了cursor才解决的。。。
1 | System.setProperty("java.rmi.server.hostname", "127.0.0.1"); |
从WireShark观察RMI通信过程
wireshark开一个本地抓包,粗略看一眼就可以发现有很多RMI协议的包,当然前后的TCP包也要注意
可以看到这里25-27号包是在tcp三次握手建立连接的过程
建立连接之后数据端与注册中心建立通讯,1099是注册端,50140是客户端
建立TCP连接后开始请求调用,看到后面有一个CALL,数据包里面包含了要调用的远程函数名,
再往下看到ReturnData,返回了很多代理服务对象,是序列化的数据,可以看到序列化的魔术头,最后还看到一个新的端口
c3b0 50096,指的是提供服务的端口
后面客户端(50141)新起一个端口和服务端(50096)建立连接,会看到建立了第二次TCP连接,有三个握手包
建立连接后继续发送序列化数据和我们要打印的数据
红色和紫色分别代表客户端和服务端发送的数据,可以清晰地看到数据的来回传送
可以看到整个过程中数据流都是通过序列化传输的,那客户端和服务端必定有序列化反序列化的地方。
看完整个流量包过程再看会前面那张流程图就会清晰很多,推荐所有人都去动手做一下。
从源码观察调用过程
服务端
创建远程对象然后发布到网上,就已经可以通信了,我们要了解这是个什么样的过程
说是没什么server这个部分其实没什么漏洞,但是要了解一下过程
创建服务并发布
为了跟着教程,这里我用Drunk师傅的代码调试,不知道为什么自己的idea跳不进去,我这里断点打在RemoteObjectImpl
RemoteObjectImpl
是继承UnicastRemoteObject
类(继承这个类是硬性要求。另一种写法是在下面的代码块写一段直接调用),所以会先到父类的构造函数,f7步进
跳到下图地方,可以看到给了一个port端口,一开始是0
再继续往下看,来到 exportObject()
,就是这个函数负责把服务发步到网络,跟进去
看到还套了一层,里面有一个UnicastServerRef()
,这里在idea里直接点击UnicastServerRef,就会跳转到这个函数里面
有一个**LiveRef
**,这个很重要,继续跟进
是一个构造函数,点this进去看看,构造函数如下
第一个参数是ID,第三个参数是true,主要是第二个参数,TCPEndpoint这个名字一听就是和网络请求有关的,构造参数是一个host一个port
LiveRef构造函数点击this进去
可以看到进行了赋值,host和port都在endpoint里面了,而这个endpoint在LiveRef里面,注意这些id,会发现整个过程处理的都是一个LiveRef
以上完成了对LiveRef
的建立,代码会回到前面
一直走,其中这一步把之前的LiveRef
给ref,操作对象一直都是同一个
跳到这里又调用了一个`exportObect,继续跟进
一直跟到这里出现了stub,这个是在整个RMI一个很重要的东西
在这里先放一张原理图
RMI在服务端建立的时候,现在服务端新建一个stub,再把stub传给RMI registry,最后让RMI Client去获取stub
这个stub在后面一个createProxy的地方被创建,看名字就知道是创建代理,跟进去看一下
可以看到这里有一个判断,这里不会进入,先不管,后面接触了再理解
然后到了后面就是创建动态代理,看一下里面的参数
第一个参数是一个AppClassLoader,第二个是个远程接口,第三个是个调用处理器,可以看到里面包裹的就是我们之前一直操作的Ref,始终处理的对象就是这一个。
创建完动态代理后出来,可以看到stub已经有内容,里面也带上了Ref
再继续往后,看到代码在创建一个Target,我们把taget理解成一个总封装,把我们之前创建的有用的参数包起来
看一下构造函数里面怎么操作的
看里面的值,一直都是一个LiveRef
一路步过跳出target,看到后面又进入一个exportObject()
一路跟进,到后面发现进入到TCPTransport里面的exportObject(),一进来就看到里面一个listen()函数,这里相当于开始处理真正的网络请求
跟到一定的地方会看到里面新建一个socket,等待连接
后面的thread里就去做完成连接之后的事,如果有连接,就会进入run
后面还有一个地方给port赋值
listen()主要就是开了一个socket,在整个流程里添加了port,这块内容记录在stub里面
发布服务后的记录
再继续一直跟,出现了两个put,RMI会把所有信息保存在两个table里,相当于一个记录作用吧,这块就不细看了
服务端创建小结
思路还是比较清晰的,主要就是一直利用exportObject()包装对象,一直处理的对象也就是那个LiveRef,进行各种各样的封装赋值。顺便给了端口,开了socket。
完成之后还有一个记录,是保存在静态的HashMap里面的,Drunk师傅说当日志理解就可以了。
服务端自己创建远程服务整个过程不存在漏洞。
创建注册端+绑定
创建注册端和服务端还有点像,本质上也是把服务发布到网上,这块过的快一点
创建注册中心
断点打在这里
然后进入到createRegistry()
->RegistryImpl()
,这里传入端口号1099
再跟进,中间有些别的代码是些安全检查,我们一直看到136行,这里新建了一个我们很熟悉的LiveRef和一个UnicastServerRef
和之前创建远程服务端流程很像,我们跟进到里面的setup看一下
里面也是用到exportObject(),对比一下之前创建远程对象的exportObject()
对比一下他们的参数,主要区别就是第三个参数,服务端是false,注册端是true,描述是permanent,很好理解,这代表我们注册端注册的服务是一个永久对象,服务端的服务是一个临时对象
继续跟进,也是来到创建stub的地方,但是这个和之前服务端的创建就不一样,进入到createProxy()
这次这里的判断就比较重要,我们跟进stubClassExists()
判断
大概逻辑就是看能不能获取到RegistryImpl_Stub
这个类,如果有就返回true,没有就false,我们这个版本的java是有RegistryImpl_Stub
这个类的,所以返回true,我们会进入到if语句里
所以下一步是进入到createStub()
里面,跟进看一下
就是通过反射创建这个对象,里面放的是ref
相比于之前发布远程对象中的 Stub,是一个动态代理,里面放的是一个 ref。
现在发布远程对象是用 forName 创建的,里面放的也是 ref,是一致的。
创建完回去再往下,我们会进入到setSkeleton()
进去里面有一个createSkeleton()
,根据前面的流程图,我们知道skeleton是作为服务端的代理
skeleton是通过forname()的方法创建的
再往后,又来到target,作用也很前面服务端一样把数据包裹进去
此时看变量情况,ref里面多了skel对象
迅速跳过,到下面的exportObject()
,流程和前面一样,直到一个super.exportObject(target);
跟进去
有一个ObjectTable.putTarget(target)
又是一次封装
一路赋值完,我们关注一下控制台变量的情况,主要是点开objTable,可以看到有三个target
target@801里面disp和stub里面包的ref是同一个,端口号都是1099,stub是一个RegustryImpl_Stub
target@842里面的stub是$proxy对象的,也可以看到ref详情
再看target@840,里面的stub是一个DGCImpl_Stub,是分布式垃圾回收的一个对象,后面会再接触到
绑定
主要是这个bind()方法,比较简单,标注在图里了
小结
注册端创建和服务端创建总体还是差不多的,区别在于一个是持久的一个是临时的,还有stub的判断和创建方式也有些不同。关于为什么最后数据有三个target,且里面stub的类型还不一样,要到后面才能解答。
客户端请求注册中心–客户端
*这块存在漏洞
三个代码都打断点
进入getRegistry()
,会看到很多之前熟悉的操作,赋值LiveRef,createProxy等
和之前一样新建一个ref,可以看到ip端口信息都在里面了,这里就算我们获取了注册中心等stub,后面去查找远程对象
代码第二句进来看到很多log相关的语句,无关紧要,跳了
直到这个lookup,这里因为java版本对不上,没办法打断点,直接看逻辑吧
此时看到变量情况,会看到有一个param_1="remoteObj"
,这个是传参的var1,这个参数是作为序列化数据传进去的,注册中心后续会通过反序列化读取。
接着有一个supre.ref.invoke(var2)
,查看详情可以知道是UnicastRef
类,invoke()
方法里面有 call.executeCall()
在 call.executeCall()
里面处理真正的网络请求
进入到call.executeCall()
里面有一个readObject()
看到in变量,不难理解就是数据流的东西
这里处理异常本意是好,但是如果注册中心返回的是一个恶意的流,会导致在这个地方反序列化。这个点更隐蔽,影响面更大,因为所有处理网络请求都会经过这个地方
以上就是客户端和注册中心之间的操作,f8回到主函数
可以看到我们已经获取了remoteObj这个动态代理,里面包含了一个ref
客户端请求服务端–客户端
这里断点打在第三句
因为此时helloService
是一个动态代理类,所以一开始会进入到invoke
重点看到最底下的invokeRemoteMethod
方法,跟进去,里面有一个ref.invoke()
,是一个重载的方法,里面是用来创建一个连接,具体逻辑如下
继续走,有一个marshalValue
方法,里面会做一堆判断,然后序列化我们调用远程函数时传进去的参数
再继续往前,又遇到call.executeCall()
,前文也提到了,RMI执行网络请求的时候就会一定会走到这个地方,是存在危险的
后面就是unmarshalValue()
,因为我们传入的参数类型是String,不符合前面一系列判断,这里会进行一次反序列化操作,把数据读回来,这里也是一个攻击点
然后一路跟进就走会主函数,代码完成远程函数调用
小结一下
- 存在攻击的点,注册中心->服务端,查找远程对象的时候存在攻击点,具体表现为服务端打客户端,入口类是
call.executeCall()
,里面再抛出异常的时候存在反序列化点 - 服务端->客户端两个攻击点,一个是
call.executeCall()
,另一个是unmarshalValue()
注册中心处理客户端请求
从注册中心角度看客户端请求,主要是处理skel,前面新建注册中心的过程有一个listen(),最后肯定会走到RegistryImpl_Skel
里面(具体过程可以回顾白日梦组长的视频)
断点打在这里
RMIServer开启调试模式,RMIClient运行程序,调试会自动跳到这里
看一下此时target里面有一个stub,stub里面的ref指向的是1099端口
往下走到final Dispatcher disp = target.getDispatcher();
,这一步是把skel
的值放到disp中
再继续往下有一个disp.dispatch
,跟进去
oldDispatch()
disp.dispatch
()->oldDispatch()
->skel.dispatch()
重点是skel.dispatch
,这部分源码主要是case,以下这段引自
我们与注册中心进行交互可以使用如下几种方式:
- list
- bind
- rebind
- unbind
- lookup
这几种方法位于
RegistryImpl_Skel#dispatch
中,也就是我们现在 dispatch 这个方法的地方。如果存在对传入的对象调用
readObject
方法,则可以利用,dispatch
里面对应关系如下:
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
只要中间是有反序列化就是可以攻击的,而且我们是从客户端打到注册中心,这其实是黑客们最喜欢的攻击方式。我们来看一看谁可以攻击。
比如case 0里面有readObject()
一圈看下来只有list没有
小结
客户端请求的时候,注册中心主要就是处理target,完成skel的生成和处理
漏洞点主要在dispatch,多数case里面都有readObject(),方便后续利用
服务端处理客户端请求
继续走一样的流程,但是这次的target里面stub是$proxy0
,和注册中心的流程有些区别
来到这里,因为skel是null,所以不会进入oldDispatch()
后面可以看到一个method,参数是我们传入的参数sayhello(要调用的远程函数名)
继续往下走,重点是unmarshalValue()
,这个部分可以理解成对称。前面客户发送请求的时候,把参数用marshalValue序列化,这里用unmarshalValue反序列化出来,还有其他地方有这样的对应。
DGC
前面创建注册中心的时候有一个DGCImpl_Stub
,现在看一下是什么机制
断点打在这里
前面说会把taget放到一个静态表里,相当于日志记录作用,在put之前,我们可以看到变量里面就有这个DGC,而且还存放在static里面
现在来看DGC是怎么创建的,主要就是这两句,看似没什么操作
但是这里用了静态变量 调用静态变量会触发类的初始化
看到DGCImpl里面有一个static方法,作用是 class initializer,断点打在这里
后面又有createProxy(),进去里面有一个createstub
这里像注册中心创建远程服务一样,看是否可以获得这个类DGCImpl_Stub
,是有的
整个过程就像注册中心创建远程服务,但是目的不一样,这里创建的是用于回收机制的,并且端口随机
DGC有漏洞的地方:看到这个类DGCImpl_Stub
本身,里面有clean和dirty方法
DGCImpl_Skel
也有
- DGC是自动创建的一个过程,用于清理内存。客户端和服务端都有攻击点,而且这是自动生成的,只要创建了远程对象就会有DGC服务
参考
【Java反序列化RMI专题-没有人比我更懂RMI】https://www.bilibili.com/video/BV1L3411a7ax?p=10&vd_source=46e5237289ae6c1a3c7bcab6091e42a6
碎碎念
这篇拖了好久好久好久,博客也没有保持更新频率,真是太拖拉了,战线拉太长现在结束了没有心情注意格式和内容正确了,后面有空再修一下吧。学这部分内容相对比较枯燥,总体还是好理解了,算是自己的java基础多添加了一块砖。希望后面赶上进度。
v1.5.2