Java安全之RMI协议分析

时间:2024-01-27 22:47:26

Java安全之RMI协议分析

0x00 前言

在前面其实有讲到过RMI,但是只是简单描述了一下RMI反序列化漏洞的利用。但是RMI底层的实现以及原理等方面并没有去涉及到,以及RMI的各种攻击方式。在其他师傅们的文章中发现RMI的攻击方式很多。 所以在此去对RMI的底层做一个分析,后面再去对各种攻击方式去做一个了解。

0x01 底层协议概述

RPC

RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在,如TCP或UDP,为通信程序之间携带信息数据。在OSI网络通信模型中,RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。RPC采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。

小总结:

在最原始数据通讯中其实还是归根到TCP/UDP协议,但是自使用了RPC后可以不需要了解底层网络协议,但是底层还是通过TCP/UDP去进行网络调用的。 其实RPC也只是远程方法调用的统称,重点在于方法调用中。而RMI实现就是Java版的一个RPC实现。

JMX

JMS:Java 消息服务(Java Messaging Service) 是一种允许应用程序创建、发送、接受和读取消息的Java API。JMS 在其中扮演的角色与JDBC 很相似, JDBC 提供了一套用于访问各种不同关系数据库的公共APIJMS 也提供了独立于特定厂商的企业消息系统访问方式

JMS 的编程过程很简单,概括为:应用程序A 发送一条消息到消息服务器(也就是JMS Provider)的某个目的地(Destination),然后消息服务器把消息转发给应用程序B。因为应用程序A 和应用程序B 没有直接的代码关连。

RPC 和RMI的区别

RPC(Remote Procedure Call Protocol)远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC不依赖于具体的网络传输协议,tcp、udp等都可以。

RPC是跨语言的通信标准。

RMI可以被看作SUN对RPC的Java版本的实现,当然也还有其他的RPC

微软的DCOM就是建立在ORPC协议之上实现的RPC。

RMI集合了Java序列化Java远程方法协议(Java Remote Method Protocol)。这里的Java远程方法协议则是JRMP。

RMI和JMS的区别

传输方式

  • JMS 与 RMI 的区别在于:采用 JMS 服务,对象是在物理上被异步从网络的某个 JVM 上直接移动到另一个 JVM 上。
  • RMI 对象是绑定在本地 JVM 中,只有函数参数和返回值是通过网络传送的。

方法调用

  • RMI 一般都是同步的,也就是说,当client端调用Server端的一个方法的时候,需要等到对方的返回,才能继续执行client端,这个过程跟调用本地方法感觉上是一样的,这也是RMI的一个特点。
  • JMS 一般只是一个点发出一个Message(消息)到Message Server端,发出之后一般不会关心谁用了这个message(消息)。
  • 一般RMI的应用是紧耦合,JMS的应用相对来说是松散耦合的应用。

本段取自RMI与RPC的区别

由此得知其实RMI协议是发送方法以及方法参数,请求到server端后,server进行执行后返回结果给client端。

0x02 RMI底层架构

底层架构概念

根据上面内容其实可以总结为一句话,RPC是为了隐藏网络通信过程中的细节,方便使用。而在RMI中也是一样的。RMI中为了隐藏网络通讯的过程细节采用了动态代理的方式来进行实现。

下面先来看到调用流程图

在客户端和服务器各有一个代理,客户端的代理叫Stub(存根),服务端的代理叫Skeleton(骨架),合在一起形成了 RMI 构架协议,负责网络通信相关的功能。代理都是由服务端产生的,客户端的代理是在服务端产生后动态加载过去的。

stub担当远程对象的客户本地代表或代理人角色,负责把要调用的远程对象方法的方法名及其参数编组打包,并将该包转发给远程对象所在的服务器。

Stub编码后发送的数据包内容,包含如下内容:

1. 被使用的远程对象的标识符
2. 被调用的方法的描述
3. 编组后的参数

Skeleton接收到Stub发送数据会执行如下操作:

1. 从数据包中定位要调用的远程对象
2. 调用所需的方法,并传递客户端提供的参数
3. 捕获返回值或调用产生的异常。
4. 将打包返回值编组,返回给客户端Stub

本段内容部分取自:RMI反序列化漏洞分析

总结大体的内容如下:

客户端(Client):服务调用方。
    
客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
    
服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
    
服务端(Server):服务的真正提供者。

这里值得注意的一点是前面说到的传输进行打包和解包的步骤其实就是序列化和反序列化,传输的是序列化的数据。

架构调用流程

RMI大致的远程调用执行流程:

1. 客户端发起请求,请求转交至RMI客户端的stub类;

2. stub类将请求的接口、方法、参数等信息进行序列化;

3. 基于socket将序列化后的流传输至服务器端;

4. 服务器端接收到流后转发至相应的Skeleton类;

5. Skeleton类将请求的信息反序列化后调用实际的处理类;

6. 处理类处理完毕后将结果返回给Skeleton类;

7. Skelton类将结果序列化,通过socket将流传送给客户端的stub;

