Java Sec —— RMI

文章发布时间:

最后更新时间:

Java 学习笔记——RMI

0x00. BackGround

RMI (Remote Method Invocation) 远程方法调用

RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试 图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。

可以看到,RMI 整个通信方式与邮件发送与接收所经过的协议流程类似

Untitled

  • RMI 所支持的特性:

    1. 动态类加载

      RMI 支持动态类加载,如果设置了 java.rmi.server.codebase,则会尝试从其中的地址获取 .class 并加载及反序列化

      设置方法

      开启 RMI 安全策略管理并配置对应的策略文件

      System.setProperty("java.rmi.server.codebase","[http://127.0.0.1:9999/](http://127.0.0.1:9999/)");

      使用启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:9999/" 进行指定

0x01. 源码解析

  • 测试 demo

    • Java code for Server side

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public class RemoteServer {
      public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
      startReg();

      // 创建远程对象
      RemoteInterface remoteObject = new RemoteObject();
      // 绑定
      Naming.bind("rmi://localhost:1099/Hello", remoteObject);
      }

      public static void startReg() {
      try {
      LocateRegistry.createRegistry(1099);
      System.out.println("Server Start");
      } catch (Exception e) {
      e.printStackTrace();
      }
      }
      }
    • Java code for Client side

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class Client {
      public static void main(String[] args) throws RemoteException, NotBoundException {

      // sun.rmi.registry.RegistryImpl_Stub
      Registry registry = LocateRegistry.getRegistry("localhost", 1099);

      System.out.println(Arrays.toString(registry.list()));

      // lookup and call
      RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
      System.out.println(stub.sayHello());
      System.out.println(stub.sayGoodbye());
      }
      }
    • Java code for Remote object Interface

      1
      2
      3
      4
      5
      6
      7
      public interface RemoteInterface extends Remote {
      public String sayHello() throws RemoteException;

      public String sayHello(Object name) throws RemoteException;

      public String sayGoodbye() throws RemoteException;
      }
    • Java code for Remote object Implementation

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {

      protected RemoteObject() throws RemoteException {
      }

      @Override
      public String sayHello() throws RemoteException {
      return "Hello My Friend";
      }

      @Override
      public String sayHello(Object name) throws RemoteException {
      return name.getClass().getName();
      }

      @Override
      public String sayGoodbye() throws RemoteException {
      return "Bye";
      }
      }
  • 流程分析

服务注册

1. 远程对象创建

起始于:RemoteInterface remoteObject = new RemoteObject();

其继承于父类 UnicastRemoteObject 因此初始化时会调用其构造函数, port 默认为 0,并继续调用 exportObject 方法

Untitled

Untitled

这里可以看到在 export 自身的同时,还会新建 UnicastServerRef 实例,这个东西内部会进一步创建一个 LiveRef 对象,由随机数+端口进行标识

Untitled

Untitled

跟进发现,它还会继续调用 TCPEndpoint.getLocalEndpoint(var2)

其中会初始化网络通信所需的 host port 等信息

Untitled

最终将 TCPEndpoint 实例赋值到 LiveRef 的 ep 字段中

Untitled

回到主线,Remote 对象会进一步通过 UnicastServerRef 实例进行 export

Untitled

Untitled

Untitled

可以看到该 UnicastRef 实例所存的就是之前保存了网络通信信息的 LiveRef

Untitled

跟进可以看到,首先创建了一个以 “_Stub” 为后缀的类实例(构造函数参数为包含已初始化LiveRefUnicastRef 实例)

