RMI基础
RMI的介绍文档:RMI应用概述(Java™教程 > RMI)
这里用到的环境是jdk8u65
RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind rebind unbind 这三个方法只能对 localhost 进行攻击,后续我们会提到。
RMI介绍
RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。
- 这个协议就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范。
所以我们等下起环境,尽量写两个项目,一个做服务端,一个做客户端
由于每个服务对应端口是动态的,所以引入registry注册端,提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
RMI实现
Server端
- 先写一个接口,定义一个方法
|
|
注意这里要继承Remote接口,然后抛出异常
- 然后写接口的实现类
|
|
继承 UnicastRemoteObject 类,用于生成 Stub(存根)和 Skeleton(骨架)。 这个在后续的通信原理当中会讲到
实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
- 然后写注册远程对象
|
|
这里端口默认1099,bind的绑定这里需要跟后面客户端查找的registry一致
Client端
客户端只需从从注册器中获取远程对象,然后调用方法即可。当然客户端还需要一个远程对象的接口,不然不知道获取回来的对象是什么类型的。
所以同样要先写一个接口,注意要跟server端的名字一样
|
|
然后客户端获取远程对象,然后调用方法
|
|
然后可以测试一下,先运行server端代码,开启1099端口接收,然后client端运行代码,转换大写的函数被调用,server端返回大写的Hello
从IDEA断点调试分析RMI原理
具体可以看这张图,来源是JAVA安全基础(四)– RMI机制-先知社区
创建远程服务
这里肯定是不存在漏洞的,存在漏洞的地方在端之间的交互,在前面的实例化这里下断点,把下面代码都注释了
一直强制步入,到达我们前下写的接口实现类RemoteObjImpl
的构造函数,现在我们要把它发布到网络上去,我们要分析的是它如何被发布到网络上去的
首先它是继承父类UnicastRemoteObject
,所以先进到父类的构造函数里
可以看到这里port值为0,也就是说他会把我们的服务发布到一个随机的端口上,不是前面说的默认端口1099,因为这个是注册中心的默认端口
然后一路F8步过到这里exportObject
方法
这个方法就是用来将远程服务发布到网络上的,之前我们在接口实现类的构造函数注释了一段代码
|
|
如果不能继承UnicastRemoteObject
类的话就要手动调用exportObject
函数
我们看看这个函数
第一个参数接受一个对象不多说明,第二个参数是new UnicastServerRef(port)
,很明显是处理我们的网络请求的类,我们继续跟进
可以发现它又新建了一个叫LiveRef
的类,继续跟进
这里有个技巧,可以点F7步入后选择LiveRef
跟进
一个构造函数,跟进this
这里来了三个参数,重点看第二个参数
TCPEndpoint
类显然是一个网络请求相关的类,查看构造函数
|
|
传入ip和端口,这里又调了this,我们回到前面继续跟进this
这里之前全是封装,到这里才有ip和port,所以记住这些数据都封装在LiveRef
里面
回到前面的new LiveRef(port)
,进super看父类构造函数
整个服务从始至终只会调用一个LiveRef
,然后一路步过,走到前面exportObject
函数
接下来就是一直在不同的类里面调用exportObject
函数
走到这里看到一个stub,由上面那个图,我们发现stub实际上是在客户端client的,这里服务端为什么用到了
其实是先在服务端创建stub传给注册中心,然后客户端获取stub,后面跟skeleton交互,传给服务端
我们继续跟进stub,步过走到createProxy
函数,跟进去看看
if判断这里是判断是否存在stub,后面会用到
我们继续看下面
一个动态代理的创建,然后继续跟回到上面,有个target把前面东西做了一个封装
然后跟进Target
这里其实可以看到
disp下面的是server端的LiveRef
和stub下面是client的LiveRef
是一样的,这样才能实现两个之间的通信
后面我们看看target封装之后的内容
我们看到这里又调用了exportObject
,把前面封装好的target传入
跟进这个函数,一路跟到这个部分
这个listen说白了就是开端口监听,跟进去看看
然后看到下面有个newServerSocket
函数,显然是创建了新的socket
然后开启一个新的线程,处理这些网络请求,回到上面我们查看target,发现port有数值了
其实前面的TCPEndpoint类里面的newServerSocket
方法就已经随机赋值了,如果前面listen的port为0,就随机赋值
然后这里走完,下面又走到一个exportObject
方法
跟进去,然后是一堆赋值,最后跟到这里
也是一个赋值,把前面存储在target里面的内容赋值给两个表
然后接下来一直步过,每个部分都返回值,整个流程就结束了
创建注册中心 + 绑定
这里在创建注册中心这里下断点,因为注册中心与服务端是独立的,谁先谁后无所谓,本质是一个东西
直接跟进去
这里调用了一个创建注册中心的方法,然后给默认端口传到一个类里面,我们继续跟
这里会走到奇怪的代码,我们先在RegistryImpl
类里面下断点,然后上面F8之后点恢复就能跟进去了
这部分if判断是检测端口是否为注册中心端口,然后做一个安全检查,这里直接步过,走到下面这里
这里看到很熟悉的LiveRef和UnicastServerRef,跟前面创建远程服务一个逻辑,可以看到这里的端口已经设置为1099,如果跟进会发现跟前面流程完全一致
调用exportObject的参数略有不同
这里是前面创建远程服务的
然后下面这个是注册中心的
可以发现第三个bool值不一样了,跟进去看看这个是什么
permanent,所以前面创建远程服务的是临时对象,而我们这里创建注册中心的是永久对象
接着流程相似,创建stub,这里的stub创建就与前面有不一样的地方了,首先还是走到createProxy里面
前面我们这个stubClassExists
没有分析,这里分析一下
其实就是在类名后面加上_Stub
,然后查看有没有这个类,有的话返回true
这里是RegistryImpl
类,所以查找的就是RegistryImpl_Stub
,显然这里是存在的
所以进了判断,之前创建远程服务,我们记得直接就没进判断,这里进createStub
函数
这里单纯就是获取了RegistryImpl_Stub
类,然后实例化了一下
我们走出去,看看走完这个流程的stub现在是什么
可以看到还是UnicastRef,跟前面创建远程服务的stub差不多,只不过前面的stub是先创建动态代理,然后把ref放进去,这里是直接反射,用forName
方法,直接把ref传进去
然后下面这里stub是定义好的,走到setSkeleton
函数这里,用来创建Skeleton
的,前面那个图里面也很清楚
然后跟进去
这里会走到createSkeleton
然后同样反射获取已有的RegistryImpl_Skel
类,走出来,我们可以看到skel被赋值了
然后走完这里,同样走到了target进行封装数据
这里跟进流程跟前面服务端一样,我们直接跟到target把值赋值给表的地方,我们查看这次封装了什么
跟到这里,然后查看static下的objTable,发现有三个表,至于为什么有三个表,后面会说
可以看到这里的skel是叫DGIImpl_Skel
,是一个垃圾回收机制有关的对象,这里的port为62173,是一个随机分配的端口
第二个表这里的skel没有赋值,然后端口跟上面随机赋值的结果一样
第三个表这里才是我们前下注册中心有关的,skel值为RegistryImpl_Skel,然后端口是1099
绑定
这里把前面代码的bind部分加上断点
然后跟进
本地进行一个检查,没必要跟了,全都通过,直接来到下一步,如果绑定了,就直接获取我们绑定的RemoteObj
,没有就put进去
客户端-客户端请求注册中心
首先在所有代码部分都下断点
首先是获取注册中心,我们跟进去看看,这里其实直接就是接收ip和端口新建了一个liveref,然后直接调动态代理,并没有通过序列化和反序列化的方式进行
这里跟进去发现其实跟前面注册中心创建stub的流程一样,只是接受了注册中心给的几个参数,并没有用到序列化和反序列化
这里我发现其实shift+F7的智能步入挺好用的,后面都用这个步入,不会走到奇怪的地方
然后接着就走到第二行代码,这里实际上是在获取服务端前面创建的动态代理,但是这里调试不了
因为这里是1.1版本的class文件,没办法下断点,我们直接看逻辑就行了
这里接着会走到newCall方法,也就是创建一个链接,没什么可看的
这里我们直接看lookup逻辑,这里的var1就是我们前面传入的Remoteobj
,然后他newCall完,执行了writeObject
,也就是序列化的过程,显然注册中心会有一个反序列化的过程
接着就是调用invoke方法,这里调用的是父类UnicastRef
这个类的方法
这里调用了一个executeCall方法,接着跟进去
跟到这个StreamRemoteCall
类,这里是真正实现网络请求的方法
处理完之后走出来
这里来了个readObject
,反序列化的地方,这里我们很明显知道,如果注册中心有恶意代码,这里经过反序列化就能攻击客户端
我们在前面的StreamRemoteCall
类那里下断点就能走了,不过我怎么步入都走不到这里,还是静态分析吧
往下看,看到这里又来了个readObject
这里在异常发生时,会进行反序列化,获取详细的信息,如果我们注册中心有恶意流的话,同样也会攻击客户端
由于这里这个executeCall方法是由invoke方法调用的,也就是说,类里面所有调用invoke方法的地方,都可能被攻击
就比如下面这个bind方法
虽然不像lookup一样直接反序列化,但是它调用了invoke方法,还是可能被攻击
客户端-客户端请求服务端
这里接着上面来调试,走到第三行代码,这里正常debug走不进去,要在RemoteObjectInvocationHandler
类下的 invoke()
方法的 if 判断里面打个断点,这样才能走进去。
后面我算是明白了,服务端的代码必须在后台运行着,这样才能走到里面去
事实上,是由于前面的代码是起了一个动态代理来调用方法,然后会走到调用处理器的invoke方法
然后走到
然后我们再走到invoke里面看看
走到这里调用了一个marshalValue方法,实际上就是序列化,参数就是我们前面传的hello
前面的序列化走完,往下看
又调用了executeCall方法,又是一个利用点,走完这个
我们前面的函数定义有返回值就会进到这个if判断里,这里有一个unmarshalValue方法,跟前面反着来的,显然是一个反序列化的函数
然后就走完了
注册中心-客户端请求注册中心
不多说直接上断点位置
然后服务端开调试,客户端运行一遍走到断点这里
然后往下走,调用disp的dispatch方法
跟进去
skel不为null,走到oldDispatch里面
然后走到skel的dispatch方法,这里我们看看Registry_Skel
的源码
这里有个switch选择
|
|
然后查看是否有readObject,最后只有list不行,其他都能反序列化,所以这里的利用点就是dispatch,用于客户端攻击注册中心
服务端-客户端请求服务端
这里接着上面来,然后按F9直到target这里的stub为proxy
按F9的过程我们会发现stub有三个,对应了之前的三个表
这里跟进dispatch我们发现skel为null了,进不了if判断,也就是进不了oldDispatch
然后一直步过
我们看到了熟悉的unmarshalValue方法,用于反序列化的,用于客户端打服务端的
DGC-客户端请求服务端
跟上面那个一样,前面我们F9的时候stub是变了三次,其中一次就是DGC
同样的流程debug
然后断点下在ObjectTable
类里面
然后在这里下个断点
然后走到createProxy里面
然后就是跟前面动态代理一个流程,判断加上_Stub
是否为true了,然后有这个类,接着创建stub,然后包装到target,实际上跟前面注册中心一样
接下来就是看看DGCImpl_Stub
和DGCImpl_Skel
了
同样都有invoke或者直接就有readObject,能被反序列化攻击,而且好处在于他只要远程服务创建它就会创建,而且服务端和客户端都能被攻击,也就是JRMP绕过
总结
如果是漏洞利用的话,单纯攻击 RMI 意义是不大的,因为在 jdk8u121 之后都基本修复完毕了。
RMI 多数的利用还是在后续的 fastjson,strust2 这种类型的攻击组合拳比较多