RMIScout: Safely and Quickly Brute-Force Java RMI Interfaces for Code Execution

Java Remote Method Invocation (RMI) is a Java API that performs remote procedure calls and allows a client application to access or invoke the services available on a remote Java Virtual Machine (JVM). Numerous deserialization vulnerabilities affect RMI because its communications rely on the transfer of serialized Java objects (see Nicky Bloor’s 2017 44CON talk). Servers that use RMI expose an RMI registry service to look up remote objects and delegate remote method invocations.

Although RMI registries do not disclose a list of available method signatures, if you can guess it, you can invoke it. My latest tool to share, RMIScout, performs wordlist and brute-force attacks against exposed Java RMI interfaces to safely guess method signatures without invocation.

By "safely," I mean that this approach saves you, the security tester, from an awkward conversation with a client or product owner where you have to confess that you invoked a method on a remote server with random parameters.

This technique is powerful and quick, allowing approximately 2,500 signature guesses per second. Identified signatures with non-primitive parameters are often exploitable deserialization vectors, so this can lead to a high-impact finding during an assessment.

First, let’s explore how Java RMI works and the significant impact of enumerating remote methods. Then we’ll discuss how RMIScout approaches safe method guessing and exploitation.

GUESSING SIGNATURES == CODE EXECUTION

In addition to being able to directly invoke the methods you enumerate, there is also an opportunity to perform Java deserialization attacks against some signatures. Other than being high impact, it also allows you to sidestep any AuthN/AuthZ logic that may be involved in invoking methods.

A known security issue with RMI is its unsafe deserialization of non-primitive method parameters, as shown in the JRE source code below:

165.	protected static Object unmarshalValue(Class<?> type, ObjectInput in) throws IOException, ClassNotFoundException {
166.		if (type.isPrimitive()) {
167.			if (type == Integer.TYPE) {
168.				return in.readInt();
169.			} else if (type == Boolean.TYPE) {
170.				return in.readBoolean();
171.			} else if (type == Byte.TYPE) {
172.				return in.readByte();
173.			} else if (type == Character.TYPE) {
174.				return in.readChar();
175.			} else if (type == Short.TYPE) {
176.				return in.readShort();
177.			} else if (type == Long.TYPE) {
178.				return in.readLong();
179.			} else if (type == Float.TYPE) {
180.				return in.readFloat();
181.			} else if (type == Double.TYPE) {
182.				return in.readDouble();
183.			} else {
184.				throw new Error("Unrecognized primitive type: " + type);
185.			}
186.			} else {
187.			return type == String.class && in instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)in) : in.readObject();
188.		}
189.	}


Figure 1:
Source code excerpt from 
sun.rmi.server.UnicastRef

As shown above, when the parameter is a non-primitive type, it is deserialized directly using readObject(). To address this, JDK Enhancement Proposal (JEP) 290 was established, which provided process-wide filtering for object deserialization. However, developers easily overlook this configuration because they are often focused on object filters at the application level. But without manually configured process-wide filtering, RMI deserialization is unchecked by default.

On servers not implementing whitelisting, any known RMI signature that uses non-primitive types (e.g., java.lang.String) can be exploited by replacing the object with a serialized payload. An Trinh's 2019 Blackhat EU talk highlighted the prevalence of this misconfiguration identifying affected products, such as the following:

  • VMWare vSphere Data Protection
  • vRealize Operations Manager
  • Pivotal TC Server and Gemfire
  • Apache Karaf and Cassandra

On top of brute-forcing method signatures, RMIScout can integrate with ysoserial (payloads) and GadgetProbe (remote class enumeration) to perform deserialization attacks against services that either lack process-wide serialization filters or have them incorrectly configured.

ADVANTAGES OF BRUTE-FORCING SIGNATURES

To execute remote methods, Java RMI clients submit a 64-bit hash of the method signature, which the server uses to identify the corresponding server-side method. These hashes are computed with the following logic:

1. Source code representation of the signature:
void myRemoteMethod(int count, Object obj, boolean flag)

2. Bytecode representation of signature:
myRemoteMethod(ILjava/lang/Object;Z)V

3. Method Hash: big-endian representation of first 8 bytes of the SHA1 of the signature:
Hash = SHA1String(“myRemoteMethod(ILjava/lang/Object;Z)V”).substring(0,8).reverse()

