RMI
RMI全称是Remote Method Invocation,远程方法调用。这种思想其实是和C语言的RPC类似的。在这里的RMI是Java独有的一种机制,也就是可以让某个java虚拟机上的对象调用另一个java虚拟机中对象上的方法。
具体思想就是让我们获取远程主机上对象的引用,我们调用这个引用对象,但实际方法的执行在远程服务端上。
RMI架构
从RMI设计角度来讲,基本分为三层架构来实现RMI,分别为RMI服务端,RMI客户端和RMI注册中心。(但是其实服务端和注册中心是可以放在一起的)
- Client-客户端:客户端调用服务端的方法
- Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端方法执行的结果
- Registry-注册中心
Stub 和 Skeleton:
RMI引入了两个概念,分别是Stubs(客户端存根)以及Skeletons(服务端骨架),当客户端(Client)试图调用一个远端的Object,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用服务端(Server)的目标类之前,也会经过一个对应的远端代理类,就是Skeleton,它从Stub中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeleton的调用对于 RMI服务的使用者来讲是隐藏的。
时序图如下:
还是比较清楚的。
基本流程学习
测试环境:
- JDK 8u411
调用对象如何定义
使用RMI,首先我们需要定义一个能够远程调用的接口,这个接口必须扩展java.rmi.Remote
接口,用于表示可以从非本地虚拟机调用其方法,所以远程调用的对象必须实现这个接口。其次这个接口的所有方法都必须声明抛出java.rmi.RemoteException
异常,如下实例:
|
|
随后就可以来创建这个远程接口的实现类,这个类通常会扩展java.rmi.server.UnicastRemoteObject
类,扩展此类后,RMI会自动将这个类 输出(export) 给远程想要调用它的 Client 端(注册中心?)。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。示例代码如下:
|
|
如果不让远程对象成为 UnicastRemoteObject 的子类,后面就需要自己手动使用UnicastRemoteObject的静态方法exportObject
来手动 export 对象。
如何调用
前面就已经创建好了可以被远程调用的对象。如何调用呢。Java RMI 涉及了一个 Registry 的思想,也就是我们可以使用注册表来查找一个远端对象的引用。
如何理解,可以将Registry理解为 RMI电话本,当我们想要在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称(Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。
这种电话本的思想,由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
方法获取了Registry接口的实现类,并调用相关方法进行实现的。
- java.rmi.registry.Registry:这个接口在RMI下有两个实现类,分别是RegistryImpl和RegistryImpl_Stub。后面再看。
我们通常使用LocateRegistry#createRegistry()
方法来创建注册中心,然后将待调用的类进行绑定:
|
|
客户端进行调用:
|
|
先运行RMIServer:
成功运行并绑定。再运行客户端RMIClient去调用对象的方法:
成功调用。
需要注意的是这里 RemoteInterface 接口在 Client/Server/Registry 均应该存在,一般 Registry 与 Server 在同一端上。
——————
其他特性说明
前面就是一个简单的远程调用通信。还需要知道RMI还可以使用动态类加载和安全管理器来安全传输Java类。
动态类加载
java.rmi.server.codebase:该属性表示一个或多个URL位置,可以从中下载本地(CLASSPATH)找不到的类,相当于一个代码库。
如果客户端在调用时,传递了一个可序列化对象(比如前面例子中sayName()方法我传进去一个类实例),这个对象在服务端不存在,则在服务端会抛出ClassNotFound 的异常,但是 RMI 支持动态类加载,如果设置了java.rmi.server.codebase
,则会尝试从其中的地址获取.class
并加载及反序列化。
客户端同样,如果得到一个客户端没有的class文件,就会从服务端提供的java.rmi.server.codebase
URL去加载类。
可使用 System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/");
进行设置,或使用启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:9999/"
进行指定。
安全设置
我们通过网络加载外部类并执行方法,必须要有一个安全管理器来进行管理,如果没有设置安全管理,则RMI不会动态加载任何类,通常使用如下代码:
|
|
管理器和管理策略相辅相成,所以还需要提供一个策略文件,里面配值允许哪些主机进行哪些操作,如下便是全部权限:
|
|
同样可以使用 -Djava.security.policy=rmi.policy
或 System.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString());
来进行设置。
源码分析
前面学习了大概的流程,现在来跟一下源码看具体实现。
服务注册
远程对象创建
在前面我们创建了一个远程对象 RemoteTest,继承了 UnicastRemoteObject类,这个类使用 JRMP 协议 export 远程对象,并获取与远程对象进行通信的 Stub。并在服务端创建过程中对这个类进行了初始化,对应下代码:
|
|
打断点跟一下这里的流程,在RemoteTest初始化时,由于远程对象继承了UnicastRemoteObject类,所以会调用这个类的构造方法:
这里的this就是我们要注册的远程对象(RemoteTest对象),然后就会调用UnicastRemoteObject类的exportObject()方法:
然后实例化了UnicastServerRef类并且又调用UnicastRemoteObject类的 exportObject方法。
- 先跟进这个UnicastServerRef类的实例:
这里又实例化了一个LiveRef类,这里的var1即前面的port(即0),再跟进LiveRef类的初始化:
又实例化了一个objID类,继续跟进:
也就是对objID类的填充。值分别为:
回到LiveRef类的构造方法,然后就会调用LiveRef类的构造方法:
然后就是对LiveRef类的填充:
在LiveRef类初始化完毕后,就会进行父类的初始化:
再跟进一下父类的实例:
也就是说现在将UnicastRef类的ref变量赋值为了一个LiveRef类的实例。最后回到UnicastServerRef类的构造方法:
也就是对这个类的填充,对这些类进行了实例化。
- 再跟进exportObject()方法
前面跟了UnicastServerRef类的初始化,再来看这里的exportObject()方法:
这里毫无疑问会进入if语句,这里的if语句简单来说就是会给这个RemoteTest类的“ref”设置为一个UnicastServerRef类实例?(说是使得远程对象能够在网络上被正常地定位和访问)。
然后就会调用sref(UnicastServerRef类实例)的exportObject()方法:
在这里看到一个createProxy()方法,和动态代理那里感觉很像,看一下这个方法的参数传递与源代码。
- 同样的先看一下这里的getClientRef()方法:
返回了一个UnicastRef类实例,前面有用到这个类(UnicastServerRef类初始化时调用的父类),继续跟进:
还是和前面的差不多,看了一下变量类型这些,还是将LiveRef类实例赋值给了这个变量(并且好像也是差不多的),其实在前面调用getClientRef()方法时就调用了this.ref
,但是由于本类UnicastServerRef没有这个变量,其实最终传进去的就是我们前面初始化时赋值的父类UnicastRef的ref变量。所以这里的目的应该就是获取填充了的UnicastRef类实例。
- 再回到原先的createProxy()方法:
然后就会调用到sun.rmi.server.Util
类下的createProxy()方法:
简单跟了一下这个源码中的方法:
- getRemoteClass():也就是判断var0的接口是否继承Remote.class,是的话就返回这个var0,毫无疑问我们的RemoteTest类接口是继承了的,所以这里的var3可以看作是等价于var0的。
然后继续看,到了if条件,var2即传参进来的forceStubUse变量,而这个变量在我们最开始对UnicastServerRef类进行初始化的时候就已经赋值为false了:
然后ignoreStubClasses默认为false,在java中 && 运算符的优先级比 || 的高,现在已知var2为false,!ignoreStubClasses
为true,现在重点看这个stubClassExists()方法:
简单来说就是判断 withoutStubs 变量是否存在var0这个key,不存在的话就会调用forName()方法去找xxx_Stub 这个类,找到了就返回true。很正常会进入到这个if语句,但同样的不存在RemoteTest_Stub这个类会进入到catch语句:
最后stubClassExists()方法返回了false。
——————
最后并不会进入if条件:
看源码,简单跟方法,大概如下:
- getRemoteInterfaces():简单来说就是获取实现了Remote.class接口及Remote.class接口。在这里获取到的也就是Remote.class和RemoteInterface.class.
后面的也就和动态代理大差不差了。代理类为RemoteObjectInvocationHandler类,委托类是UnicastRef类实例。
所以最后就是返回了一个Remote类型的RemoteObjectInvocationHandler代理对象。
所以说只要对这个代理对象调用方法就会到RemoteObjectInvoactionHandler类的invoke()方法。
回到UnicastServerRef类的export()方法,前面返回了一个Remote类型的代理对象,后面又实例化了一个Target类:
简单看一下这里的Target类初始化时传进去的参数:
-
var1:即RemoteTest类实例
-
this:也就是UnicastServerRef类实例。
-
var5:即前面的代理对象
-
this.ref.getObjID():
就是返回我们前面实例化LiveRef类时的id。
- var3:即最开始传入export()方法的false
简单看看赋值情况:
这里的Target对象也就是封装了我们的远程对象和生成的动态代理类。
然后调用了LiveRef类的exportObject()方法,传进去了刚实例化完的Target对象:
即:
这里的ep也就是前面分析的LiveRef实例是有过赋值的变量。
所以现在又会调用TCPEndpoint类的exportObject()方法:
然后就会调用到TCPTransport类的exportObject()方法:
这里的listen()方法就是为本地的stub(代理类)开启一个随机端口,然后本地监听这个端口。
然后调用了父类的export()方法,传进去了Target类实例,跟进一下:
这里主要就是将Target对象存放进ObjectTable类中,ObjectTable用来管理所有发布的服务实例Target,简单看一下这里的putTarget()方法:
这里就是放进了键值对,在这个类中的这两个objTable和implTable变量都是HashMap类实例。这里可以看到是向里面put进值了的,现在来看一下值的情况:
value有三个,分别是:
- 我们前面分析出来的代理类:
- 后面注册中心创建分析出来的RegistryImpl_Stub类:
- DGCImpl_Stub类,好像是与垃圾回收相关的,默认创建:
这里最开始学的时候是踩了一下坑的,我是直接用的流程学习的代码来分析的,注册中心创建的流程也是会调用到UnicastServerRef类的exportObject()方法的,所以其实也会进行一次这里的put操作,个人调试时死活不能进入super.exportObject()方法,直接打断点会断在注册中心调用的super.exportObject()方法中,那里就只会有两个,并且看不出来有代理对象,所以其实这里的学习调用可以直接用如下代码:
|
|
不要注册中心来调试。
(题外话结束)
————————
最后回到UnicastServerRef类的exportObject()方法,最终是返回了代理对象的:
大概远程创建对象的过程就是这样,注意这里是将远程对象和代理对象都放进了一个Target类实例,并且这里代理对象的委托类是UnicastRef类。
——————————
注册中心创建
对应如下代码:
|
|
同样的加断点跟一下过程:
实例化了一个RegistryImpl类实例,传进去了port为1099,跟进一下这里的构造方法:
稍微注意一下这里的bindings变量被赋值为了Hashtable类。
然后会进入else语句:
注意看这里的LiveRef类实例语句,传进去了一个id和我们传进去的port(1099),看一下这里的id变量的定义:
这里和前面远程对象创建的分析部分还是有点像的。所以这里是传入了ObjID类实例和port(1099),对LiveRef类的变量进行了赋值:
然后就会调用setup()方法:
- 先跟UnicastServerRef类的初始化:
所以这里还是将UnicastServerRef类的父类UnicastRef的ref变量赋值为了一个LiveRef类实例:
- 再跟调用的setup()方法,传进去了刚初始化完的UnicastServerRef类实例:
跟了一下,这里应该是将RegistryImpl类的父类RemoteServer的父类RemoteObject的ref变量赋值为了传进来的UnicastServerRef类实例(不确定,先看后面)。
然后又调用了UnicastServerRef类的exportObject()方法,只不过这里传入的是RegistryImpl类实例:
又到了前面分析过的exportObject()方法。
跟进这里的Util.createProxy()方法:
关键还是这个stubClassExists()方法:
这里找到了RegistryImpl_Stub这个Class对象?所以可以返回true。
所以会进入if语句,而不是上一次分析的进入else语句:
这里又调用了createStub()方法,分别传入了RegistryImpl类的class对象和参数传递的UnicastRef对象:
这里的代码逻辑还是挺好看的,也就是对RegistryImpl_Stub的class对象调用了newInstance()方法并传递了UnicastRef作为参数,所以这里的createStub()方法(即createProxy()方法)就是实例化了一个RegistryImpl_Stub类并返回它。 回到UnicastServerRef类的exportObject()方法:
不同点又来了,这里就会进入到这个if条件,前面返回的RegistryImpl_Stub类会赋值给var5,而RegistryImpl_Stub类的父类就是RemoteStub,自然会符合这个if条件语句的判定,跟进setSkeleton()方法,这里传入了RegistryImpl类实例:
变量中不存在key为RegistryImpl的Class对象,会进入这里的if条件:
然后调用Util类的createSkeleton()方法,还是将RegistryImpl类实例作为参数传递:
这里的getRemoteClass()方法前面分析过,所以这里可以直接将var1看作RegistryImpl类的Class对象。
所以这里的createSkeleton()方法就是实例化了一个RegistryImpl_Skel类。
即现在UnicastServerRef类的skel变量是RegistryImpl_Skel类实例:
setSkeleton()方法结束。
所以在注册中心板块这里,实现了RegistryImpl_Stub类的RegistryImpl_Skel类实例。
——————
回到UnicastServerRef类的exportObject()方法,后续还是封装进了Target类:
还是分别说一下这里的参数:
- var1:RegistryImpl类实例
- this:UnicastServerRef类实例(类包含skel这个变量)
- var5:RegistryImpl_Stub类实例
- this.ref.getObjID():返回前面的LiveRef中的id,前面也是提到过的:
- var3:前面传参时传进来的值,为true
然后调用LiveRef的export()方法,流程和前面差不多,直接到Transport类:
这里又将一个Target对象放进了ObjectTable中。所以现在是有变量中有两个键值对:
- 一个RegistryImpl_Stub类实例:
并且我们还能看到前面分析的RegistryImpl_Skel类实例:
- 一个默认创建的DGCImpl_Stub类实例:
正好与前面的远程对象创建那里相呼应,很好说明这里为什么是两个,前面为什么是三个。
注册中心与远程服务对象注册的大部分流程相同,差异在:
- 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
- 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)
服务注册
对应代码:
|
|
跟一下过程:
这里的parseURL()方法就是解析传进来的url,效果如图:
跟进一下这里的getRegistry()方法源码:
和流程学习那里的客户端获取注册表对象的代码基本相同,稍微注意一下。
然后调用到了RegistryImpl_Stub类的bind()方法(正好对应到注册中心创建):
this.ref 与前面注册中心分析时的RegistryImpl_Stub类的newInstance()传入的参数相对应。
所以这里调用UnicastRef类的newCall()方法是合情合理的,这里的newCall方法简单来说就是返回一个连接对象
所以这里就是建立连接(服务端和注册端建立)然后向流里writeObject数据。
然后会调用到UnicastRef类的invoke()方法来进行网络传输:
里面会调用到executeCall()方法,而executeCall()方法会调用readObject()方法反序列化来反序连接对象:
个人调试时没到这一步(稍微注意一下)
这里有网络数据的传输,必然会到注册中心进行代码调用,后续的在注册中心的调用可以参考后面的服务调用那里的注册中心的过程。
在那里反序列化传过去的值后成功绑定。
服务发现
前面已经基本分析完了服务端的代码,再来看一看客户端。
服务发现,就是获取注册中心并对其进行操作的过程,这里包含Server端和Client端两种。
对应代码:
|
|
先运行服务端,再打断点调试客户端。
进入lookup()方法:
这里的parseURL()方法同样还是一个解析的操作,最后效果如下:
getRegistry()方法也是同样的,起一个获取注册表的操作:
所以还是获取到了RegistryImpl_Stub类对象,然后调用了RegistryImpl_Stub类的lookup()方法:
在这里获取到连接后,会对这个连接序列化:
然后通过UnicastRef类的invoke()方法来传输这个var2,还是起一个网络传输的作用。
然后通过反序列化,来获取注册远程对象时创建的代理类:
最后返回了这个var20:
从图中可以看出就是我们之前构造的代理对象。
也就是说,这里在调用 其lookup 方法时,会向 Registry 端传递序列化的name,然后将 Registry 端返回的结果反序列化。
所以在客户端的代码中,我们获取到的o就是这个代理对象:
服务调用
在前面我们已经获取到了代理对象。现在再来分析一下过程是怎么样的。
客户端
既然已经获取到了代理对象,并且对代理对象调用了方法,对应如下代码:
|
|
所以现在会调用RemoteObjectInvocationHandler类的invoke()方法:
此时的参数情况:
还是挺符合预期的。
断点也刚好断在这里,也算是刚好说明sayHello()方法不是一个Object方法:
然后这里会调用invokeRemoteMethod()方法作为返回值,跟进这个invokeRemoteMethod()方法:
前面的if语句都没有进入,这里的ref就是前面创建动态代理时的UnicastRef类,所以现在会调用UnicastRef类的invoke()方法,简单说一下这里传递的参数:
- proxy:也就是代理对象
- method:也就是sayHello。
- args:null
- getMethodHash(method):获取方法的hash?序列化传过去?结果如下:
进入UnicastRef类的invoke()方法:
第一框内代码就是获取相关连接的方法等。
第二个框内的代码就是获取连接对象,对应var7,这个连接对象也是老朋友了。
继续看invoke()方法中的其他重要代码:
这里的marshalValue()方法就是进行序列化,对传入的参数进行序列化,可以简单理解为想要调用的方法名等的序列化。
后面又调用了executeCall()方法,这个代码逻辑和前面分析那次的差不多,也是一个网络传输的方法?
然后又调用了unmarshalValue()方法用来反序列化,和前面marshalValue()方法附近的代码对比,那里的序列化的操作,在这里则是反序列化的操作,跟进这里的unmarshalValue()方法,有一个反序列化操作:
所以这里的客户端有一个反序列化操作,这里的反序列化操作应该就是反序列化获取到服务端调用方法得到的返回值。
最后方法会返回var13,也就是var47:
看了一下参数,确实这里最后返回的就是sayHello()方法的return的值。
注册中心
这里就需要调试注册中心的代码,每次客户端或者服务端与注册中心交互的时候都会调用。
在注册端,由sun.rmi.transport.tcp.TCPTransport#handleMessages()
方法来处理请求,当服务传入rmi时,就是进入第一个switch/case语句:
这里会调用serviceCall()方法,并且传进去了一个var6变量,这里的var6就是之前说过的一个连接对象。
跟进这个serviceCall()方法:
但是我是class文件,源码不是很全,简单过一遍。
这里对ObjectTable类调用了getTarget()方法:
即方框中的代码大体逻辑就是从ObjectTable中获取封装的Target对象。
箭头指向的getImpl()方法就是获取其中的封装的RegistryImpl对象。
然后获取其中封装的UnicastServerRef对象,UnicastServerRef是Dispatcher接口的一个实现类:
然后调用了UnicastServerRef类的dispatch()方法:
这里的var37就是前面调用getImpl()方法得到的RegistyImpl类实例,var1就是传进来的连接对象。
继续跟进,如下:
skel就是前面分析过的RegistryImpl_Skel类,所以又会调用oldDispatch()方法:
这里可以看到又会调用RegistryImpl_Skel类的dispatch()方法(class文件简单分析一下):
里面涉及到了switch/case,
这里的0表示调用的是bind()方法:
(对应服务端调用的 bind 方法的代码)
重点的两个变量:
- var7:即传进来的连接对象
- var6:即传进来的RegistyImpl类实例
代码逻辑就比较好看懂了。
先对这个连接对象字节流反序列化以获取远程对象和RMI服务名称。
然后通过调用bind()方法来绑定RMI服务,然后跟进一下这里的bind()方法:
所以现在就进行了一次绑定操作,对应客户端代码调用的bind()方法绑定。
——————
这里case的2表示处理lookup方法的请求:
以此类推,1是处理list()方法,3是rebind()方法等。
服务端
再来看一下服务端是如何调用的,此时我们就需要调试服务端,启动客户端。
打断点于UnicastServerRef的dispatch()方法如下位置:
上图就已经可以看出这里的skel为null,不会调用oldDispatch()方法,也是判断是Registry端还是Server端的过程。继续往后面看:
看这里涉及到的几个参数:
先调用unmarshalParameters()方法反序列化了客户端传来的参数,然后调用了invoke实现了sayHello()方法的执行。然后再调用marshalValue()方法将数据(即调用方法后返回的值)序列化。
成功闭环。
简单说明一下这里的var42变量的调用:
调试跟进这个方法,进入到如下:
注意看此时l的变量,正好和前面客户端传入的method的hash值是相同的。
这一个点基本可以说明在UnicastServerRef的dispatch
方法中就是通过this.hashToMethod_Map
里的Method的hash来查找的。
总结
RMI 底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,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 远程方法调用结果。
DGC
Distributed Garbage Collection,分布式垃圾回收。
当RMI服务器返回一个对象到其客户端时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个RMI服务,就会伴随着DGC服务端的启动。
在前面源码分析时,就发现到objTable中有默认的DGCImpl对象:
回到ObjectTable的putTarget()方法,关键代码如下:
这里的dgclog是DGCImpl类的静态变量:
这里调用了这个静态变量,会对DGCImpl类进行类初始化,而DGCImpl类存在static静态代码块:
基本就能解释我为什么ObjectTable中会默认有DGCImpl对象了。
同时还创建了DGCImpl_Stub代理类和DGCImpl_Skel对象。
很熟悉,并且其实这里的命名规则和处理逻辑类似Registry对象。
Java提供了java.rmi.dgc.DGC接口,这个接口继承了Remote接口,定义了dirty和clean方法。
在RegistryImpl_Stub#lookup()方法中,前面我们都是分析到反序列化就没走了,但是这里在后续调用的done()方法中其实还有这个DGCImpl类的利用:
持续跟进,可以看到如下代码:
所以这里会创建DGCImpl_Stub类实例等。
DGC通信的处理类同样的是 DCGImpl_Skel 的dispatch()方法,还是对应的switch/case语句,如下:
case1:
可以看到这里调用了反序列化,然后调用调用dirty()方法,然后再序列化输出。
case0对应的clean()方法。
攻击 RMI
攻击Server端
恶意方法
远程方法的调用时机发生在服务端。当注册的远程对象上存在某个恶意方法,我们可以在客户端调用这个方法来攻击服务端。比如假设服务端有如下方法的代码:
客户端如下调用这个方法:
|
|
这样就能成功在服务端调用这个恶意方法。
(个人感觉利用点不高)
恶意服务参数
在Client 端获取到Server端创建的Stub后,会在本地调用这个Stub 并传递参数,Stub会序列化这个参数,并传递给Servre端。Server端会反序列化Client端传入的参数并进行调用,如果这个参数是Object类型的情况,Client端可以传给Server端任意的类,直接造成反序列化漏洞。
基本利用
就比如我前面给的远程调用对象RemoteTest.java文件,里面就有一个接收Object类型参数的方法:
所以我们可以在远程调用这个对象的sayName()方法时传进去一个反序列化payload。
以CC6为例,所以我们可以如下构造:
RMIClient.java
|
|
然后运行服务端和客户端,成功弹出计算机:
运行结果为:
|
|
其他利用
在前面,我们是保证Server端和Client端调用的服务接口是一样的,那么如果不一致如何利用。
所以我们可以尝试传递的是 Server 端能找到的参数是 HelloObject 的 Method 的 hash,但是传递的参数却不是 HelloObject 而是恶意的反序列化数据(可能是 Object或其他的类)呢?
答案是可以的,在 mogwailabs 的 [PPT](https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides Exploiting RMI Services.pdf) 中提出了以下 4 种方法:
- 通过网络代理,在流量层修改数据
- 自定义 “java.rmi” 包的代码,自行实现
- 字节码修改
- 使用 debugger
并且在 PPT 中还给出了 hook 点,那就是动态代理中使用的 RemoteObjectInvocationHandler 的 invokeRemoteMethod
方法。
Afant1 师傅使用了 Java Agent 的方式,在这篇文章里,0c0c0f 师傅使用了流量层的替换,在这篇文章里,有兴趣的师傅请自行查看。
最简单的debugger没复现出来,以后再说吧,学学其他的只有方法,学了java agent后就来把这个复现了。
利用方面:个人感觉其实还是挺广的,比如服务端要求只接收一个类型的参数,但是我就是要传反序列化的进去。大概就这样解决了。
总结就是 Server端的调用方法存在非基础类型的参数时,就可以被恶意 Client 端传入
动态类加载
在前面也说过,RMI的重要特性:动态类加载机制。就是当本地 ClassPath 中无法找到响应的类时,会在指定的codebase 里加载 class。这个特性在 6u45/7u21 之前都是默认开启的。
为了能够远程加载目标类,需要 Serrver 加载并配置 SecurityManager,并设置java.rmi.server.useCodebaseOnly=false
。
Server 端调用 UnicastServerRef 的dispatch
方法处理客户端请求,调用unmarshalParameters
方法反序列化客户端传来的参数。
这个方法的反序列化由MarshalInputStream类实现,跟进它的resolveClass方法:
这里通过readLocation()方法获取codebase地址。
然后需要满足useCodebaseOnly为false,然后才能传入codebase变量。
然后调用RMIClassLoader#loadClass()方法来加载类,实际上委托的是sun.rmi.server.LoaderHandler.loadClass方法。
方法中调用到了loadClassForName()方法
通过 Class.forName()
传入自定义类加载器 LoaderHandler$Loader
来从远程地址加载类。
而 LoaderHandler$Loader
是 URLClassLoader 的子类。
最后会调用到URLClassLoader.loadClass方法来加载类
因此 Client 端可以通过配置此项属性,并向 Server 端传递不存在的类,使 Server 端试图从 java.rmi.server.codebase
地址中远程加载恶意类而触发攻击。
替身攻击
当远程对象接收参数类型不再是Object,而是指定类型(远程对象类型)。
而攻击者希望使用 CC 链来反序列化,比如使用了一个入口点为HashMap的exp,那么攻击者在本地的环境中将HashMap重写,让HashMap继承注册的远程对象(RemoteObject)
(了解一下这个思路,其实和前面说的那个有点重合了。不多说)
攻击注册中心
对应的反序列化触发点在RegistryImpl_Skel类的dispatch()方法。当调用Naming.lookup()方法或bind()方法时,都会调用到这个dispatch()方法中,也就是对应的switch/case方法。
注册中心的基本方法:
- bind()
- list()
- lookup()
- rebind()
- unbind()
前面我们就分析了,在Client端或者Server端调用这些基本方法时,基本都会进入到这里的switch/case语句再次调用。
并且在这些基本方法中,有的还会反序列化RemoteCall字节流来获取远程对象和RMI服务名称,就有可能会触发反序列化攻击。
需要注意的是:一般Java远程访问注册中心做了限制,只有来源地址为本地才能调用bind、rebind、unbind方法。
bind/rebind
(一般这个可以归类为服务端攻击)
这两个方法附近都是调用了反序列化的。
所以这里使用这两个都是可以的。
而bind()方法参数需要一个Remote类型的对象:
如何满足呢,动态代理,这是因为当代理类proxy被反序列化时,被代理对象也会被反序列化,自然也会执行POC链。
操作点就很多了,在CC1中利用AnnotationInvocationHandler,它是InvocationHandler的子类,可以再次借用,来修改传入参数的类型。如下操作:
CC1+动态代理:
|
|
这里用的LazyMap链,同样的TransformedMap链也能用。
(但是有点小怪的就是不应该只有8u71以下才能弹吗,8u411也能弹了?)
unbind/lookup
这两个方法的利用也许有JDK版本要求?JDK8u411并没有看到反序列化,而JDK8u71却有反序列化操作。这里先用JDK8u71看看。
(虽然注册中心有限制,但是lookup()方法在客户端也是能用的)
在JDK8u71中,这两个方法也存在反序列化:
如果能控制var10,就能造成任意类反序列化。即现在需要控制这个var2,虽然这改成了8u71,但是过程差不多。
溯源一下参数,可以发现这里的var2就是StreamRemoteCall对象,也就是那个连接对象。
相关的传输过程:
这里就是调用的Invoke()方法来传输数据。
所以我们可以调用this.invoke方法来传输请求到注册中心。需要的参数如下:
先获取对应的RegistryImpl_Stub对象:
|
|
然后获取到这个ref变量,这个ref是Registryimpl_Stub类的父类的父类的变量,这个变量的赋值在前面注册中心创建那里是说明了的,如下代码获取:
|
|
这里获取到连接对象还需要获取到operations变量的值,如下:
|
|
然后直接拼接进恶意类即可,我这里使用CC6,注意点细节还是很好看懂的:
|
|
攻击Client端
如果攻击的目标作为Client端,也就是在Registry/Server端可控时,也是可以导致攻击的。客户端主要有两个交互行为,第一是从Registry端获取调用服务的Stub 并反序列化,第二步是调用服务后获取执行结果并反序列化。
有如下几个利用点:
- 恶意Server Stub:
Client端在Registry端lookup后拿到的在Server端在registry端注册的 代理对象并反序列化触发漏洞。
- 恶意Server 端返回值
就是Server端返回个iClient端恶意的返回值,Client端反序列化触发漏洞。
- 动态类加载:
同攻击Server端的动态类加载,Server端返回给Client端不存在的类,让Client端去codebase地址远程加载恶意类触发漏洞。
简单以Server端为例:
RemoteTest类代码:
|
|
接口哪些简单改改即可,然后客户端调用即可:
成功反序列化。
参考文章:
https://su18.org/post/rmi-attack/#%E4%B8%80-rmi-%E4%BB%8B%E7%BB%8D
https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
https://www.cnblogs.com/gaorenyusi/p/18329213