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:
--rmi-dumpregistry
script)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.
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:
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).
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:
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.
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.
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!