8. stub在接收到流后反序列化,将反序列化后的Java Object返回给调用者。

socket层中执行流程

  1. server在远程机器上监听一个端口,这个端口是jvm或者os在运行时随机选择的一个端口。可以说server在远程机器上在这个端口上导出自己。

  2. client并不知道server在哪,以及sever监听哪个端口,但是他有stub。stub知道所有这些东西,这样client可以调用stub上他想调用的任何方法。

  3. client调用给你stub上的方法

  4. stub链接server监听的端口并发送参数,详细过程如下:

    4.1 client连接server监听的端口
    4.2 server收到请求并创建一个socket来处理这个连接
    4.3 server继续监听到来的请求
    4.4 使用双方协定的歇息,传送参数和结果
    4.5 协议可以是JRMP或者 iiop

  5. 方法在远程server上执行,并发执行结果返回给stub

  6. stub返回结果给client,就好像是stub执行了这个方法一样。

其实也是一样对于了下面的这张图

但是第二部分内容是值得思索的一点,为什么client不知道server的监听端口和server在哪,而stub却知道呢?client不知道server的host和port的话,stub是如何创建一个知道所有这一切的stub对象呢?

这时候就引出了RMIRegistry(注册中心)的作用了。

RMIRegistry作用

RMIRegistry 可以认为是一个服务,它提供了一个hashmap,里面是 public_name, Stub_object 名值对。比如有一个远程服务对象叫做 Scientific_Calculator,然后想把这个服务对外公布为 calc,这样会在server上创建一个stub对象,把他注册到RMIRegistry ,这样client就可以从RMIRegistry 中得到这个stub对象了,可以使用一个工具类java.rmi.Naming来方便的操作注册和操作。

实现可看Java安全之RMI反序列化该篇文章。

总结:

简单来说就是使用java.rmi.Naming将一个某一个类注册进RMIRegistry 里面,注册后会在server端创建stub对象,而client就可以从RMIRegistry 中得到这个stub对象。前面内容说到过代理都是由服务端产生的,客户端的代理是在服务端产生后动态加载过去的。RMIRegistry运行在server端当中。

RMIRegistry 执行流程详解

  1. 首先RMIRegistry 运行在server端,RMIRegistry 自身也是一个远程对象。有一点需要注意的是:所有的远程对象(继承了UnicastRemoteObject对象)都会在sever上任意的端口导出自己,因为RMIRegistry 也是一个远程对象,他也在server上导出自己,这个端口是1099。

  2. 服务端运行在server上,在UnicastRemoteObject构造函数里面,他把自己导出在server上一个任意端口上,这个端口client是不知道的

  3. 当你调用Naming.rebind()的时候,会传入一个CalcImpl 的引用(这里叫做 c)作为第2个参数,Naming 就会构造一个stub对象,详细如下:
    a. Naming会使用getClass来获取类的名字,这里就是CalcImpl
    b. 加上后缀_Stub 变成了 CalcImpl _Stub
    c. 加载CalcImpl_Stub.class到虚拟机中
    d. 从c中获取RemoteRef 对象

    e. 就是这个RemoteRef 对象中封装了服务端细节,包括服务端的hostname、port
    f. 获取了RemoteRef 对象之后,就可以构造stub对象了。
    g. 传递stud对象到RMIRegistry中进行绑定,即(publicname,stub)
    f. RMIRegistry 中内部使用一个hashmap来存储(publicname,stub)

  4. 当客户端使用 Naming.lookup()的时候,会传入public name 作为参数,RMIRegistry 就会返回stub给客户端调用

这里作者说的导出自己这个没太理解这个的意思,我的理解是映射,将内容映射到1099端口中。

总结:

总体来说就是所以的远程对象都会在server的任意的端口上映射,RMIRegistry 也会进行映射,但是RMIRegistry 映射的端口是1099(默认是,可以修改)。远程对象进行映射的端口,client是不知道的。但是stub对象会知道。

在调用Naming.rebind()并并且传入某一个引用的时候,Naming 就会构造一个stub对象。Naming内部采用getClass来获取类的名字,并且添加_Stub后缀后价值到虚拟机中,然后从引用中获取 RemoteRef 对象,该对象就封装了封装了服务端的细节。而获取到该RemoteRef 就可以构造stub对象了,构造完成后传递到RMIRegistry注册中心中进行绑定,内部采用hashmap键值对的方式,即(publicname,stub),这时候使用Naming.lookup()传入对应的方法名,则会返回对应的stub到client端。

RMIRegistry 存在的意义只是为了方便client获取到stub对象,stub构造函数中需要一个RemoteRef 对象,这个对象只能在server端获取。

本段内容部分摘取自:深入理解rmi原理

0x03 结尾

简单的分析了一下RMI底层的架构,但是这些其实都仅仅是基于概念和理论层面的,具体的代码实现其实还没去看。在其中也是看得晕头转向的,部分也摘取了其他师傅们的文章内容,感觉已经总结很到位了,疯狂安利。摘取的内容下也贴出来 摘取内容的出处,感谢各位师傅们的详细讲解。