Java RMI Exploitation
Java Remote Method Invocation (RMI) is Java’s native mechanism for distributed object communication: it lets a JVM invoke methods on objects hosted in another JVM, typically across a network. Because RMI is built on Java object serialization, historically ships with no authentication, and is frequently exposed to the network by default, RMI endpoints are a rich attack surface — the Oracle RMI tutorial1 covers the benign side; this page covers the offensive one. RMI registries conventionally listen on TCP 1099 (rmiregistry), with actual service objects on dynamically-assigned ports.
Architecture
RMI has three moving parts:
- Registry — a naming service (“phone book”) where servers register remote objects under well-known names. Clients call
LocateRegistry.getRegistry(host, port)thenregistry.lookup("name")to obtain a stub. By default, the registry only acceptsbind/rebind/unbindcalls from localhost, butlookupis remote. - Server — the object implementation. It extends
java.rmi.Remote, is exported viaUnicastRemoteObject.exportObject(), and is registered withNaming.bind()/registry.bind(). Exported objects listen on 0.0.0.0 by default. - Client — looks the object up in the registry and calls methods on the stub exactly as if it were local. Arguments and return values travel via Java serialization.
A key consequence: the client must have the remote interface (and any custom argument classes) on its classpath. The historical workaround, dynamic code loading from a remote codebase, is blocked by the security manager on modern JDKs — which is why attackers usually pull the interface definitions from the target’s own client JAR (often downloadable from the same web server).
Reconnaissance
nmap fingerprints RMI well: the registry shows up as java-rmi on 1099/tcp, and the rmi-dumpregistry NSE script enumerates bound names, implemented interfaces, and the host:port of each object’s actual endpoint:
nmap -p 1099 -sVC $TARGETThe dedicated tool is remote-method-guesser (RMG)2 (qtc-de, Black Hat USA 2021 Arsenal) — a Java RMI vulnerability scanner whose enum action reports, per endpoint:
- bound names and their implementing classes
- remote codebases in use (
useCodebaseOnlystatus) - whether
java.lang.Stringis unmarshalled viareadObject()(outdated config) - CVE-2019-2684 registry localhost-bypass status
- security manager presence
- JEP 290 filter status on the registry, DGC, and ActivationSystem (including the An Trinh bypass)
RMG can also bind/unbind/rebind objects, launch ysoserial deserialization attacks with the JEP290 bypass, and attempt remote codebase loading. The repo ships three practice servers (plain RMI, SSRF-to-localhost-RMI, Spring Remoting) as containers.
Attack surface
1. No authentication / cleartext
The RMI standard provides no built-in authentication; any host that can reach the registry can look up and invoke any bound object. Where RMI backs a fat client, application-level “login” methods move trust to the attacker-controlled client — a custom client can simply skip the login call and invoke privileged methods directly (interface knowledge permitting). Traffic is also plaintext by default, so credentials passed through remote calls are sniffable.
2. Deserialization attacks (pre-JEP 290)
Because method arguments are unmarshalled with ObjectInputStream.readObject(), any reachable RMI method that accepts a non-primitive type is a Java deserialization entry point. Moritz Bechler’s ysoserial exploits weaponized this:
ysoserial.exploit.RMIRegistryExploit— sends a gadget chain to the registry’sbindmethodysoserial.exploit.JRMPClient— targets the Distributed Garbage Collector (DGC) present on every RMI listener
Both work reliably when a usable gadget (CommonsCollections, Groovy, …) exists in the target’s classpath; the returned exception even leaks whether the gadget class resolved. Gadget chains come from ysoserial3.
3. JEP 290 and its limits
JEP 290 (JDK 9, backported to 8u121 / 7u131 / 6u141) introduced look-ahead deserialization filters (jdk.serialFilter) and shipped built-in whitelist filters for the RMI registry and DGC — killing the two generic ysoserial RMI exploits on patched JVMs.4 However:
- The built-in filters only protect the registry/DGC/activator infrastructure. Application-level methods remain deserializable unless the developers set a process-wide filter (
-Djdk.serialFilter=...) — which is rare in practice. - CVE-2019-2684 / An Trinh’s registry bypass (2019)5 circumvents the registry filter by forcing an outgoing JRMP connection back to an attacker listener, which then feeds the gadget through a channel with no filter.
- Pre-2020 JVMs unmarshal
Stringarguments viareadObject(), letting an attacker swap a string argument for a gadget object (fixed in 8u242 / 11.0.6 / 13.0.2 / 14.0.1; other non-primitive argument types remain exploitable via type-confusion replacement, e.g. with a JDI-scriptable debugger like YouDebug or a proxy like Nicky Bloor’s BaRMIe6).
Detection angle: because the built-in filters are whitelists, sending a bogus object and reading the stack trace remotely reveals whether JEP 290 is installed; the URLDNS gadget (works on all Java versions) confirms deserialization reachability via a DNS callback.
4. Registry binding abuse
On vulnerable endpoints, the localhost-only bind restriction can be bypassed (CVE-2019-2684), letting an attacker rebind/unbind registry entries — effectively machine-in-the-middle for every RMI client that subsequently looks up the hijacked name.
5. Remote codebase loading
Where java.rmi.server.useCodebaseOnly=false (non-default), a server will load stub classes from an attacker-supplied codebase URL — direct remote code execution on anything old enough to permit it.
Method-hash brute forcing
RMI dispatches methods by a SHA-1-derived 64-bit hash of the method signature (name + argument types, not argument names). Guessing an entire interface is infeasible, but for deserialization you only need one method taking an object argument — spraying common signatures (login(String,String), logMessage(int,String), …) is a plausible last resort when no client JAR is available.
Defensive takeaways
- Don’t expose registries to the network; if RMI is required, firewall both the registry and the (random!) object ports, or tunnel over TLS.
- Patch the JDK — JEP 290 is backported to 8u121+; the String-unmarshalling fix landed in 8u242+.
- Set a process-wide
jdk.serialFilterwhitelist; treat interface definitions as public. - Assume any reachable RMI endpoint equals unauthenticated RCE on unpatched Java.
RMI is one facet of Java’s broader offensive surface (JNDI injection, deserialization, JNI); memory-corruption work on the JVM’s native side happens under gdb.
Related
- java — language hub: reverse shells, Log4Shell delivery, JNI memory-safety boundary
- nmap —
rmi-dumpregistryNSE script for discovery - python-pickle-worm — the Python analogue: deserialization of attacker-controlled objects
- gobuster — finding downloadable client JARs / hidden codebase directories on the web tier
Sources
- Oracle — An Overview of RMI Applications (The Java Tutorials)
- MOGWAI LABS — Attacking Java RMI services after JEP 290 (2019)
- MOGWAI LABS — An Trinhs RMI Registry Bypass (2020)
- GitHub — qtc-de/remote-method-guesser (RMG)
- GitHub — frohoff/ysoserial
- GitHub — NickstaDB/BaRMIe
- HackTricks — 1098/1099/1050 - Pentesting Java RMI - RMI-IIOP