Java反序列化之RMI(一)

admin 2024年12月18日22:39:52Java反序列化之RMI(一)已关闭评论7 views字数 3764阅读12分32秒阅读模式

Java RMI

0x01 RMI简介

RMI(Remote Method Invocation)远程方法调用,一种允许程序跨JVM调用对象的思想。

调用方法即会涉及到参数传递,在Java中,如果我们想完整的在网络中传递一个对象,通常会用到序列化,来安全的、完整的传递一个Java类对象。

在JVM之间通信时,RMI对本地和远程对象的处理方式是不同的,RMI并不是将远程对象复制一份传递给客户端,而是引入了两个概念,分别是StubSkeleton

当Client试图调用一个远程的对象时,实际会调用的Client的代理类,这个代理类就是Stub,而在调用远程方法前,Server端也会注册一个远程代理类,这个代理类就是Skeleton

Stub对使用者是透明的,Client可以像调用本地方法一样直接通过它来调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务端主机地址等等,并实现了远程调用过程中具体的底层网络通信细节,因此StubsSkeletons的调用对于RMI服务的使用者来讲是隐藏的,我们无需主动调用RMI的相关方法。

Java反序列化之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.Registryjava.rmi.Naming

Registry

即注册中心。

创建注册中心

locateRegistry.createRegistry()

Naming

提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法。

该类的每个方法都有一个URL格式的参数,格式如下:

//host:port/name

Naming的方法

  • lookup(): 查询
  • bind(): 绑定
  • rebind(): 重新绑定
  • unbind(): 取消绑定
  • list(): 列表

naming其实就是一个对注册中心进行操作的类。

实例

需要被远程调用的接口ExecServer

Java反序列化之RMI(一)

接口的实现类ExecServerImpl

Java反序列化之RMI(一)

创建注册中心的服务端代码RMIServer

Java反序列化之RMI(一)

查找注册中心并调用Stub中存储的远程方法的客户端代码RMIClient

Java反序列化之RMI(一)

启动RMIServer,然后启动RMIClient

Java反序列化之RMI(一)

0x03 流程及原理

1、创建需要被调用的远程对象

Java反序列化之RMI(一)

首先在服务端的第一行,创建了远程对象execServerimpl,该对象是继承自UnicastRemoteObject,在初始化时,会调用UnicastServerRef#exportObject来创建execServerimpl这个远程对象。

进入exportObject方法,发现调用了Util.createProxy方法

Java反序列化之RMI(一)

继续跟进,在createProxy方法中,可以看见创建了一个InvocationHandler类也就是RemoteObjectInvacationHandler类var6,然后使用了动态代理,参数为var4、var5、var6,分别是var0的类加载器、所有接口的Class对象的数组、invocationHandler,被代理的对象是execServerimpl,也就是说这一步为我们创建的ExecServerimpl实现的ExecServer接口创建动态代理类

Java反序列化之RMI(一)

继续跟进,回到UnicastServerRef,在下方,new Target,并用该对象封装了刚刚生成的动态代理类var5,也就是Client_Stub

Java反序列化之RMI(一)

在下一行,调用了this.ref.exportObject(),这里的ref就是该类的父类UnicastRef的成员变量LiveRef对象

Java反序列化之RMI(一)

跟进该exportObject方法,会发现首先调用了LiveRef#exportObject,接着调用了TCPEndpoint#exportObject,对本地端口进行监听

Java反序列化之RMI(一)

接着会调用super(Transport).exportObject()

Java反序列化之RMI(一)

在该方法中,调用了ObjectTable.putTarget()

Java反序列化之RMI(一)

ObjectTable用来管理所有发布的服务实例Target,ObjectTable提供了根据ObjectEndpoint和Remote实例两种方式查找Target的方法

Java反序列化之RMI(一)

这里我们顺便看一下RemoteObjectInvacationHandler这个类,由于是动态代理类,关注的重点必然是invoke(),在invoke中,对被代理对象做了一个判断,如果调用的Object的方法,则会调用invokeObjectMethod()方法,否则则会调用invokeRemoteMethod()方法

Java反序列化之RMI(一)

而在invokeRemoteMethod中实际是委托RemoteRef的子类UnicastRef#invoke方法执行调用

UnicastRef#invoke方法是一个建立连接,执行调用,并读取结果并反序列化的过程。UnicastRef包含属性LiveRef,LiveRef类中的Endpoint,Channel封装了与网络通信相关的方法

Java反序列化之RMI(一)

真正的反序列化方法在unmarshalValue(),可以看出来会对除了8种基础类型的数据类型做反序列化处理。

Java反序列化之RMI(一)

2、创建注册中心

在实例中,我们用于创建注册中心的代码如下:

Java反序列化之RMI(一)

调用了LocateRegistry.createRegistry()

Java反序列化之RMI(一)

createRegistry()中,实际是new了一个RegistryImpl对象

所以我们跟进一下RegistryImpl的构造方法

Java反序列化之RMI(一)

在构造函数中,new了两个对象:LiveRefUnicastServerRef,var1则是通过createRegistry()传入的端口

最后调用了setup()方法

跟入该方法

Java反序列化之RMI(一)

该方法中,调用了UnicasetServerRef.exportObject()来创建一个RegistryImpl

exporotObject则与第一步中创建动态代理类的步骤类似

Java反序列化之RMI(一)

创建完代理类后,会通过setSkeleton()来创建服务端Skeleton

Java反序列化之RMI(一)

在该方法中调用了createSkeleton()来创建

Java反序列化之RMI(一)

在该方法中,通过反射加载RegistryImpl_Skel进内存,并返回实例化的Skeleton对象

创建注册中心大致就是这样,可以看出以创建远程对象类似

3、绑定服务

Server与Registry在同一端

最终调用registryImpl#bind()进行绑定

Java反序列化之RMI(一)

Server与Registry在不同端

通过Naming.bind()Registry.bind()

Java反序列化之RMI(一)

最终都需要通过调用LocateRegistry.getRegistry()来创建Registry,与创建注册中心的过程类似

Java反序列化之RMI(一)

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值,如果找到了,则反序列化客户端传来的参数,并反射调用。

Java反序列化之RMI(一)

调用的结果在返回给客户端,客户端在进行反序列化,完成整个过程。

关于原理就是上述这样,不是太详细,建议大家自己跟调一下,这里放一张su18师傅博客的图片,这部分原理建议深入理解,有助于搞清楚漏洞攻击方法和原理。

Java反序列化之RMI(一)

在该图中,我们也可以比较直观的发现,所有的通信流程均通过反序列化实现,所以为什么说RMI是基于100%的反序列化的,而且在三个端点中,都有反序列化的操作,因此针对三个端点,都有攻击的可能,后续为大家展示不同端点的攻击方法以及JEP290&bypass的相关知识。

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年12月18日22:39:52
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java反序列化之RMI(一)https://cn-sec.com/archives/638564.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.