Java RMI
0x01 RMI简介
RMI(Remote Method Invocation)远程方法调用,一种允许程序跨JVM
调用对象的思想。
调用方法即会涉及到参数传递,在Java中,如果我们想完整的在网络中传递一个对象,通常会用到序列化,来安全的、完整的传递一个Java类对象。
在JVM之间通信时,RMI对本地和远程对象的处理方式是不同的,RMI并不是将远程对象复制一份传递给客户端,而是引入了两个概念,分别是Stub
、Skeleton
。
当Client试图调用一个远程的对象时,实际会调用的Client的代理类,这个代理类就是Stub
,而在调用远程方法前,Server端也会注册一个远程代理类,这个代理类就是Skeleton
。
Stub
对使用者是透明的,Client
可以像调用本地方法一样直接通过它来调用远程方法。Stub
中包含了远程对象的定位信息,如Socket
端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,因此Stubs
、Skeletons
的调用对于RMI服务的使用者来讲是隐藏的,我们无需主动调用RMI的相关方法。
0x02 RMI实例
运行一个RMI实例,我们需要:
-
需要被调用的接口(Server): 这个接口必须继承 java.rmi.Remote
接口,所有方法都必须抛出java.rmi.RemoteException
异常 -
接口的实现类(ServerImpl): 需要提供一个构造函数并且抛出 RemoteException
异常,并且最好java.rmi.server.UnicastRemoteObject
类(后面会解释) -
注册中心: 在服务端创建并运行 -
客户端代码: 查找远程对象并调用方法
那么又是如何创建注册中心,如何查找并调用远程对象的呢?
Java为RMI引入了一个Registry
,使用注册表来查找远程对象的引用。好比手机上的"联系人",存储着联系人的名字和电话。我们想获取某人信息时(RMI),只需要在"联系人"(Registry)上通过名字(Name)找到电话号码(Reference),并通过号码找到人。
上述思想的实现即是java.rmi.registry.Registry
和java.rmi.Naming
Registry
即注册中心。
创建注册中心
locateRegistry.createRegistry()
Naming
提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法。
该类的每个方法都有一个URL格式的参数,格式如下:
//host:port/name
Naming的方法
-
lookup(): 查询 -
bind(): 绑定 -
rebind(): 重新绑定 -
unbind(): 取消绑定 -
list(): 列表
naming其实就是一个对注册中心进行操作的类。
实例
需要被远程调用的接口ExecServer
接口的实现类ExecServerImpl
创建注册中心的服务端代码RMIServer
查找注册中心并调用Stub中存储的远程方法的客户端代码RMIClient
启动RMIServer
,然后启动RMIClient
0x03 流程及原理
1、创建需要被调用的远程对象
首先在服务端的第一行,创建了远程对象execServerimpl
,该对象是继承自UnicastRemoteObject
,在初始化时,会调用UnicastServerRef#exportObject
来创建execServerimpl
这个远程对象。
进入exportObject
方法,发现调用了Util.createProxy
方法
继续跟进,在createProxy
方法中,可以看见创建了一个InvocationHandler
类也就是RemoteObjectInvacationHandler
类var6,然后使用了动态代理,参数为var4、var5、var6,分别是var0的类加载器、所有接口的Class对象的数组、invocationHandler,被代理的对象是execServerimpl
,也就是说这一步为我们创建的ExecServerimpl
实现的ExecServer
接口创建动态代理类
继续跟进,回到UnicastServerRef
,在下方,new Target,并用该对象封装了刚刚生成的动态代理类var5,也就是Client_Stub
在下一行,调用了this.ref.exportObject()
,这里的ref就是该类的父类UnicastRef
的成员变量LiveRef
对象
跟进该exportObject
方法,会发现首先调用了LiveRef#exportObject
,接着调用了TCPEndpoint#exportObject
,对本地端口进行监听
接着会调用super(Transport).exportObject()
在该方法中,调用了ObjectTable.putTarget()
ObjectTable用来管理所有发布的服务实例Target,ObjectTable提供了根据ObjectEndpoint和Remote实例两种方式查找Target的方法
这里我们顺便看一下RemoteObjectInvacationHandler
这个类,由于是动态代理类,关注的重点必然是invoke()
,在invoke中,对被代理对象做了一个判断,如果调用的Object的方法,则会调用invokeObjectMethod()
方法,否则则会调用invokeRemoteMethod()
方法
而在invokeRemoteMethod
中实际是委托RemoteRef
的子类UnicastRef#invoke
方法执行调用
UnicastRef#invoke
方法是一个建立连接,执行调用,并读取结果并反序列化的过程。UnicastRef包含属性LiveRef,LiveRef类中的Endpoint,Channel封装了与网络通信相关的方法
真正的反序列化方法在unmarshalValue()
,可以看出来会对除了8种基础类型的数据类型做反序列化处理。
2、创建注册中心
在实例中,我们用于创建注册中心的代码如下:
调用了LocateRegistry.createRegistry()
在createRegistry()
中,实际是new了一个RegistryImpl
对象
所以我们跟进一下RegistryImpl
的构造方法
在构造函数中,new了两个对象:LiveRef
和UnicastServerRef
,var1则是通过createRegistry()
传入的端口
最后调用了setup()
方法
跟入该方法
该方法中,调用了UnicasetServerRef.exportObject()
来创建一个RegistryImpl
exporotObject
则与第一步中创建动态代理类的步骤类似
创建完代理类后,会通过setSkeleton()
来创建服务端Skeleton
在该方法中调用了createSkeleton()
来创建
在该方法中,通过反射加载RegistryImpl_Skel
进内存,并返回实例化的Skeleton
对象
创建注册中心大致就是这样,可以看出以创建远程对象类似
3、绑定服务
Server与Registry在同一端
最终调用registryImpl#bind()
进行绑定
Server与Registry在不同端
通过Naming.bind()
与Registry.bind()
最终都需要通过调用LocateRegistry.getRegistry()来创建Registry,与创建注册中心的过程类似
4、查找服务
查找服务就是指客户端查找到注册中心,并对注册中心进行操作
在Client端使用LocateRegistry.getRegistry()
方法查找注册中心
5、远程调用对象方法
在客户端调用lookup方法时,会向Registry端传递序列化的Name,然后将Regisyry端回传的结果反序列化
在Registry端,则是调用Registry_Skel#dispatch()
,然后调用Registry#lookup()
最后将查询到的结果序列化
Client在拿到Registry返回的序列化结果后,将其反序列化,对其进行调用,注意这里是通过动态代理让RemoteRef#invoke()
方法进行远程通信,由于这个动态代理类保存了Server端的监听端口,因此在宏观上看,像是Client与Server端直接通信。
Server接受请求后,由UnicasetServerRef#dispatch()
来处理,在hashToMethod_Map()
中寻找Client端对象执行Method的hash值,如果找到了,则反序列化客户端传来的参数,并反射调用。
调用的结果在返回给客户端,客户端在进行反序列化,完成整个过程。
关于原理就是上述这样,不是太详细,建议大家自己跟调一下,这里放一张su18师傅博客的图片,这部分原理建议深入理解,有助于搞清楚漏洞攻击方法和原理。
在该图中,我们也可以比较直观的发现,所有的通信流程均通过反序列化实现,所以为什么说RMI是基于100%的反序列化的,而且在三个端点中,都有反序列化的操作,因此针对三个端点,都有攻击的可能,后续为大家展示不同端点的攻击方法以及JEP290&bypass的相关知识。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论