其次为 Remote 对象创建了动态代理(利用 RemoteObjectInvocationHandler

Untitled

对于 RemoteObjectInvocationHandler 其 invoke 方法,当待调用代理方法非 Object 声明,并且方法名不是 finalize 时,会调用 invokeRemoteMethod

Untitled

这里可以看到实际上真正调用的是 sun.rmi.server.UnicastRef#invoke(java.rmi.Remote, java.lang.reflect.Method, java.lang.Object[], long)

Untitled

Untitled

该方法会获取 LiveRef 当中的网络通信信息,创建连接,执行调用,并获取结果执行 unmarshalValue 进一步执行原生反序列化

Untitled

回到 createProxy 方法,,将动态代理和 Remote 实例封装到 Target 类当中之后调用 sun.rmi.transport.LiveRef#exportObject 方法,其会进一步调用 TCPTransport#exportObject 来监听端口

Untitled

Untitled

注意到之后会将 Target 实例通过 sun.rmi.transport.Transport#exportObject 放到 ObjectTable 中

Untitled

该 ObjectTable 内部通过 HashMap 管理所有 Target 实例,支持通过 ObjectEndpoint 和 WeakRef 进行索引

Untitled

2. 注册中心创建

起始于 LocateRegistry.createRegistry(1099);

首先调用 RegistryImpl 构造函数,根据端口是否指定 1099 进入不同控制分支,但都会创建 LiveRef 通讯实例以及 UnicastServerRef 对象。接下来调用 setup 方法

Untitled

后者跟进 UnicastServerRef#exportObject 第一个参数为当前类实例 RegistryImpl

Untitled

后续流程同样走到 sun.rmi.server.Util#createProxy 不同点在于这里 stubClassExists 方法 withoutStubs map 是空的,因此会成功创建 RegistryImpl_Stub 实例并返回,不再进一步向下创建动态代理

Untitled

RegistryImpl_Stub 类继承于 RemoteStub 并实现了一系列 bind list lookup 等服务操作 API

Untitled

与服务注册时使用的 RemoteObjectInvocationHandler 类似,其通信时数据传输也是通过直接序列化实现

Untitled

后续注册中心就会调用 setSkeleton 方法设置服务端 skeleton,可以看到实际会通过反射创建 RegistryImpl_Skel 实例,设置到 UnicastServerRef 的 skel 字段上

Untitled

Untitled

后续流程与创建远程服务对象一致,创建 Target,并设置到 ObjectTable 中

3. 服务注册

通用方法是 registry.bind

服务发现

请求注册中心

体现在 LocateRegistry.getRegistry()

「服务端行为」

首先创建了一个包含注册中心通信地址的 RegistryImpl_Stub 对象;

Untitled

接下来调用 bind 方法进行服务对象和命名绑定时实际会调用 sun.rmi.registry.RegistryImpl_Stub#bind 方法,首先通过父类 UnicastRef 上设置好通讯地址的 ref 字段来调用 sun.rmi.server.UnicastRef#newCall

这里就是简单通过 UnicastRef#newCall 与注册中心建立连接,后续直接写入输出流,写入方法名和远程实例

Untitled

服务调用

以客户端获取到注册中心后,调用 lookup 寻找指定命名的 RMI 服务为例

Untitled

过程很简单,建立连接、序列化服务名称并写入输出流、获取到输入流并执行反序列化

Registry 端/服务端表现位于 RegistryImpl_Skel 的 dispatch 方法

整体流程框架

Untitled

0x02. Attack

1. 攻击 Server 端

  • 恶意服务参数

    发生在客户端调用远程服务对象的方法时,通过获取到的远程代理,传输序列化的方法名(SHA1 based hash)及参数

    一般情况下,若远程服务接口对应的方法参数类型为 Object,则可以直接构造任意的序列化数据造成 RCE Attack

    但是若不知道方法参数具体类型,仍想要传输任意类型的序列化数据,该怎么办?

    Untitled

    TODO 📭

    基本思路就是利用 agent 等方式,hook 住 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法方法,也就是客户端在利用代理,调用服务端对象方法时,参数类型传递时动态修改为指定要求的类型即可

  • 动态类加载

    ==「适用版本」==6u45/7u21

    条件:SecurityManager + java.rmi.server.useCodebaseOnly=false

    position: 反序列化操作发生在服务端接收响应客户端发来的网络请求时,调用的 sun.rmi.server.UnicastServerRef#dispatch 方法,序列化输入流由 MarshalInputStream 封装,期间执行 resolveClass 检查序列化数据

    Untitled

    其 var5 反序列化得到指定 codebase url 地址,同时想要真正执行类加载,还需要满足 useCodebaseOnly 为 false,这也是为什么存在版本限制(6u45/7u21 默认为 false)

    重点在于 codebase 地址可以被双方控制

2. 攻击 Registry 端

以 bind 为例,注册端知道会使用 sun.rmi.registry.RegistryImpl_Skel#dispatch 来接收服务端发来的序列化的服务名称以及远程代理对象,并通过 RegistryImpl 的 bind 方法保存在 bindings HashTable 中

Untitled

因此这里可以直接传递恶意的序列化远程代理类,在注册端反序列化时 RCE

具体的实践:yso_RMIRegistryExploit

yso 中以 CC1 为例,获取到 CC1 恶意 gadget 后,创建 Remote 代理(利用的 handler 是 AnnotationInvocationHandler,将 payload 设置到 handler 的 map 中,反序列化时伴随着触发),之后通过 bind 序列化传递

Untitled

3. 攻击 DGC

Server 端启动 DGCImpl,在 Registry 端注册 DGCImpl_Stub ,Client 端获取到 DGCImpl_Stub,通过其与 Server 端通信,Server 端使用 RegistryImpl_Skel 来处理。

DGC 在客户端接收服务端返回的远程对象调用同时,会创建 DGCImpl_Stub 以及 DCGImpl_Skel 来维护远程对象的引用

==「DCGImpl_Skel#dispatch」==:监听请求

Untitled

==「DGCImpl_Stub#dirty/clean 更新/回收引用对象」==:

Untitled

武器化Exploit: ysoserial —— JRMP

  • JRMP Listener module

    本质是一条反序列化 Gadget ,可以达到在本地指定端口开启 JRMP 协议的效果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    * Gadget chain:
    * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
    * UnicastRemoteObject.reexport() line: 266
    * UnicastRemoteObject.exportObject(Remote, int) line: 320
    * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
    * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
    * LiveRef.exportObject(Target) line: 147
    * TCPEndpoint.exportObject(Target) line: 411
    * TCPTransport.exportObject(Target) line: 249
    * TCPTransport.listen() line: 319
    *
    * Requires:
    * - JavaSE
    *
    * Argument:
    * - Port number to open listener to
    */

    作用大致就是类似反连平台,可以自动响应连接请求,并返回指定的序列化数据

    ==「Gadget 构造」==:

    首先看下用到的类,首先拿到 RemoteObject 的构造函数(参数类型为RemoteRef, 参数实例传递子类 UnicastServerRef,并指定端口)

    Untitled

    之后会调用 sun.reflect.ReflectionFactory#newConstructorForSerialization 传入 var1 代表 ActivationGroupImpl class,var2 代表指定 RemoteObject 构造器,当 var2 的声明类非 var1 时,会去创建序列化构造函数(ActivationGroupImpl 类)

    Untitled

    这里的作用实际创建一个序列化构造函数,允许在反序列化时调用构造函数来创建新的对象实例

    如果想在反序列化时执行一些特殊的初始化或处理步骤,可以在类中定义一个特殊的构造函数,然后在该构造函数中执行这些操作。但请确保这个构造函数是 public 的且没有参数。

    Untitled

    最终返回的是 ActivationGroupImpl 实例,向上转型为 UnicastRemoteObject(好神奇

    最后反射设置字段 port 为指定端口

    ==「Gadget 反序列化分析」==:

    回忆 UnicastRemoteObject 的作用

    java.rmi.server.UnicastRemoteObject 类通常是远程调用接口实现类的父类,或直接使用其静态方法 exportObject 来创建动态代理并随机监听本机端口以提供服务。

    也就是我们在创建远程服务对象时声明其为父类的那个

    readObject 方法中会执行 reexport()

    Untitled

    之后指定端口 exportObject

    Untitled

    export 过程中创建 UnicastServerRef 网络通信实例

    Untitled

    之后的分析与前面一致了就,势必会打开指定端口的 JRMP 协议,监听请求,期间的网络传输数据采用序列化格式

    Untitled

    那么 ActivationGroupImpl 在这里的作用是什么?

    这里不是很懂,应该是父类 UnicastRemoteObject 构造函数无法直接构造,或者使用 UnSafe 来构造也可以

  • JRMP JRMPClient(payload)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
    *
    *
    * UnicastRef.newCall(RemoteObject, Operation[], int, long)
    * DGCImpl_Stub.dirty(ObjID[], long, Lease)
    * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
    * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
    * DGCClient.registerRefs(Endpoint, List<LiveRef>)
    * LiveRef.read(ObjectInput, boolean)
    * UnicastRef.readExternal(ObjectInput)
    *
    * Thread.start()
    * DGCClient$EndpointEntry.<init>(Endpoint)
    * DGCClient$EndpointEntry.lookup(Endpoint)
    * DGCClient.registerRefs(Endpoint, List<LiveRef>)
    * LiveRef.read(ObjectInput, boolean)
    * UnicastRef.readExternal(ObjectInput)
    *
    * Requires:
    * - JavaSE
    *
    * Argument:
    * - host:port to connect to, host only chooses random port (DOS if repeated many times)
    *
    * Yields:
    * * an established JRMP connection to the endpoint (if reachable)
    * * a connected RMI Registry proxy
    * * one system thread per endpoint (DOS)

    入口点从任意一个继承父类 RemoteObject 的对象开始,父类的 readObject 方法会进一步调用 ref 字段的 readExternal 方法

    sun.rmi.server.UnicastRef#readExternal 这里会调用 [LiveRef.*read](http://LiveRef.read)* 来恢复创建 ref 字段的通信实例

    Untitled

    创建 TCPEndpoint 之后,会进一步调用 DGCClient.*registerRefs*

    Untitled

    其中会循环获取所有通信节点,调用 EndpointEntry#registerRefs 最后调用 makeDirtyCall

    Untitled

    最后触发 DGC 的 dirty 操作

    因此可以看出,在 UnicastRef 进行反序列化时,会触发 DGC 通信及 dirty 方法调用,此时如果与一个恶意服务通信,返回恶意数据流,则会造成反序列化漏洞。

    yso 项目中使用的是 RemoteObjectInvocationHandler 作为 handler,封装成 Registry 的代理类

    利用:利用JRMPClient这个Gadget去调用JRMPListener,然后JRMPListener利用CommonsCollections这个Gadget来实现RCE。

  • 精简版

    因为 RemoteObjectInvocationHandler 动态代理 Registry 接口形成的这个 Registry 已经后续被列入黑名单了,因此需要绕过。

    • 缩短链子

      我们知道上一条链子中 UnicastRef 实现了 Externalizable 接口,因此可以直接反序列化并调用重写 readExternal 方法,进而触发和前述一样的利用链

      • Java Poc

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        public Object getObject(String command) throws Exception {
        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
        port = new Random().nextInt(65535);
        host = command;
        }
        else {
        host = command.substring(0, sep);
        port = Integer.valueOf(command.substring(sep + 1));
        }

        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        return ref;
        }
    • 更换代理接口

0x03. JEP 290


提供的机制如下:

(1) 提供一个限制反序列化类的机制,白名单或者黑名单

(2) 限制反序列化的深度和复杂度

(3) 为 RMI 远程调用对象(exportObject)提供了一个验证类的机制

(4) 定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器

版本:

Untitled

JEP 290 主要是在 ObjectInputStream 类中增加了一个serialFilter属性和一个 filterChcek 函数,其中 serialFilter就可以理解为过滤器。
ObjectInputStream 对象进行 readObject 的时候,内部会调用 filterChcek 方法进行检查,filterCheck方法中会对 `serialFilter属性进行判断,如果不是 null ,就会调用 serialFilter.checkInput 方法进行过滤。
设置过滤器本质就是设置 ObjectInputStreamserialFilter 字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:

  1. 设置全局过滤器是指,通过修改 Config.serialFilter这个静态字段的值来达到设置所有 ObjectInputStream对象的 serialFilter值 。具体原因是因为 ObjectInputStream 的构造函数会读取Config.serialFilter的值赋值到自己的serialFilter字段上,所有就会导致所有 new 出来的 ObjectInputStream对象的 serailFilter 都为Config.serialFilter的值。
  2. 设置局部过滤器是指,在 new ObjectInputStream 的之后,再修改单个 ObjectInputStream 对象的 serialFilter 字段值

JEP290 机制,实际可以通过 JVM 穿参数或者配置文件的方式来设置防御规则,RMI 可采用默认白名单的防御规则。规则传入为字符串形式,在 Global 类的构造函数中进行解析,具体解析逻辑如下所示

Untitled

解析之后返回并设置到 ObjectInputFilter 类的 configuredFilter 字段上,当 ObjectInputStream 在初始化时,便会获取到 configuredFilter 字段上的实例,并设置到输入流的 serialFilter 字段之上。当进行 readObject 之前会自动触发 filterCheck 方法,进而 serialFilter 非空时调用 serialFilter.checkInput 枚举输入流中的类与规则进行匹配,进行合法性校验

Untitled

「RMI 中 JEP290 机制的实现」

RegistryImpl 在构造函数中,伴随着 UnicastServerRef2 的初始化,会通过 lambda 表达式以及方法引用的方式设置 RMI 的默认 JEP290 防御规则

Untitled

Untitled

上图是 RegistryImpl 的 registryFilter 这个静态常量字段通过 JVM 传参或者配置文件的方式来设置过滤器

下图可以看到如果 registryFilter 为空,或者 checkInput 方法返回 Status.UNDECIDED 字面量的话则会采用 RMI 默认的 JEP 防御规则。

Untitled

在服务端后续处理序列化数据请求(匹配 RegistryImpl 对应的 ObjID)时,可以看到 RMI 通过 Config.setObjectInputFilter 的方式设置 RMI 局部过滤器,

Untitled

DGCImpl 与 RegistryImpl 设置过滤器的方式基本一致

Untitled

  • RMI JEP290 绕过

    case1: 如果服务端”绑定”了一个对象,他的方法参数类型是Object 类型的方法时,则可以绕过 JEP 290

    本质:普通对象在 exportRemoteObject 时,不会像 RegistryImpl 和 DGCImpl 一样,注册带有初始化好的 filter 到 UnicastServerRef 上,进而在导出并封装到 Target 的过程中, disp 实际时 filter 字段为 null 的 UnicastServerRef。

    因此,后续在根据 ObjId 从 ObjectTable 中获取 Target 后 disp 后,再进行 dispatch 过程,上述的 unmarshalCustomCallData 方法由于 filter 为 null 因此会直接返回 null, 进而调用 unmarshalValue 方法触发原生反序列化的 source,不再进行 filterChcek 等类检查操作,绕过了 JEP 290 限制

    case2: 对于方法参数类型是由 Object 类型引申类型(如 String),可如下操作:

    1. 将 java.rmi 软件包的代码复制到新软件包,然后在其中更改代码

    2. 将调试器附加到正在运行的客户端,并在序列化对象之前替换对象

    3. 使用 Javassist 之类的工具更改字节码
    4. 通过实现代理来替换网络流上已经序列化的对象

0x04. Extension

  • Pentest

    • Discovery

      nmap 服务发现,支持 RMI 注册端口探测、接口探测以及具体代理实现的位置探测

      Untitled

  • easycve

    Untitled

    Untitled

我们在远程复现时会遇到一个 trick:RMI 默认获取的 IP 地址为主机网卡的地址,而非浏览器上显示的公网地址,因此可能会出现服务端对象无法调用的情况,可以利用 debugger 或者反射修改一下 LiveRef 字段 ep 属性中的 host 值

参考链接

[1] https://su18.org/post/rmi-attack/

[2] 浅学RMI反序列化 | Boogiepop Doesn’t Laugh (boogipop.com

[3] mogwailabs/rmi-deserialization: Slides/Demos from the BSides Munich 2019 talk “Attacking Java RMI in 2019” (github.com)

[4] JRMP安全问题分析-从CVE到CTF - 先知社区 (aliyun.com)