java RMI反序列化-原理篇
java RMI反序列化-原理篇
前言:RMI 作为后续漏洞中最为基本的利用手段之一,学习的必要性非常之大。本文着重偏向于 RMI 通信原理的理解,如果只懂利用,就太脚本小子了。
这里有个坑点:就是 RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind rebind unbind 这三个方法只能对 localhost 进行攻击,后续我们会提到。
RMI 是什么?
Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。RMI 使用 JRMP(一种协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。
RMI 的架构
Stub和Skeleton
Stub(存根)和 Skeleton(骨架),当客户端试图调用一个远端对象,实际上会调用客户端本地的一个代理类,也就是 Stub。而在调用服务端的目标类之前,也会经过一个对应的代理类,也就是 Skeleton。它从 Stub 接收远程方法调用并将它们传递给对象。
RMI 实列元素
- Client:客户端,客户端调用服务端的方法
- Server:服务端,服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket
- Registry:注册中心,类比成 RMI 的电话薄。提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。
RMI 的实现
这张图非常详细的描述了 RMI 的过程,先是远程的服务端创建并注册远程对象,然后客户端再进行查找的的时候先会去注册中心进行查找,然后注册中心返回服务端远程对象的存根 然后调用远程对象方法时,客户端本地存根和服务端骨架进行通信,然后就是骨架代理进行方法调用并且再服务端进行执行,然后骨架又把结果返回给存根,最后存根把结果给客户端,更详细的图
一个RMI 实列
java. rmi. Remote 接口
java.rmi.Remote
接口用于标识可以从非本地虚拟机调用其方法的接口。任何作为远程对象的对象必须直接或者间接实现。只有那些远程接口(继承 java.rmi.Remote
接口)中指定的方法才可以远程使用。
java.rmi.server.UnicastRemoteObject类
RMI 提供了一些远程对象实现可以继承的便利类,这些类有助于远程对象的创建,其中包括java.rmi.server.UnicastRemoteObject类。这个类构造方法会调用exportObject或调用exportObject静态方法,它会返回远程对象代理类,也就是Stub。如果不继承该类可以手动调用其静态方法 exportObject
来手动 export 对象。
RMI Server
一、编写一个远程接口
远程接口要求:
- 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
- 同时需要继承Remote类,也就是需要实现java.rmi.Remote接口
- 接口的方法需要声明java.rmi.RemoteException报错
- 服务端实现这个远程接口
定义一个我们期望能够远程调用的接口,这个接口必须扩展 java.rmi.Remote
接口,用来远程调用的对象作为这个接口的实例,也将实现这个接口,为这个接口生成的代理(Stub)也是如此。这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException
异常,
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMIinter extends Remote {
public String hello() throws RemoteException;
}
二、编写一个实现了这个远程接口的实现类
实现类要求:
- 实现远程接口
- 继承UnicastRemoteObject类(具体效果上面有说)
- 构造函数需要抛出一个RemoteException错误
- 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
- 注册远程对象
package org.example;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMIobj extends UnicastRemoteObject implements RMIinter {
protected RMIobj() throws RemoteException {
super();
}
public String hello() throws RemoteException {
System.out.println("hello()被调用");
return "gaoren";
}
}
现在被调用的对象就创建好了,接下来就是如何实现调用了。
RMI Registry
在上面的流程图不难看到还有个Registry 的思想(不再解释),这种思想主要由 java.rmi.registry.Registry
和 java.rmi.Naming
来实现。
1、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 接口的实现类,并调用其相关方法进行实现的。
2、java.rmi.registry.Registry
这个接口在 RMI 下有两个实现类,分别是 RegistryImpl_Skel
以及 RegistryImpl_Stub
。
我们通常使用 LocateRegistry#createRegistry()
方法来创建注册中心
package org.example;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class Registry {
public static void main(String args[])throws Exception {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
// 创建远程对象
RMIinter rmiobj = new RMIobj();
// 绑定远程对象
Naming.bind("rmi://localhost:1099/Hello", rmiobj);
}
}
RMI Client
客户端进行调用,向注册中心查询相应的Name,调用相应的远程方法
package org.example;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.Arrays;
public class cilent {
public static void main(String[] args)throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
System.out.println(Arrays.toString(registry.list()));
RMIinter stub = (RMIinter) registry.lookup("Hello");
System.out.println(stub.hello());
}
}
这里 RMIinter 接口在 Client/Server/Registry 均应该存在,只不过通常 Registry 与 Server 通常在同一端上。
首先需要启动服务端RMI服务,运行服务端代码。然后客户端请求远程方法,也就是运行客户端的代码 服务端
客户端
这样一次简单的远程调用就完成了,不难发现其实方法的执行是在服务端执行的。
源码分析
总共是 6 个互相通信过程+3 个创建过程。
服务注册
创建远程服务
关键代码:
RMIinter rmiobj = new RMIobj();
在这句代码下上断点然后开始调试,在到 UnicastRemoteObject
构造函数之前,会先调用其静态方法进行一些赋值,不过不影响不用管,直接到其构造函数
初始化时会调用 exportObject
方法,这里的 obj 是我们要注册的远程对象。
可以看到这里的 exportObject
方法是个静态函数,所以说如果没有继承 UnicastRemoteObject
类可与进行静态调用其方法。然后其参数 new 了一个 UnicastServerRef
类,跟进一手
又 new 了一个 LiveRef
类,继续跟进
this 就是调用其构造函数,然后 new 的那个就是个 id,跟进其构造函数,
三个参数,第二个参数就是处理的网络请求,把端口进行一通处理,继续跟进 this
可以看到刚刚的第二个参数就是 ip 和端口嘛,然后出去。
其实这里总的来说就是创建了个 LiveRef 类然后将其赋值给服务端和客户端。
出来后继续跟进 exportObject
方法
这里可以看到把刚刚的 liveref
赋值给了 sref
,也就是服务端。包含了我们的网络处理信息。
跟进 sref.exportObject
方法
可以看到这里出现了 stub
代理类,通过 createProxy
来创建的,跟进
先看下这里的三个参数
impClass 就是 stub 代理类,cilentRef 实质上还是之前创建的 ref。
继续看看到一处逻辑
最主要的是 stubClassExists(remoteClass),其值为真就调用 if 语句,跟进看看:
因为这里的 remoteClass 没有 remoteClass_Stub 所以返回 false,
那么就会进行动态代理的创建,用 RemoteObjectInvocationHandler
为UnicastRef
对象创建动态代理,最后会返回一个Remote类型的代理类,在调用代理类方法时,就会会调用 RemoteObjectInvocationHandler.invoke
方法,这个后面再议。
继续下一步,stub 创建好后其实可以看到里面最主要的还是 ref 部分。
再往下走,发现其创建了 target 类,
看其参数,也就是一个总封装,把前面那些对象什么的全部放了进去。
继续跟进看到调用了 ref.exportObject
,然后一直跟进
最后到了 TCPTransport.exportObject
方法,
listen()
里面就是涉及到网络请求的内容的,就是开启一个端口然后等待客户端连接进行操作。
可以看到就是已经开启端口了(默认的随机端口),然后又调用了父类的 exportObject 方法,
将Target
对象存放进ObjectTable
中,ObjectTable 用来管理所有发布的服务实例 Target。
其实总的来说涉及到的大多是网络通信的东西,就是把创建的 ref 赋值给服务端然后创建个 stub,在把前面那些对象全部封装进 target,然后在 TCPTransport 中对网络通信进行处理发布服务,最后把 target 放进 ObjectTable 中。
创建注册中心
LocateRegistry.createRegistry(1099);
跟进 createRegistry
函数,
new
了一个 RegistryImpl
对象,继续跟进,
看到这里和上面远程对象注册很像,都 new 了个 liveref 对象,这里就不在跟进了和上面是一样的。
继续走又创建了个 UnicastServerRef
对象,
就是个赋值,跟进 setup
方法里面,
调用了 uref.exportObject
方法,回顾上面的远程对象注册,是调用的 sref.exportObject
,
其实都是 UnicastServerRef
的 exportObject
方法,这里的 this 是 RegestryImpl
对象
继续跟进
又是创建 stub,不过稍有区别了,跟进
这里的 RometeClass
是 RegestryImpl
对象,由于存在 RegestryImpl_Stub
,所以会返回 true,执行 if 语句。
执行的 createStub
方法其实也就是创建了个 RegestryImpl_Stub
实列化对象,
最后回到 exportObject
方法,stub 代理类也就创建好了,对比和上面远程对象注册中的 stub 确实不一样,上面的 stub 是动态代理创建的。
然后又因为满足下面的 if 条件会执行 setSkeleton
方法
看到又是创建 skle 代理,其实和 stub 代理创建差别不大,实列化了 RegestryImpl_Skel
对象
最后又是创建了个 target 对象
也是把刚刚那些对象进行一个总封装,不过比起上面的远程对象注册多了个远程对象 Impl 中多了 skel 对象,并且 stub 对象也不一样了。
最后还是调用 putTarget
方法将其添加进 objecttable 中,可以看到多了一个 hashmap,11 是远程对象注册添加的,至于那个 2 是 DGC ,后面再说。
创建注册中心与创建远程服务的大部分流程相同,差异在:
- 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
- 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)
服务端绑定
关键代码,
Naming.bind("rmi://localhost:1099/Hello", rmiobj);
先是把两个参数进行处理,然后只把 name 被 obj 传入 registry.bind
中。
调用了 newCall
方法,(这里我这个类是 class 文件,没有源码无法正常调试,随便调调好了)
继续跟进,
看到熟悉的 ref 了,总之这个方法就是建立一个连接,然后继续看到会对其进行序列化,
然后执行了UnicastRef.invoke
方法,
跟进后在方法 executeCall
,在该方法中又对连接对象行了反序列化。
这个应该属于远程绑定,一般服务端和注册中心在一端可以直接执行如下命令进行绑定
java.rmi.registry.Registry r = LocateRegistry.createRegistry(1099);
r.bind("Hello", rmiobj);
这个 bind 就太好分析了,就不多说了。
服务发现
服务发现,就是获取注册中心并对其进行操作的过程,这里面包含 Server 端和 Client 端两种。如果是在 Server 端,我们希望在注册中心上绑定(bind)我们的服务,如果是 Client 端,我们希望在注册中心遍历(list)、查找(lookup)和调用服务。
相应代码:
RMIinter stub = (RMIinter) registry.lookup("Hello");
调用 lookup 方法,通过对应的 RMI_NAME
,获取远程对象接口
同样是个建立个连接,然后对 remoteCall 进行序列化。
又通过 UnicastRef.invoke
方法传输这个 remoteCall
,通过反序列化来获取注册远程对象时创建的代理类 stub。
最后return这个对象。
整个服务调用三个地方过程
上面 Client 拿到 Registry 端返回的动态代理对象并且反序列化后,对其进行调用,这看起来是本地进行调用,但实际上是动态代理的 RemoteObjectInvocationHandler 委托 RemoteRef 的 invoke 方法进行远程通信,由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信。
客户端
话不多说,直接看
stub 是个动态代理类,在其调用 hello()
方法的时候会直接调用到 handler 的 invoke 方法,
最后调用到了invokeRemoteMethod
函数
跟进,这里 ref 是 UnicastRef,会调用其 invoke 方法。
marshalValue()
就是进行序列化,是对传入的参数进行序列化,只是这里调用的 hello 方法是个无参方法。
然后继续看见调用了executeCall
函数。
刚刚上面服务注册不难看出里面可以进行反序列化,这里不在深入了,继续看这个 invoke 方法逻辑,发现如果方法有返回值还会调用 unmarshalValue
方法进行反序列化,
但是由于这里返回值是 string 型不符合条件会直接返回。
到此客户端的方法调用就结束了。
这里又两个反序列化的点,第一个就是直接的 stub 进行反序列化,第二个就是返回值进行反序列化。
注册中心
接下来继续看注册中心,要从 listen 哪里开始跟进,总之就是发布网络后处理一些请求的 JRMP
协议内容,最后调用到了 serviceCall
方法
可以对 objectTable 进行了个获取,看看 target 里是什么
就是注册中心的 stub 嘛,然后继续看发现其分发器 disp 里有 skel 代理类
在该函数最下面调用了 disp.dispatch 方法
然后继续看,skel 不是 null 满足条件执行 if 语句,调用 oldDispatch
方法,
在这个方法里面最后调用到了 skle 的 dispatch 方法
跟踪进入
由于这个类是 class 文件,就简单分析一下吧,有很多 case,然后这里客户端调用的是 lookup 方法是 2
就是查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
服务端
最后在看服务端是怎么处理的。
前面是差不多的就是在进行 skle 的条件判断的时候会是 false
不会执行 if 语句,继续向下走,
发现其会把在客户端进行序列化的参数进行反序列化(我这里方法没有参数无法进行调试)。
最后进行方法调用。
然后再把返回值进行序列化
至此就完美闭环了。
总结
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 服务器返回一个对象到其客户端(远程方法的调用方)时,其跟踪远程对象在客户机中的使用。当再没有更多的对客户机上远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。
在前面远程对象注册时调用 put 时发现,在还没有 put 进行封装 target 时,里面已经存在一个 target 了。
可以看见其 stub 是 DGCImpl_Stub
类,那这个是怎么创建的呢?可以看见在执行 putTarget
方法时有这么一串代码
因为这里的 dgclog 是个静态变量
在调用静态变量时会完成类的初始化,最后会创建代理类
从上面的 target 不难看出这里的 stub 创建更像注册中心中 stub 的创建,因为 DCGImpl 存在 DCGImpl_Stub,所以相同的还会创建 DGCImpl_Skel 对象。
Java提供了java.rmi.dgc.DGC
接口,这个接口继承了Remote接口,定义了dirty和clean方法
看到 dirty 方法调用了 UnicastRef.invoke 方法,
剩下的就是里面反序列化了。然后再看服务端的 DCGImpl_Skel 中
case1 或 2 就是对应的不同方法嘛,也是存在反序列化的。
参考:https://su18.org/post/rmi-attack/#2-攻击-registry-端
参考:https://nivi4.notion.site/Java-RMI-8eae42201b154ecc89455a480bcfc164
参考:https://xz.aliyun.com/t/9053?time__1311=n4%2BxnD0DuAiti%3DGkD9D0x05Sb%2BDOSYKaNTNaTek4D#toc-1