RMI
前言
Remote Procedure Call 远程过程调用
RMI服务是java提供的一种远程调用对象服务 调用对象可以像在调用本地对象一样
RMI架构
RMI服务总的来说有3个主要部分: 服务端 客户端 注册中心
服务端 (远程对象)
服务端主要用来提供远程对象
注册中心
服务端用来将对象绑定在注册表上
客户端
客户端通过注册表调用远程对象

stub与skeleton是RMI调用过程中的一个代理类
客户端实际通过stub去调用远程对象并返回结果给客户端
服务端实际通过skeleton去与stub进行远程的数据交流
RMI的实现
服务端
服务端实际有2部分组成 注册中心 远程对象
服务端要提供对象服务 需要先有一个接口
1 | //RMIInterface.java |
接口需要先继承一个Remote对象 说明他可以被作为Remote对象在远程调用中被使用 作用就像Serialized类 只是个说明而已
有了接口还需要一个实现接口的类
1 | //RMIInterfaceImpl.java |
现在接口实现类已经写好 可以写服务端了
1 | //RMIServer.java |
至此服务端写好了
客户端
1 | //RMIClient.java |
RMI攻击
RMI服务底层调用的是JRMP协议
RMI服务默认使用该协议传输数据
JRMP协议在传输数据的时候会自动的序列化对象 以便传输数据
那么这里就存在可以利用的漏洞了
RMI服务端攻击
如果一个RMI服务存在一个远程对象的方法可以接受一个对象参数
那么我们可以直接传入一个我们构造好的恶意类 当RMI服务端序列化该类的时候 就会执行我们恶意的代码
这里在实现类中增添了一个接口Object对象的参数

我们只需在Client上面传入一个cc1包装好的恶意类 即可实现攻击

这种方法需要远程对象接受一个Object参数 才能触发
但是如果是Hashmap或者Hashtable之类的 我们也可以将恶意类藏在这之中也可以实现攻击
反过来看 如果服务端的远程对象返回一个恶意类 客户端也一样的可能中招
Register攻击
如果远程对象方法没法被利用 可以直接攻击注册中心
我们可以构造一个Remote对象 并将其传给注册中心 注册中心也会自动的反序列化这个对象 这时就可能触发漏洞

客户端这里我通过向注册表发送一个对象绑定请求 要求将demo对象绑定为我们发送过去的类
这里发送的对象正是我们包装好的恶意类 一旦接收便会自动序列化 从而触发执行
注意:这里bind的对象一定要是Remote对象 所以我们在bind之前就包装了一个代理类 强行继承Remote接口

注册表中的其他几个方法也会触发反序列化
bind rebind 正如上面演示的一样 直接发送对象即可
bind unbind rebind 只能在本地调用 没啥用
RegistryImpl_Skel.dispatch中执行指定方法之后 又会调用RegistryImp的指定方法

然后会进入RegistryImpl的方法

除了lookup没有调用checkAccess 其他都会调用
RegistryImpl中的checkAccess判断当前客户端的ip是否与服务器一样

lookup 这个方法只能传字符串 该怎么触发序列化呢? 现在应该回到底层代码看看具体是个什么情况
RMI底层代码
RMI服务中有几个关键api
java.rmi.Remote

Remote接口仅仅作为一个说明 继承该接口的类 在后面的使用中可以作为远程对象
java.rmi.server.UnicastRemoteObject

UnicastRemoteObject类是实现将对象转化为一个远程对象的必要条件
按理来说 Remote接口的实现类应该要显式的继承这个UnicastRemoteObject类 它将自动完成转化

我们可以来看看这个父类的构造器干了什么事

它会自动的将这个实现类导出为远程对象代理类 所以我们不必显式的exportObject出一个远程对象代理类
RMI的动态类加载
RMI还可以使用动态类加载和安全管理器来安全传输Java类
java.rmi.server.codebase:该属性表示一个或多个URL位置,可以从中下载本地(CLASSPATH)找不到的类,相当于一个代码库。
对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的class文件,如果需要客户端正确调用这些子类中被重写的方法,客户端就需要从服务端提供的java.rmi.server.codebaseURL去加载类
对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebaseURL去加载对应的类。
客户端与服务端两边的java.rmi.server.codebaseURL都是互相传递的。
无论是客户端还是服务端要远程加载类,都需要满足:
- 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并配置java.security.policy
- 属性java.rmi.server.useCodebaseOnly的值必须为false
指定java.rmi.server.codebase
创建注册中心
跟进一下LocateRegistry.createRegistry方法

会实例化一个RegistryImpl对象

来到构造器当中

jdk源码中默认想要注册中心开在1099号上
但是我们这里是1234 直接就进入到else语句中

实例化LiveRef 并传入id与port

进入LiveRef 的构造器中 又会调用其他类型的构造器
进入到TCPEndpoint.getLocalEndpoint()当中

这里又会进入另一个getLocalEndpoint()

这里实际上底层获取服务器ip信息的方法
直接跳到方法的最后 看看具体参数

看到最后会返回我们的出口ip与刚刚绑定到port
到现在开始LiveRef的实例化

将刚刚获取到的一些信息都绑定在LiveRef上

现在LiveRef初始化就结束了

进入到setup中 会先实例化UnicastServerRef对象 并将刚刚的LiveRef传到其构造器当中

