引言
关于Java RMI 的内容反反复复也是看了很多到了,它在我的日常工作中更多的是作为一个知识点而存在,时不时就得拿出来温习一下,时间久了就会被遗忘掉具体的细节。
但这个概念也算是Java漏洞利用中的一个明星概念以及有一定复杂度与难度的知识点了,也是面试中的常客(虽然已经很久没有参加面试了,不过我想应该是这样的,毕竟Java安全的东西说破天也就那么多)
RMI介绍
RMI 顾名思义即远程方法调用,用户通过一些指定查找远程服务器上的对象方法并实施调用,概念是很清楚的,但其实现的机制却不简单。
在Java的RMI实现中涉及到三方成员,即调用客户端,远程注册中心以及远程服务提供者。调用客户端很容易理解即RMI调用的发起者,远程服务提供者也不难理解即提供RMI服务的主体,
远程注册中心想必也并不复杂即远程服务提供者将其能够提供的服务注册到注册中心的注册表中供调用客户端进行查询。我们换个更加通俗一点的说法可能更易理解。
假如我是一个中间商,我提供的服务是联系消费者与供应商,我这里有一张表,这张表上记录了每个供应商拥有的商品的类型以及价格等信息,这些信息是我的核心商业秘密,一般消费者是无法获取到这些信息的。
你是一个小市民,当牛做马10多年终于凑够的房子的首付想要购入一套新房但是你不知道要向谁购买,你机缘巧合下找到了我这个中间商,我告诉你我这里有很多房地产商的联系方式,你只需要付给我10%房价的佣金(黑心)
我就把他们的联系方式告诉你,你虽然万般不愿但也没有更好的办法只能接受我的剥削,我们的交易最终达成,你拿着从我这里买来的联系方式找到房地产开发商购买商品房。
最终我们实现了三赢,你买到了房子,我赚了佣金,房地产开发商得到了现金流。
在这个例子中,你就是远程调用者,我就是注册中心,房地产开发商就是服务提供者,我们的关系也就是RMI中三个关键概念的关系。
RMI调用实例
首先创建一个远程对象,其需要继承UnicastRemoteObject
类并实现一个Remote
类的子接口。
继承UnicastRemoteObject
类是为了在创建该远程对象实例的时候能够调用到继承UnicastRemoteObject
的构造方法从而完成远程对象服务器的监听以及对象的导出,当然这都是后话,后面会进行详细介绍。
实现Remote
子接口是因为该接口起到一个标志作用,表明该实现类是可以被远程调用的。
而远程对象类为什么实现的是Remote
的子类而不是直接实现Remote
接口则是因为调用客户端在获得远程对象引用的存根对象时得到的是一个动态代理对象,该对象会被客户端映射为远程的对象直接调用其对应的方法,
而Remote
类默认是不存在这些方法的,所以我们需要一个接口先将这些方法进行生命以便调用客户端能够对获得的远程对象存根进行强制类型转换,这一点我们在客户端的创建代码中可以看到。
1 | import java.rmi.Remote; |
创建Remote
接口的子接口CustomRemote
该接口定义了方法sayHello
,该方法也就是可以被客户端调用的方法。
1 | import java.rmi.Remote; |
创建注册中心
1 | import java.rmi.AlreadyBoundException; |
创建调用客户端
1 | import java.rmi.NotBoundException; |
远程服务提供者是如何启动的
我们在 RMI调用实例 中提到过远程服务提供者需要继承UnicastRemoteObject
类,其目的是为了能够调用该类的构造方法从而完成远程对象服务器的监听以及对象导出,我们分析的起点也就是该类的构造方法。
此处调用了UnicastRemoteObject
的有参构造方法并传入了一个参数0
,该参数表示监听的端口,0
即由系统自动分配一个端口。
1 | protected UnicastRemoteObject() throws RemoteException |
接下来调用了exportObject
方法,顾名思义这是一个与远程对象导出有关的方法,其接收两个参数,第一个参数传入我们实例化的远程对象本身,第二个参数为端口。
1 | protected UnicastRemoteObject(int port) throws RemoteException |
exportObject
方法继续调用了其重载方法,该重载方法接受两个参数,第一个参数由外层方法进行原样传递,第二个参数被封装到UnicastServerRef
对象中。UnicastServerRef
的实例对象是对远程对象服务器的启动极为重要的一个类,包括对象导出的具体逻辑、接受消息的消息分发都与该类有关。
此处在进行实例化的时候主要作用是将port
参数封装到LiveRef
实例对象中,并将该LiveRef实例赋值给UnicastServerRef
的ref
成员变量。
在LiveRef
对象中则主要是通过port
参数创建了服务器监听的Endpoint
对象并实例化了一个ObjID
用于唯一标识正在创建的远程对象。
具体的代码就不放了,就是一些new以及赋值操作。
1 | public static Remote exportObject(Remote obj, int port) |
在重载的exportObject
方法中,首先会对输入的远程对象进行类型判断,如果其继承了UnicastRemoteObject
类则将刚才创建的单播服务引用UnicastServerRef
设置到该对象的ref
成员变量中。 然后会调用UnicastServerRef
的exportObject
方法。
1 | private static Remote exportObject(Remote obj, UnicastServerRef sref) |
在UnicastServerRef
的exportObject
方法中,会获取到正在创建的远程对象的Class
对象,然后调用Util.createProxy
方法创建一个当前正在创建的远程对象的动态代理对象。
然后判断该代理对象是否是RemoteStub
的子类,如果是则会为当前正在创建的远程对象创建一个存根。接着将获取到的代理对象封装到Target
对象中,并继续执行导出动作。
在进入到后面到导出操作前,这里我们需要重点关注动态代理对象的创建、存根对象的创建、Target对象的创建以及在导出后的方法hash计算的过程。
1 | public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { |
首先我们关注动态代理对象的创建过程,这里会调用Util.createProxy
方法,该方接受三个参数,第一个参数为当前正在创建的远程对象的Class
对象,第二个参数由getClientRef
方法获取,
该方法将当前UnicastServerRef
的ref
成员变量封装到一个UnicastRef
对象中,从名字中可以看出如果UnicastServerRef
表示服务端的引用那么UnicastRef
则表示客户端的引用,
第三个参数表示是否强制使用存根,默认为false
。
进入到createProxy
方法中,该方法首先调用getRemoteClass
方法获取传入参数的所有父类中直接实现了Remote
的子接口的类,这也是为什么我们前面不直接实现Remote
接口而是先创建其子接口再实现的原因。
在调用getRemoteClass
方法时我们传入的是BindObject
的Class
对象,因为BindObject
实现了Remote
的子接口,所以这里会直接返回BindObject
的Class
对象。
紧接着会遇到一个判断结构,这里重点关注stubClassExists
方法的运行结构,因为ignoreStubClasses
默认为false
,根据运算符的优先级and
会优先于or
执行,所以这里的判断结果取决于stubClassExists
的运算结果。stubClassExists
方法会判断传入的var3
的类名拼接_Stub
作为新的类名的类是否存在,若存在则返回true
,若不存在则返回false
。
此处我们传入的是自定义的类,很明显其存根类是不存在的,所以这里会返回false
,代码也会进入到else
的逻辑中。那么当var3
为什么时会进入到if
紧跟着的代码块中呢。
当var3
表示RegistryImpl
的Class
对象时会出现这样的情况,这将发生在注册中心的创建过程中,我们后面会介绍到的。
在else
紧跟着的代码块中,我们创建了UnicastRef
对象的动态代理对象,使用的invocationHandler
为 RemoteObjectInvocationHandler
,这一点是极为重要的,
因为在动态代理对象执行过程中invocationHandler
的invoke
方法会影响原本对象方法执行的行为,请记住这一点。
1 | public static Remote createProxy(Class<?> var0, RemoteRef var1, boolean var2) throws StubNotFoundException { |
对于用户创建的用户注册的远程对象来说,调用createProxy
方法后获得的是一个UnicastRef
的动态代理对象,该对象封装了一个LiveRef
对象,
而这个LiveRef
对象中又封装了一个ObjID
对象,这个ObjID
正是当前创建远程对象的唯一标识符,在后面的操作中调用客户端需要以来这个ObjID
来从导出表中查找对应的远程对象。setSkeleton
方法的调用需要特殊的时机,即createProxy
创建的存根对象实现了RemoteStub
类,很明显我们自行创建的远程对象的存根对象是没有实现这个类的,那么也只有前面提到的RegistryImpl_Stub
对象会实现这个类了。
在setSkeleton
方法中,其会创建RegistryImpl
的骨架,即创建RegistryImpl_Skel
对象并存储到UnicastServerRef
的skeleton
成员变量中。
1 | public void setSkeleton(Remote var1) throws RemoteException { |
BindObject
对象即是我们长在创建的远程对象,后面不在使用正在创建的远程对象
这个说法而直接使用BindObject
对象代替。
在进行Target
对象的封装时,首先会创建BindObject
对象的弱引用, 创建时传入的第二个参数是一个队列,当BindObject
对象被GC
回收时,会将弱引用对象加入到这个队列中,
通过对这个队列的监听可以定制某个对象被回收时的行为,这是与DGC
有关的内容了这里不进行详述,后面会再开一篇专门介绍RMI
中的DGC
。
传入的第二个参数为UnicastServerRef
对象本身,其此时作为消息的分发器被传递,后面当服务器接受到客户端的消息时将由该对象对消息进行分发与处理。
传入的第三个参数作为BindObject
对象的存根,其本身是一个UnicastRef
的动态代理对象。
传入的第四个参数是ObjID
对象,其表示远程对象的唯一标识符。
传入的第五个参数表示远程对象是否是持久的,如果为true
则表示远程对象是持久的,否则表示远程对象是临时的。我们自行创建的远程对象该参数均为false
,即不是持久的对象。
其作为弱引用被Target
对象引用,当没有其他对象引用时将被GC
回收掉,而持久存在的对象则只有系统创建的RegistryImpl
以及DGCImpl
对象。pinImpl
方法就是用来进行对象持久化的方法,其本质就是创建了一个使用=
号的赋值操作来对远程对象进行引用,只要Target
对象不被回收,该远程对象将一直存在。
1 | public Target(Remote var1, Dispatcher var2, Remote var3, ObjID var4, boolean var5) { |
尾巴处理完了,我们接着看对象的导出操作是什么样的。ep
以及transport
都是再LiveRef
实例化过程中创建的对象,需要了解的可以倒回去看看。
1 | public void exportObject(Target var1) throws RemoteException { |
下面就是最为核心的对象导出方法了,该方法包含重要的两个步骤,其一是完成serverSocket
服务的建立,其二是进行真正的对象导出。
1 | public void exportObject(Target var1) throws RemoteException { |
首先关注serverSocket
的建立。