Lessons Learned on Brute-forcing RMI-IIOP With RMIScout

I'm excited to announce some new features that have been added to RMIScout. RMIScout is a tool to perform wordlist and brute-force attacks against exposed Java RMI interfaces to safely guess method signatures without invocation. Since the initial release, I've added the following features:

  • New communication methods:
    • RMI-IIOP support (typically port 1050)
    • RMI Activation stub support
    • SSL support
  • Invoke mode:
    • Direct invocation of identified methods. Supply parameters and invoke RMI methods from the CLI
  • List mode:
    • List registry names available on remote registry (similar to the nmap --rmi-dumpregistry script)
  • Localhost bypass (thanks to Nicky Bloor for the idea):
    • Automatically rewrite connection information for servers that advertise services bound to 127.0.0.1 when they are externally exposed
  • New interactive demo.
    • Docker container hosting multiple types of RMI services for testing out RMIScout

With these new additions, you should be able interact with most RMI servers. One exception is RMI-JMX, which remains out of scope. This is due to the excellent MJET tool by Mogwai Labs, which focuses on RMI-JMX services.

NEW FEATURES WITH RMISCOUT 1.4

One helpful feature that was missing from RMIScout was direct invocation of identified methods. Now you can  invoke any signature using strings and/or primitives (or arrays of these types) from the CLI:

Invoke signature using strings and/or primitives from the CLI

Check out the RMIScout GitHub repo for more commands and interactive demos (and lots of GIF demos!).

Also consider taking a look at Lucasz Mikula’s excellent two-part article on Java RMI attacks (part two includes tips on using RMIScout).

LESSONS LEARNED ABOUTS BRUTE-FORCING RMI-IIOP

Believe it or not, products still use RMI-IIOP, including multiple Oracle and IBM products. I waded through the hard-to-find documentation and oddities so you don’t have to.

Unlike standard Java RMI (aka RMI-JRMP) services that are identified by a method hash, Java Method invocation over the CORBA Internet Inter-Orb Protocol (RMI-IIOP) uses two different algorithms to identify method signatures:

  1. For non-overloaded methods, the signature is just the method name represented as a string. Parameter types, the number of parameters, and return type are all disregarded.
  2. For overloaded methods (methods sharing the same name), RMI-IIOP uses a concatenated string with the method name and its respective ordered types (examples below).

Let’s take a look at an example interface and a decompiled RMI-IIOP stub. Here is an excerpt of the remote interface from the RMIScout demo:

public int add(int paramInt1, int paramInt2) throws RemoteException;
public String sayTest19(int paramInt) throws RemoteException;
public String sayTest19(List paramList1, List paramList2) throws RemoteException;
public String sayTest19(List[] paramArrayOfList, int paramInt) throws RemoteException;
public Object sayTest20(String paramString) throws RemoteException;


Figure 1 - Excerpt of Demo interface

First let’s look at the add(int,int) method. Since its method name is unique, the generated stub is simply the method name. The server compares the client’s requested method (paramString in the figure below) against a string literal.

Because this method only uses primitive parameter types, the compiled stub has no type safety. The server will perform two 8-byte reads and interpret the bytes as long integers. For brute-forcing, the lack of type safety makes it impossible to know if we guessed the correct types. Furthermore, any additional input from the client is disregarded, thus preventing safe identification via an error for too many supplied parameters:

if (paramString.equals("add"))
{
int m = localInputStream.read_long();
i2 = localInputStream.read_long();
int i3 = localCorbaImpl.add(m, i2);
localObject9 = paramResponseHandler.createReply();
((org.omg.CORBA.portable.OutputStream)localObject9).write_long(i3);
return (org.omg.CORBA.portable.OutputStream)localObject9;
}


Figure 2 - Unique method name using primitive parameters

Now, let’s look at the overloaded sayTest19 methods. Here, the CORBA stub compiler appends the signature with information about the types to differentiate between the overloaded method names. Some naming schemes are more intuitive than others. In this case, we are provided type safety by the signature itself:

if (paramString.equals("sayTest19__long"))
{
int n = localInputStream.read_long();
localObject6 = localCorbaImpl.sayTest19(n);
localObject8 = (org.omg.CORBA_2_3.portable.OutputStream)paramResponseHandler.createReply();
((org.omg.CORBA_2_3.portable.OutputStream)localObject8).write_value((Serializable)localObject6, String.class);
return (org.omg.CORBA.portable.OutputStream)localObject8;
}
if (paramString.equals("sayTest19__java_util_List__java_util_List"))
{
localObject3 = (List)localInputStream.read_value(List.class);
localObject6 = (List)localInputStream.read_value(List.class);
localObject8 = localCorbaImpl.sayTest19((List)localObject3, (List)localObject6);
localObject9 = (org.omg.CORBA_2_3.portable.OutputStream)paramResponseHandler.createReply();
((org.omg.CORBA_2_3.portable.OutputStream)localObject9).write_value((Serializable)localObject8, String.class);
return (org.omg.CORBA.portable.OutputStream)localObject9;
}
if (paramString.equals("sayTest19__org_omg_boxedRMI_java_util_seq1_List__long"))
{
localObject2 = (List[])localInputStream.read_value(new List[0].getClass());
i2 = localInputStream.read_long();
localObject7 = localCorbaImpl.sayTest19((List[])localObject2, i2);
localObject9 = (org.omg.CORBA_2_3.portable.OutputStream)paramResponseHandler.createReply();
((org.omg.CORBA_2_3.portable.OutputStream)localObject9).write_value((Serializable)localObject7, String.class);
return (org.omg.CORBA.portable.OutputStream)localObject9;
}


Figure 3 - Overloaded methods with unique signatures

And for sayTest20(String), we again have a unique method name, but here we are deserializing a String class. In this case, the complex parameter allows us to force a ClassCastException to allow identification without invocation.

if (paramString.equals("sayTest20"))
{
localObject1 = (String)localInputStream.read_value(String.class);
localObject4 = localCorbaImpl.sayTest20((String)localObject1);
localObject7 = paramResponseHandler.createReply();
Util.writeAny((org.omg.CORBA.portable.OutputStream)localObject7, localObject4);
return (org.omg.CORBA.portable.OutputStream)localObject7;
}


Figure 4 - Unique method name but using non-primitive parameters

So, what does this mean for safely brute-forcing RMI-IIOP stubs? Overall, it’s a significantly smaller keyspace; most of the time we will only need to get the name of the method correct. That said, we will likely accidentally invoke methods that only use primitives, and we won’t always know the true method signature.

RMI-IIOP Brute-forcing Limitations

1. We can't identify methods solely using primitive typed parameters without invoking the method

This is because there is no concept of type checking in the generated stubs, any values sent along will be deserialized and cast to the expected primitive (as seen in the add(int, int) example above). Unlike RMI-JRMP, primitives are not up-cast to an Object-derived type, upcasting throws a ClassCastException instead of execution.

2. We can't identify the maximum number or types of parameters

If a method is not overloaded, we will only have an exception if there is a ClassCastException when deserializing a parameter or an unexpected EOFException because of insufficient parameters. Extra parameters in the input stream will just be ignored.

3. We can't identify the return types

Return types are not included in any part of the signature matching, so there’s no guaranteed way to identify the return type. If it’s an Object-derived type, we may get a local ClassCastException if RMIScout attempts to deserialize an incorrect typed response (invoke mode), but for primitives, we won’t know.

4. We have to send two requests for every check

RMIScout needs to test both possible signature formats because the overloaded methods use a distinct alternative format.

5. We need to use JRE8 to successfully use RMIScout's RMI-IIOP functionality

JRE9 stripped out RMI-IIOP functionality, so to run these tests and take advantage of existing standard library code, we need to use JRE8.

Overall, there is a risk of accidental invocation in brute-forcing these signatures. As such, RMIScout displays a warning prior to running IIOP brute-forcing. However, it is also significantly easier to enumerate signatures for IIOP. Using custom wordlists with method names least likely to cause harm is recommended (e.g., a method name like deleteRecord may match against deleteRecord(int) whereas evaluateString is less likely to match a primitive).

We can still achieve arbitrary Java deserialization by replacing object or array types in a method signature. Unlike RMI-JRMP, String types can still be exploited in RMI-IIOP servers compiled with the latest build of the JDK8.

LOOKING FORWARD

RMIScout has been a fun tool to write and maintain, the design definitely gave me more respect for the meta-programming powers of Java. I used the Javassist library to dynamically generate bytecode, the reflection API, and the permission API to dynamically rewrite RMI standard library code to avoid having to reimplement protocols. If you find yourself working on a Java-based tool that leverages an existing protocol, I recommend checking out the RMIScout source code for inspiration to modify behavior on the fly.

My goal was to create an easy-to-use pentest tool for interacting with and safely brute-forcing RMI services. As Java-RMI continues to die its slow death, I hope this tool helps save you some time when assessing RMI services. It was always a pain having to write custom clients just to interact with the services; hopefully, this RMIScout update will have abstracted most of that away for even more protocols and use cases.

There will always be products with unique customizations to Java-RMI communications (e.g., mutual TLS/Client Certificates) that RMIScout won’t be able to address, but hopefully it will save you the trouble of interacting with and exploiting most services.

Happy hacking!