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:
- Attacking Java RMI services after JEP 290
- Java RMI for pentesters part two - reconnaissance & attack against non-JMX registries
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,
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:
- RMI deserialization vulnerability in PeerUnit/mock-dht
- RMI deserialization vulnerability in TubeDB
- RMI deserialization vulnerability in Weka