Java RMI uses the default Java deserialization mechanism for passing parameters during remote method invocations. In other words, RMI uses ObjectInputStream that is a well-known unsafe deserialization mechanism. If an attacker can find and send a deserialization gadget to a vulnerable remote method, in the worst case it can result in arbitrary code execution.

I recently wrote a CodeQL query that looks for dangerous remote objects registered in an RMI registry. This post describes the vulnerability and how the query works.

Luckily, not all RMI methods are vulnerable. Making a long story shorter, to be vulnerable, a remote method has to accept a complex object as a parameter. If it accepts only primitive types, strings and a few other types from the Java core library, then the method is safe. You can find more details in the following articles:

Here is an example of vulnerable code:

RemoteObject.action() is vulnerable because it accepts a complex parameter. The vulnerability here can be fixed by specifying a deserialization filter introduced by JEP 290. Here is an example:

It is also possible to configure a global deserialization filter by calling ObjectInputFilter.Config.setSerialFilter(ObjectInputFilter) method or by setting jdk.serialFilter system or security property. Make sure that you use Java version that contains JEP 290. I put some more examples of vulnerable code, demo exploits and mitigation in this repository.

Now, let’s have a look at the CodeQL query that detects such vulnerabilities. The main part is a configuration for tracking data flows from constructing dangerous remote object to registering them in an RMI registry:

A source of such a data flow is a constructor call for a type that has a vulnerable method. The predicate hasVulnerableMethod() checks whether a class has vulnerable methods or not.

A sink is a method call to one of the methods that registers a remote object in an RMI registry. For example, Registry.bind() or Registry.rebind().

The predicate isAdditionalTaintStep() adds an additional taint-propagation step. If a remote object doesn’t extend UnicastRemoteObject class, then it has to be exported by calling one of the UnicastRemoteObject.exportObject() methods before registering the object in a registry. This operation is covered by the predicate. Plus, this predicate plays a role of a sanitizer because it propagates taint only if exportObject() was called without a deserialization filter.

The query detected multiple issues in various open source projects on GitHub. Here are some examples:

More examples of alerts can be found on LGTM. Finally, the query was able to detect CVE-2016-2170 in older Apache OFBiz releases.