https://su18.org/post/rmi-attack/#%E9%9B%B6-%E5%89%8D%E8%A8%80
学完之后感觉 SU18 师傅这一段内容很精华,也算是对 RMI 的整个流程都概括进来了,想到要复习了可以先看看下面这段:
- RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
- Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
- RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
- Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
1.RMI 介绍
RMI (Remote Method Invocation) 英文翻译过来就是“远程方法调用”
是一种调用远程位置的对象来执行方法的思想,这听起来就很危险,而远程调用的思想其实在 C 语言中就有体现,RPC(Remote Procedure Calls),用来打包和传输数据结构。而在 java 中,远程传输通常都是传输一个对象,这个对象包括属性值和方法,传输的媒介往往都是 java 的序列化数据,结合动态类加载和安全管理器实现一个 java 类的传输
具体的实现思想就是让我们获取到远程主机上对象的引用,我们调用这个引用对象,但实际的执行在远程的位置上
为了简化网络通信,RMI 引入了两个概念 Stubs(客户端存根),Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就叫 Stub。而在我们最终调用远端(Server)的目标类之前,我们还会经过一个远端代理类,这个类就是 Skeleton,它会接受我们刚才想通过调用 Stub 去调用远端目标类的具体信息,并传递给真实的目标类
Stubs 和 Skeletons 的调用对于使用 RMI 服务的使用者来说是隐藏的,我们不需要去主动去调用相关的方法,但实际的客户端和服务端的网络通信都是通过 Stub 和 Skeleton 来实现的
这里可以看一下 SU18 师傅的整体调用时序图
服务端的远程对象大致介绍
使用 RMI 首先我们必须要定义一个我们期望能够调用的接口,这个接口必须继承java.rmi.Remote
接口,用来远程调用的对象将作为这个接口的实例,之后的 Stub 代理类也将实现这个接口,这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException
异常
定义完这个接口,我们还要来实现这个远程接口的实现类,这个类中的内容才是我们想要实现的逻辑代码,并且通常会扩展 java.rmi.server.UnicastRemoteObject
类,RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时提供一些基础的 equals/hashcode/toString 方法等,这里必须为这个类提供一个构造函数,并且抛出 RemoteException。
现在我们来实现一个服务端的可以被远程调用的对象
首先定义它的接口
1 | import java.rmi.Remote; |
然后来写可以被调用的远程对象,这里我们选择将其继承java.rmi.server.UnicastRemoteObject
1 | import java.rmi.RemoteException; |
注册中心的大致介绍
远程对象已经创建好,接下还需要实现注册中心(Registry),将我们的远程对象绑定上去。这个注册中心的本质可以想象成一个注册表,客户端通过查找这个注册表的键值对信息,调用到它想调用的对象和方法。
具体实现通过 java.rmi.registry.Registry
和java.rmi.Naming
来实现,我们分开来讲
java.rmi.Naming
他是一个 final 类,提供了在 Registry 中存储和获取远程对象引用的方法,这个类提供的每个方法都有一个 URL 格式的参数,格式一般是 //host:port/name
- host:表示注册机所在的主机
- port:表示注册表接受调用的端口号,默认是 1099
- name: 表示一个注册表中 Remote Object 的引用的名称,不能是注册表中的一些关键字
Naming 提供了查询(lookup),绑定(bind),重新绑定(rebind),解除绑定(unbind),列表(list),用来对注册表进行操作,也就是说,Naming 是一个用来对注册表进行操作的类,而这些方法的具体实现是通过LocateRegistry.getRegistry
方法获取了 java.rmi.registry.Registry
接口的实现类,并调用其相关方法进行实现的
那接下来就是java.rmi.registry.Registry
接口,他有两个实现类,分别是RegistryImpl
以及 RegistryImpl_Stub
,这两个实现类我们下面会讲到
通常情况下我们会调用LocateRegistry#createRegistry()
方法来进行注册中心的创建
下面我们将创建注册中心和绑定远程对象进注册中心的逻辑代码写在同一个 java 程序中,如果不这么做,按照顺序(创建注册中心->绑定远程对象->客户端调用远程对象)去一个一个执行 java 程序,在绑定远程对象时就会报错
之后我们还需要将远程对象绑定到注册中心上,而LocateRegistry#createRegistry()
本身 java 程序运行时不会保持创建状态,之后的Naming#bind()
自然也找不到注册中心,所以必须写在同一 java 程序中
1 | import java.rmi.Naming; |
客户端调用实现测试类
1 | import java.rmi.NotBoundException; |
先运行 服务端的注册中心以及远程对象的绑定,之后再启动客户端程序,打印如下结果
有两个值得注意的点:RemoteInterface
接口在 Client/Server/Registry 中均应该存在
当客户端在调用远程对象时,传递了一个可序列化的对象,如果这个对象在服务端不存在,则会在服务端直接抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了java.rmi.server.codebase
,则会尝试从其他地址中获取.class 文件加载,并反序列化
上面这个特性跟安全策略这个设置有关,RMI 通过网络加载外部类并执行方法,所以我们必须要有一个安全管理器来进行管理,如果没有设置安全管理,则 RMI 不会动态加载任何类
铺垫了这么多前置知识,接下来进入源码流程分析
2.源码分析
1.服务注册
0x01 远程对象创建
我们在创建远程对象的时候提到,必须要实现远程 Remote 接口,一般情况下也是要继承 UnicastRemoteObject 这个类的
我们在创建远程对象的时候提到,必须要实现远程 Remote 接口,一般情况下也是要继承 UnicastRemoteObject 这个类的,UnicastRemoteObject用JRMP协议来export远程对象,并获取与远程对象进行通信的stub,具体是什么意思呢?
我们看具体代码实现
首次实现初始化的时候,它会事先把port变量赋值好,然后直接调用exportObject
exportObject的内容是创建一个UnicastServerRef对象,然后调用其exportObject方法
这一段首先是trycatch块中的内容,它先是调用了sun.rmi.server.Util#createProxy()
创建了一个代理类
先是获取到了我们想要创建的远程对象类,然后进入一个if-else的判断,这里的话我们首先要回忆一下什么是Stub?Stub其实就是用来实现网络通信的动态代理类。if块里面创建的Stub是它RMI包中自带的功能Stub,并非我们自己想创建的Stub,这里自定Stub是在else里面的内容。RemoteObjectInvocationHandler
来为RemoteObject实现RemoteInterface接口创建动态代理,真正实现Stub的功能
之后返回这个自定义Stub
然后创建一个Target对象,这个Target对象封装了我们的远程执行方法,和刚才生成的动态代理类
然后调用LiveRef的exportObject,这个LiveRef是UnicastRef(UnicastServerRef的父类)自带的成员属性 LiveRef
这个exportObject调用到了TCPEndpoint的exportObject方法
这里主要干了两件事:首先是对本地端口进行了监听,然后就是调用父类的exportObject,将我们刚才生成的Target 注册进ObjectTable中,这个ObjectTable用来管理所有发布的服务实例 Target,ObjectTable根据 ObjectEndpoint和Remote实例两种方法来查找Target的方法
上述流程可以用图来总结一下:
流程下来比较感兴趣的是InvocationHandler动态代理的部分,它继承了RemoteObject实现了InvocationHandler,那么它一定是一个可序列化并且可以通过 RMI 进行远程传输的动态代理类,既然是动态代理类,自然关注其 invoke 方法
三层判断,首先判断是不是代理类,如果不是就直接 throw 走了,然后判断了一下是否是 Object,如果是直接调用 invokeObjectMethod,然后判断是否是 RemoteObject,如果是就直接调用 invokeRemoteMethod 方法。
实际上我们跟进invokeRemoteMethod 方法,它的 invoke 调用的是 RemoteRef 的 invoke 方法,RemoteRef 的 invoke 方法
这里面的 invoke 方法经由 UnicastRef 实现,这里的内容主要是通过调用 LiveRef 中 Endpoint、Channel 相关方法来进行连接,获取到序列化数据(Var7 获取远程数据流, 进行一系列处理,最终获取到符合格式的序列化数据),之后调用 UnmarshaValue 来进行反序列化
调的还是原生的反序列化
到这里就是一处可利用点了,我们知道了在远程对象创建的时候,创建的远程代理类中的 invoke 方法会存在反序列化的点,最终 Sink 的地方是在 UnicastRef 的 invoke->unmarshalValue 方法
0x02 注册中心创建
注册中心的创建就是一个方法了LocateRegistry._createRegistry_(1099);
所以我们跟进 createRegistry 方法,看看它到底具体干了些什么
首先是实例化 Registry 接口的的实现类 RegistryImpl,继续跟进他的实例化方法
干了三件事:1.实例化创建 LiveRef 类,2.实例化创建 UnicastServerRef 类,3.用 setup 方法将两者进行配置**LiveRef**
主要是用来进行网络通信,获取信息和传递信息 **UnicastServerRef**
就是对与整个 RMI 传输信息流进行处理,算是一个 RMI 具体功能的实现类
而在 setup 方法中,依旧是调用 UnicastServerRef 的 exportObject 方法将远程对象给发布出去
但是这次的 exportObject 方法的流程有所不同
重要的还是 createProxy 方法,我们跟进
在创建 RegistryImpl 远程对象的动态代理类的时候,我们看第一个 if 的内容,var2 和 ingnoreStubClasses 的内容暂且不谈,只看方法 stubClassExists 的内容
由于 var0 此时是我们传进来的 RegistryImpl,它会在本地查找 RegistryImpl_Stub 到底有没有这个类,答案是肯定有的
由于是常见功能类,并且是 RMI 自带的一个部分, RegistryImpl_Stub 的本地类早已经被定义好了
这里找到之后会 return 一个 true,那我们的 if 判断的内容就进去了
进入这个 createStub,这里预知都能猜到应该就是直接 create 我们刚才检测得到的 RegistryImpl_Stub 了
直接反射调用即完成,我们可以看看 RegistryImpl_Stub 的大致结构
它完成了大部分的 Registry 注册中心应该完成的功能,且全部都是用反序列化的操作来完成的,就比如说 bind 方法,就是直接把远程对象的序列化数据绑定上去
小插曲,我们回到主线,返回的是一个 RemoteStub 类型的对象,不想我们之前发布自定义远程对象,返回的是 Remote 类型,这里也是由于返回的是 RemoteStub 对象,后面会进入创建 Skel 的方法
直接一路调用过来到 Util 的 createSkeleton 方法
发现其实就是获取到了 RegistryImpl_Skel 类名(本地肯定是存了的),然后进行反射调用,返回出去之后会被分配到 UnicastServerRef 的 this.skel
中
但是 RegistryImpl_Skel 中的 dispatch 方法它定了各项分发操作的具体内容,功能的实现也是通过序列化和反序列化实现的
之后的流程就是封装 Target 了,用来网络通信功能的 LiveRef,以及用来远程功能的 RegistryImpl_Stub 对象
然后调用 Target 的 exportObject 对象将其发布出去,之后的发布内容流程相似,不重复
注册中心的流程停在,思考一下刚才创建流程时有哪些攻击点?
- 创建动态代理的时候,得到的是本地的 RegistryImpl_Srub?它的每个方法都是通过序列化和反序列化结合才得到的
2.创建的 RegistryImpl_Skel 的 dispatch 功能是通过序列化和反序列化实现的
服务端该实现的几个基本功能点都实现完了,就是远程对象和远程服务中心,我们通过比较这两者的流程不难发现其实两者都必须通过创建远程代理对象来封装网络通信功能,最终都有一个 Target 对象,只不过里面封装的功能类的名称不一样,且具体功能也都有差异,因此创建流程也有所差异
接下来我们来分析注册中心是如何对远程对象进行操作的
2.注册中心服务功能
总计 5 个方法:bind,rebind,lookup,list,unbind,都是对远程对象进行使用
首先先看 bind 方法
0x01 服务注册-bind
我们通过服务端调用Naming
的 bind 方法,调用到了 RegistryImpl 的 bind 方法
这里的逻辑是,通过 bindings 这个 hashtable 类型的变量获取本地的远程对象,看看是否本地进行了存储,如果没有就直接调用 put,将我们刚才创建的远程对象给 put 进去。如果有,就 throw 一个报错。
不论你的注册中心和服务端在不在同一端的时候,下面这段方法流程是一样的,都是通过 LocateRegistry.getRegistry()
来获取到注册中心,到具体的 getRegistry 内容如下:
这里前面会获取一下我们想要的服务中心的 host,然后再来调用 getRegistry 方法。主要逻辑是:处理 host,然后实现功能类 LiveRef (网络通信),然后依然是将其封装进 UnicastRef,最终再放进 Util 的创建动态代理类的 createPorxy ,建一个新的动态代理类,return 出去之后调用这个动态代理类的 bind
那在不在同一端的区别在哪呢?我们看看 bind 方法到底有哪些方法实现了
这里对我们来说接触到的就是 RegistryImpl
和 RegistryImpl_Stub
,我们上面分析过,假如说我们本地注册中心和服务端同时实现,本地缓存是有RegistryImpl_Stub 这个类的,所以我们主要区别就在最后 Util 的 createProxy 中,产生的结果就是:在同一端,直接调用 RegistryImpl
,不在同一端,调用RegistryImpl_Stub
,那我们来看看两者的 bind 有啥区别
RegistryImpl_Stub:
RegistryImpl:
你会发现如果按照它们本来定义的内容,如果是本地同一端的RegistryImpl,它直接就从本地取了,不会说是还要像RegistryImpl_Stub通过序列化获取对象数据,调用 RemoteObjectInvocationHandler 动态代理的 invoke,触发反序列化操作
但实际上不论你注册中心和服务端在不在同一端,统一还是走RegistryImpl_Stub 的一系列的注册中心方法,这里我举个例子,比如说此时的服务端这么写,模拟服务端和注册中心在同一端
然后我们跳过创建的过程,直接看到底调用的是 RegistryImpl 还是RegistryImpl_Stub
发现还是RegistryImpl_Stub,因为它获取注册中心的方法 getRegistry 调的是各自本地的**LocateRegistry**
的getRegistry,各自本地都会存RegistryImpl_Stub 类,所以最终创建出来的都是RegistryImpl_Stub,比较粗暴(这里思考卡了好久。。。)
所以关于服务端要调用 bind 功能的逻辑,只需记住一句话:获取到远程注册中心功能类RegistryImpl(实际上是RegistryImpl_Stub),调用其对应的 bind 方法,完成远程对象绑定
那么**RegistryImpl_Stub **中的 bind 方法具体内容如何?我们上面初探了一下,大致是通过序列化写入相对应的对象序列化数据,之后就是调用 invoke 方法
这里具体的调用 UnicastRef 的 invoke 方法还有细节可以深挖
我们跟进到 UnicastRef 的 invoke 方法的重要部分
它这里操作是将原始类替换为了动态代理类
上述的过程是服务端用 Naming 去实现 bind 功能前后的内容,一句话总结就是获取到注册中心(这里以服务端和注册中心不在同一端为例),然后调用其注册中心功能实现类的 bind 方法向注册中心写入序列化数据,然后生成动态代理类。
- server 端调用
LocateRegistry.getRegistry()
开始获取注册中心 - 在本地创建一个 包含了具体通信地址、端口的
RegistryImpl_Stub
对象 - 通过调用这个本地的 RegistryImpl_Stub 对象的 bind/list… 等方法,来与 Registry 端进行通信
- RegistryImpl_Stub 的每个方法,都实际上调用了 RemoteRef 的 invoke 方法,进行了一次远程调用链接,向注册中心写入序列化数据
值得注意的是,在调用 invoke 的时候一般都是通过序列化和反序列化来实现数据传输的
下面就是看注册中心对这些序列化数据如何处理了,所以接下来我们跟进到注册中心的逻辑,具体入口点下图:
注册中心通过 sun.rmi.transport.tcp.TCPTransport#handleMessages
来处理请求,并且调用serviceCal
方法来处理serviceCall
首先是获取到 HashTable 里面的 Target 和 UnicastServerRef,然后再调用 UnicastServerRef 的 dispatch 方法,这里跟进 dispatch 方法
dispatch 方法我们之前只在 Skel(服务器骨架)中见过,其实根据它的翻译就能够大致了解它的用法:判断我们是要调用服务中心的哪个方法,然后分别给出具体的逻辑执行
回到 UnicastServerRef 本身的 dispatch 内容,它的主要逻辑是获取输入流,然后判断 skel 全局变量是否存在,我们回顾之前在远程对象和注册中心的创建时,如果是注册中心的代理类 create,它在创建完代理类之后会有一个如下的判断
意思就是如果我们当前是注册中心在创建动态代理类的话,就会给当前的 skel 变量赋值
所以上面的判断是在判断当前进程是服务端还是注册中心,那我们当前流程是注册中心在走,所以调用 OldDispatch 方法
oldDispatch 方法的内容前半段差不多,进行一些数据流获取和日志写入,这里还涉及到 DGCImpl 的内容,但是我们现在不跟进,之后再走。
最终调用 skel.dispatch 方法,也就是 RegistryImpl_Skel 的 dispatch 方法
在RegistryImpl_Skel 的 dispatch 中会根据数据流中写入的操作类型不同调用不同的逻辑,例如 0 就是代表 bind ,它的内容是从流中获取到对应数据,然后进行反序列化,然后调用 RegistryImpl(相对于RegistryImpl_Stub,是本地注册中心功能的实现类) 的 bind 方法
上面就是 调用 bind 方法时,注册中心和服务端各自干的事情了,其他的关于服务端和注册中心之间的方法-rebind 等,都可以参照这个流程
0x02 服务寻找-lookup
lookup 就是指客户端对注册中心的调用,寻找指定的远程对象了,这里其实跟 bind 方法的流程有很多相似的地方,客户端和服务端差不多,同样都是先获取到注册中心,当然这里也同样调用 LocateRegistry._getRegistry_
来获取,之后就是调用注册中心 RegistryImpl_Stub
的 lookup 方法了
首先是将 name 名序列化,然后将序列化数据传入数据流,之后调用 UnicastRef 的 invoke 方法与注册中心端实现网络通信,流程与之前无异
看 registry 端处理的逻辑,在 registryImpl_Skel 的 dispatch 中,lookup 的情况是 case 2
case2 这里的逻辑是将我们刚才的 name 名序列化数据反序列化,然后调用本地 registryImpl 的 lookup,接受返回结果之后再序列化将其写入数据流,然后返回给客户端
0x03 远程方法调用
client 客户端拿到 Registry 端返回的动态代理对象并且反序列化之后,下一步是调用这个远程对象的具体方法,看上去是本地调用,但接受到的远程代理对象是委托 RemoteRef 的 invoke 方法进行网络通信的,所以现在的 client 端是在直接和 server 端进行交互的,我们待会调用的时候也是直接服务端调用
服务端通过 UnicastServerRef 的 dispatch 来处理客户端的请求信息
主要逻辑如下:
首先获取数据流,然后在 hashToMethod_Map 中查找要执行方法的 hash 值对应的方法
如果找到了就会过 if 判断,打日志,之后反序列化参数,反射调用,invoke 与客户端通信将结果返回去
其实远程对象定位,然后调用远程对象方法流程只有后续的方法调用的逻辑不同,前面的寻找注册中心,以及中间的网络通信的逻辑都是大差不差的,所以我们分析 RMI 的流程可以分为 3 个大的部分:1.远程对象的创建 2.服务端对注册中心 3. 客户端对注册中心
原理流程总结
上面其实有很多问题可能当时没有解决,或者压根就没提,这里我码一下:
1.为什么会在远程对象创建的时候出现了 Stub 的初始化?
答:这里要讲明一点,Stub 是在 Server 端创建好之后,传输给 Registry,Client 再获取到的,具体逻辑见客户端getRegistry
获取注册中心
这里我想存一下 SU18 师傅的图片总结,总结的很精辟到位
我个人将上图划分:
- 远程对象的创建
- 注册中心的创建
- 获取到注册中心
- 从注册中心绑定、获取远程对象
- 远程调用
网络通信流程
这一部分的内容也是我上面一直没有提到的过程,现在补一下
入口点是 UnicastServerRef 的 ref.exprotObject 方法,也就是说我们不论是创建远程对象还是注册中心,都会经过这个步骤
这里我们以创建注册中心为例:LocateRegistry.createRegistry(1099);
直接跟进到第一层
这里的 ep 是 TCPEndpoint 类,也就是说我们之后跟进的是 TCPEndpoint 的 exportObject 方法
TCPEndpoint 的下一层是 TCPTransport 的 exportObject 方法
继续跟进
到这才算是真正实现网络通信的地方,也就是 listen 方法
listen 中的逻辑是先获取到 Endpoint,也就是我们需要发布出去的 Object 源地址,包括 host 以及端口信息,然后的 try-catch 块中是先新建一个 Socket 等待后续的连接,然后新建一个线程,如果接收到了连接的请求,就执行线程中的内容,我们继续跟进 New ThreadAction()中的 new AcceptLoop 的内容
run 里面继续跟进 executeAcceptLoop()
这里的内容就比较多了,他关键的一个步骤是会随机给我们发布出去的源地址分配一个端口号,这也是实现远程对象和注册中心(制定了端口就不会)的基本表现形式,也就是随机分配到一个端口上,能够访问到
这里全部执行完毕之后,我们的会多一个变量 port,如果这里是远程对象的发布,他会随机,但是我们这里跟进指定端口的注册中心的发布,所以是固定的 1099
发布成功之后就是将我们当前封装的 Target 存入一个 HashTable 中,因为之后的服务实现都必须要有网络代理类和相应的功能类,Target 中就封装了这些东西,到时候实现功能的时候的就是根据其键值来取
然后后面的流程就是服务端处理经由注册中心之手接收到的客户端发送过来的请求信息了,过程就不重复了
还有一个客户端通过 localRegistry 的 getRegistry 方法创建 Stub 的流程,这里简略记录一下:新开一个线程,开放一个网络端口的监听,接受到请求之后,会自动往下走线程的 Run 方法,里面调用了 Skel 的 dispatch,然后才能走服务端的处理逻辑,只不过这个 dispatch 进不去,只能静态调用
3.RMI 攻击流程分析
请注意,自 jdk8u121 之后单独关于 RMI 的漏洞攻击都基本修复完毕了,一般是与后面复习的漏洞进行组合拳攻击
其次,RMI 三端其实都存在相互攻击的可能,并且我个人感觉可以衍生出更多的方面,比如反制之类,这里仅讨论 RMI Client 对 Server 端,Client 对 Registry 端,Client 本身进行记录和 note
上面铺垫了一些基础知识,主要是 RMI 三个部分:客户端 注册中心 服务端的通信,以及各自的功能实现。 简单一点,其实就只有 Registry 端,以及使用 Registry 端的地方,因为 Registry 端本身的功能只负责传递和引用,相当于数据的中转站,它自己并没有任何的实际调用,所以我们才对注册服务的一端叫做服务端,使用服务的叫做客户端
还有一点,我们上面流程中,我们调用功能(bind,lookup 等)时,一个环节的结束,总是伴随着 writeObject,将结果的序列化数据写入数据流,整个过程的数据传输都是通过序列化数据实现的,那也就是说,我们三端都是存在攻击可能的
首先先看 Server 端
0x01 攻击对象为 Server
1x01 恶意参数攻击
该项攻击的前提是目标服务器上存在 CC 依赖或者 CB 依赖才能够进一步攻击扩展
如果是 Client 端对 Server 端进行攻击,单指远程方法调用这一块,可以回顾 2-0x03 段的内容,这里的重点是客户端传入的远程调用方法的参数进行反序列化,最终利用点在 UnicastServerRef 的 dispatch 方法的下面部分的代码, unmarshalValue 会对参数进行反序列化
所以这里我们可以传入恶意序列化数据,比如说 CC6 或者 CC11 等都是可以的
这里结合一下 Javassist 复习,写一个更加 恶意类更短的 CC6
先写恶意类的字节码
1 | import javassist.*; |
然后用最基本的 TemplateImpl 的 CC6,不过这里返回的是 HashMap 的对象,因为利用点— RMI 远程方法调用的参数首先会被 Stub 序列化,再传入 Server 端进行反序列化,省去了我们自己序列化的过程
1 | import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; |
然后开启 Server ,用 Cilent 去打
这种情况是只存在于我们攻击的参数类型为 Object 时才会成功,那假如说服务端设定的参数不是 Object,而是我们不知道的一个的参数类型呢?
假如说我们现在服务端所定义的远程对象所接受的参数类型为 HelloObject
然后我们客户端再打
很明显是不行的,具体报错如下
精确一点,这个报错是在说服务端无法找到对应的方法,因为我们所有的方法都是从 UnicastServerRef 的this.hashToMethod_Map
通过哈希键值对应得,而我们现在传过去的参数类型是 Object,与 HelloObject 的哈希值肯定不相等,所以不能成功调用,进入反序列化的过程
那么肯定是在服务端调用之前就被拦截了,具体 Hook 的点呢,是在RemoteObjectInvocationHandler
,整个客户端调到服务端的远程对象都是通过 Stub 远程代理类来做的,我们当时创建 Proxy 代理类的时候也是创建的,所以当我们的客户端与服务端通信的时候,都会经过相对应的代理方法走一遍
真正对 hash 值做处理的是在RemoteObjectInvocationHandler
的invokeRemoteMethod
中,getMethodHash 方法对我们的 method 进行了 hash 算取,这里就有很多可以操作的点了
- 通过网络代理,在流量层修改数据
- 自定义 “java.rmi” 包的代码,自行实现
- 字节码修改
- 使用 debugger
这里最简单的是直接 debugger 就行,调试的时候将类型改为 HelloObject,算取的 hash 就能够对应上了,其他方法还未复现,这里就不码了
https://www.anquanke.com/post/id/200860
https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg
感兴趣的师傅可以看看上面这两篇文章,高版本之后的很多防御点都是通过修改RemoteObjectInvocationHandler 实现的
1x02 动态类加载
之前讨论过,RMI 还存一种从指定 codebase 中加载任意类的功能,具体一点就是当 Client 端传入在 Server 端 ClassPath 找不到的类时,RMI 就会从另外一个指定的地方寻找,并加载
但是也有条件,并不是说默认开启的。1.Server 端必须加载和配置好 SecurityManager,2.java.rmi.server.useCodebaseOnly=false
必须开启 3.版本必须是 6u45/7u21 之前
具体的代码,我们还是从服务端的 UnicastServerRef 的 dispatch 看
依然还是反序列化参数的过程,我们进到默认的参数反序列化方法
这里的参数 var1 是我们指定的方法名,var2 则是 MarshalInputStream
当我们继续跟进 unmarshalValue 的时候,他调用的是 MarshalInputStream
待会反序列化的时候会调用 resolveClass 方法
这里的逻辑:首先调用 readLocation ,获取到 Codebase 的地址,然后获取到我们想要反序列化的类名,之后检测一下useCodebaseOnly
是否开启
然后就开始调用 RMIClassLoader
的 loadClass 方法,跟进
这里的话需要一直跟进到sun.rmi.server.LoaderHandler
的 loadClassForName
通过 Class.forName 来实现类加载,这里传入了自定的类加载器LoaderHandler$Loader
而这里的 Loader 也是 URLClassLoader 的子类
不论是 Client 端还是 Server 端,两端只要有一段配置了 java.rmi.server.codebase
那么 Client 就能够通过这个参数传递 Server 端不存在的类,pushServer 端去恶意 codebase 加载恶意类了
0x02 攻击对象为 Registry
1x01 客户端攻击 Registry
先看客户端,很明显的就是直接 lookup,然后经由注册中心的 RegistryImpl_Skel
的 dispatch 方法中关于 lookup 的处理逻辑
那么这里的反序列化的啥呢?是我们 lookup 参数中传入的指定远程对象的名称
就比如说这里的 Hello
那我们可以将序列化数据传入 lookup 的参数,只需注意这里必须是字符串参数才能接受
1x02 Server 攻击 Registry
Server 端与注册中心有很多交互的可能,bind rebind 等,回顾一下服务端对注册中心交互的流程,获取到注册中心对象,创建了 Stub 动态代理对象,给 Registry 端发送了信息,新线程开启,并且开始调入 UnicastServerRef 的 dispatch 方法,又由于此时是在注册中心端处理的逻辑,进入 olddispatch,调用 RegistryImpl_Skel 的 dispatch (可以往 2-2-0x01 复习一下流程)
有两处的反序列化操作,一个是绑定的键—-var7,一个是绑定的我们传入的远程对象,第一个是 String 类型,没有利用可能,只能看第二个参数的反序列化,必须继承自 Remote 接口,之后就可以直接打 CC 等
实现的话我们直接用 AnnotationInvocationHandler 来代理了 Remote 接口 即可
代码实现可以参考如下
1 | import java.lang.annotation.Target; |
这里的获取 CC6 的 hashmap 是通过new EvilClass().getEvil()实现的,与上面客户端攻击 Registry 的获取恶意对象是一样的代码,这里的处理方式不同,因为我们传入的恶意类必须是 Remote 类型 ,那就靠 AnnotationInvocationHandler 来 代理 Remote 接口满足要求
然后就是把申请完代理类之后的恶意类传入 bind 的第二个参数即可
实际上,当我们在 RegistryImpl_Skel 中查看每个方法的处理逻辑,不止 bind 方法一处直接 readObject 了,包括 bind rebind 等等方法的攻击方式都是大同小异的
请注意:攻击 Registry 端是存在 JDK 版本限制的,这里可以去看看白日梦组长 RMI 的视频,讲的很详细
0x03 攻击对象为 Client
我个人理解,常用于反制手段,因为刚才我们上面对于 Registry 端或者 Server 端都是首先考虑 Client 端发起攻击,也确实符合逻辑和实际情况,那既然三者之间的信息传输都是通过序列化和反序列化实现的,那就存在互相攻击的可能,Client 端同样
之前只提到过一点,就是客户端远程调用之后,服务端将序列化结果给传输到 Client 端的 Stub,然后进行反序列化进行结果的提取
具体调用逻辑与之前分析的相同,因为是动态代理类,直接进 Handler 的 invokeRemoteMethod 方法
然后进 UnicastRef 的 invoke 方法
将序列化数据传输给服务端经过处理后,会接受其结果,然后 UnmarShalValue 走一遍反序列化其结果
实现起来其实不太现实,一般情况要打 Client 也是将其看作 Server 端镜像着打,那么攻击方式也就是 Server 端攻击一样了
0x04 攻击对象为 DGC
DGC 是什么呢?中文翻译为分布式垃圾回收– Distributed Garbage Collection ,之前确实遇到过 DGCImpl 的创建过程,但是我们那个时候没有跟进流程,那他具体是用来干什么的呢?当我们 Server 端给 Client 返回一个远程对象供使用的时候,他会跟踪这个远程对象的调用,如果这个远程对象没有进一步的被调用,那么 Server 端 将会启用垃圾回收远程对象,每开启一个 RMI 服务,一定会伴随着 DGC 服务端的启动
我们看看具体的实现功能类和接口
首先是 java.rmi.dgc.DGC
接口,他只有两个方法,dirty 与 clean 方法
- 当客户端使用 Server 端的远程对象的时候,我们使用 dirty 来注册一个
- 当客户端不再使用远程对象时,就会调用 clean 来清除这个远程对象
他有两个实现类,一个是 DGCImpl,一个 DGCImpl_Stub
这里可能查找出来只有这两个,但其实还有一个 DGCImpl_Skel
它并没有继承自 DGC 这个接口,而是实现自 Skelton 接口
那 DGC 是什么时候创建的呢?
服务端在创建远程对象的时候,流程走到创建完集成类 UnicastServerRef 之后,开始调用它的 exportObject 方法,一直走到 TCPTransport 的 exportObject 方法这里
前面的流程就不讲了,看 try 块中的 super.exportObject
方法
跟进
上面 setExportedTransport 就不看了,跟进到 putTarget 方法
这里有一个 if 判断的内容,调用了 DGCImpl 的 dgclog 静态变量,之前有了解过,当调用一个类的静态变量时,他会自动完成类的实例化,我们跟进,实例化就不看了,当实例化完成时肯定会调用它的静态代码区
这里的创建逻辑有点像注册中心,不仅会创建动态代理的 Stub,还会 set 完 Skel,最后将代理类封装起来成为 Target 类,然后 put 进 ObjectTable 的 hash 键值对表
然后再看看具体的Skel
重点关注 dispatch
两个 case,一个是 clean,一个是 dirty,两者的作用我们上面已经提到了,我们只看关于两者是否存在可攻击点
两者其实都存在被攻击的可能