传入父类构造器中

只是简单赋值

进入setup中 会调用UnicastServerRef.exportObject
this就是RegistryImpl对象

此时开始构造代理对象

先进入getClientRef

又会调用UnicastRef的构造器
现在进入creatProxy

进入到stubClassExists方法中

检测内部是否有stub 返回false
然后回加载一个类 RegistryImpl_stub 返回true

现在开始创建Stub
Stub是之后要传给客户端用的代理类

这里又加载了一个类
var2 = sun.rmi.registry.RegistryImpl_Stub 和刚刚加载的类一样
var3获得这个stub的class类

最后返回这个RemoteStub对象

var5得到stub对象后调用setSkeleton

又会调用createSkeleton

这个createSkeleton和刚刚createStub没什么区别

最后在UnicastServerRef中 skel被赋值为刚刚创建的skeleton

创建Target实例

将之前创建的一些东西都存在这个Target对象中

然后又调用exportobject

又是经过多次的exportobject调用 现在开始监听指定端口了

上面会查找ip与port
接着进入newServerSocket





这里都是比较底层的调用 最终的结果就是在所有网络接口上监听1234端口

如果没有指定端口 会调用默认端口号


最终结果就是这个ServerSocket
到这里 基本就结束了
过程中创建了RegisterImpl_Stub RegisterImpl_Skeleton
UnicastServerRef类与UnicastRef类 起到底层网络处理的作用
注册中心服务 默认绑定在所有网络接口上的指定端口
上面出现的一个奇怪的ip按理来说应该是我们的本地局域网ip 但是我本地有个虚拟网卡服务 它自动获取ip的时候弄错了
所以默认情况下是绑定在本地局域网中的
当然也可以显式的修改
System.setProperty("java.rmi.server.hostname",RMIHOST
远程对象创建
跟进一下这个接口实现类的实例化 也就是远程对象实例化

调用父类的构造器

跳到这个构造器上

跟进一下这个exportobject

其实前面部分和注册中心没什么区别
关注这里


继续跟进creatProxy

返回一个接口实例对象

后面又会开启一个监听 因为在exportObject的时候 都是直接调用父类的的默认构造器 所以这里的port是一个随机的
这个服务就是远程对象服务

最终放回接口实现对象
简单看看Client查找注册中心与查找远程对象

调用其他的getRegistry


又是回到了这个createProxy

进入createStub


最后返回注册中心实现对象的stub存根

我们拿着这个stub去找远程对象
来到下面的查找远程对象

newCall一个远程请求 里面要放一下请求信息
然后在通过父类的ref.invoke去发送

executecall

这里releaseoutputStream会发送请求
最后又回到lookup中 并反序列化结果


done会返回一个DGC 垃圾处理
var23就是我们最后的对象
最后返回的是一个动态代理对象 通过这个代理对象去调用服务
如何利用lookup触发反序列化
来调试一下lookup方法

不难发现 当调用注册表的lookup时候 实际调用了RegisterImpl_Stub中的lookup方法
紧接着 他又会调用父类的ref中的newCall方法
newCall方法会创建一个RemoteCall
最终由父类的ref去invoke 传输上面序列化的数据 服务端收到数据会反序列化

那么如果 RemoteCall序列化的对象不是我们传入的字符串 而是一个恶意类 那么就可以达到触发反序列化漏洞的效果
但是lookup只能传入字符串 所以我们得伪造这个RemoteCall 然后去修改序列化的内容

首先要伪造RemoteCall
this也就是Registry_Stub我们有 主要是获取super.ref与operations
这2个也没什么难的 反射获取成员属性即可 然后setAccessible(true)就可以了
最后调用super.ref传输恶意类序列化数据就可以了
总的来说就是伪造一次请求 把我们的恶意类给传过去
1 | import sun.rmi.server.UnicastRef; |
1 | ● 0->bind |
我们看看服务端收到序列化数据是个什么情况
当收到数据的时候 是RegistryImpl_Skel.dispatch去处理
会进入一个switch-case语句 数字分别代表不同的方法
look up是2

很明显 当收到lookup请求 直接就是readobject()
就直接触发反序列化了

总结
客户端调用远程对象时将获取stub
调用具体方法的时候是stub将请求交UnicastRef UnicastRef又给RemoteRef 并调用RemoteCall 接着序列化 发送并交给Skeleton
Skeleton再将序列化数据 反序列化得到原始数据 通过具体的调用方法 执行相应的逻辑 并将结果 序列化发送
客户端接收并反序列化结果,获取远程对象的引用。
客户端后续对远程方法的调用,会由服务端通过反射执行实际实现类的方法。
服务端序列化方法执行结果返回,客户端反序列化并得到最终调用结果
服务端触发漏洞都源于RegistryImpl_Skeleton中readobject调用

客户端的序列化则来自RegistryImpl_Stub中的readobject调用

当动态加载类的时候 如果本地找不到这个类 会尝试去指定网站上下载
codebase = “http://127.0.0.1/eval.class“
java.rmi.server.useCodebaseOnly 这个系统属性是从 JDK 6u45、7u21 和 8u121 之后都默认设置为 true 只能加载本地.class
RMI主要可利用的是注册中心的lookup / Naming.lookup()
说些什么吧!