As shown above, the information that is used to compute a method hash are: the method name, the return types, and an ordered list of the fully qualified names of the parameters’ types. Instead of brute-forcing the 64-bit keyspace, we can use wordlists for each of these categories to guess common signatures. Using GitGot, I scraped GitHub for RMI interfaces in open source projects and found interesting patterns across the 15,000+ method signatures:

Distribution of return types of 15,000 functions sampled from RMI interfaces on GitHub

Figure 2: Distribution of return types of 15,000 functions sampled from RMI interfaces on GitHub

As shown above, using int, boolean, and void as our guessed return types gives us a 61.4% chance of guessing correctly based off this data. If we add java.lang.String, we can add an extra 9.8% to our probable success, as it represents nearly a third of the non-primitive return types. That brings our final list of candidate return types (int, boolean, void, String) to a 71.22% probability in the observed dataset.

Leveraging statistical analysis of method signatures along with wordlists of common method names and parameter lists allows us to brute-force a significantly smaller keyspace than 2^64. Below is an excerpt of the brute-force options for RMIScout:

usage: rmiscout bruteforce [-h] -i INPUT -r RETURN_TYPES -p PARAMETER_TYPES -l PARAMETER_LENGTH [-n REGISTRY_NAME] host port

Bruteforce attack on RMI interfaces

positional arguments:
host                   Remote hostname or IP address
port                   Remote RMI port

named arguments:
-h, --help             show this help message and exit
  -i INPUT, --input INPUT
                         Wordlist of candidate method names
  -r RETURN_TYPES, --return-types RETURN_TYPES
                         Set of candidate return types
  -p PARAMETER_TYPES, --parameter-types PARAMETER_TYPES
                         Set of candidate parameter types
  -l PARAMETER_LENGTH, --parameter-length PARAMETER_LENGTH
                         Candidate parameter length range expressed as a comma-delimited range EX: 1,4
  -n REGISTRY_NAME, --registry-name REGISTRY_NAME
                         Specific registry name to query. All names are queried

Figure 3: Excerpt of RMIScout help menu

RMIScout includes a deduped wordlist of prototypes (prototypes.txt) found from this exploration and it also includes a list of most frequently occurring method names (methods.txt).

HOW IT WORKS

To identify RMI functions without executing them, RMIScout leverages low-level JRE RMI functions and uses dynamic class generation to send RMI invocations with deliberately mismatched types to trigger RemoteExceptions. These exceptions allow us to identify remote methods without actually invoking them.

To accomplish this, RMIScout computes the method hash using the original user-supplied types, but substitutes the values of all parameters for an instance of a dynamically generated, serializable class. The class is generated with a random 255-character name (the underlying assumption being that this random name does not exist in the remote class path). For example:

Candidate Remote Interface:

void login(String user, String password)

RMIScout will invoke:

login((String) new QUjsdg83..255 chars..(), (String) new QUjsdg83..255 chars..())

If the RMI method is present, it will attempt to unmarshal the parameters. This will result in a remote exception disclosed to the client. Specifically, it’ll be a java.rmi.UnmarshalException either caused by a ClassNotFoundException (due to our non-existent random class) or by other exceptions (finding object-typed data when primitive-typed data was expected in the stream) without invoking the underlying method.

This technique allows RMIScout to validate the presence of remote functions (of one or more parameters) without invoking them!

I was not able to discover a method for identifying parameter-less methods without invoking them. As such, by default void argument prototypes are skipped by RMIScout unless the option --allow-unsafe is used. Note: --allow-unsafe will cause parameter-less methods to be invoked on discovery, which can lead to unexpected and possibly destructive behavior on the remote server.

CONCLUSION

I hope this simplifies the process for this attack vector and helps reinforce the importance of whitelisting classes via process-wide filtering. For more details on JEP-290 changes and process-wide filtering, check out this documentation from Oracle. I hope RMIScout helps you out on your next assessment!

Thanks to @h0ng10, @_tint0, and @nickstadb for their detailed technical writeups and inspiration!

Follow me on Twitter at: @theBumbleSec

TRY IT OUT!

https://github.com/BishopFox/rmiscout