JNDI Injection Series: RMI Vector - Fundamentals

Yani
InfoSec Write-ups
Published in
9 min readDec 27, 2022

--

https://awsimages.detik.net.id/community/media/visual/2022/01/13/ilustrasi-jalan_169.jpeg

JNDI (Java Naming and Directory Interface) is a Java API that allows clients to discover and look up data and objects via a name. It is used to obtain naming and directory services from several service providers where these objects are stored: LDAP(Lightweight Directory Access Protocol), and Java RMI registry (Remote Method Invocation) and etc.

JNDI is a simple Java API that takes just one string parameter, but if the parameter is tainted by an attacker, a victim application will be connected to a malicious LDAP/RMI server and execute arbitrary command.

This blog post will walk you through the basics about what RMI is, and you will see how RMI system can be exploited to get RCE in the next blog post. In the further more topics about JNDI injection attack will be covered in this series.

RMI is the technique that is abused to achieve RCE, it seems counter-intuitive on the surface, considering that RMI operations are subject to additional checks and constraints by a Java security manager. But that is not always the case, as some JVM versions do not apply the same restrictions and policies, and as such, RMI can sometimes be a more effortless channel compared with others like LDAP which are more tightly monitored by defenders.

1. Introduction to Java RMI

Java RMI is shipped with the Java JDK 1.1 and higher. It is a true distributed computing application interface for Java.

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). Within the RMI system, an object is exported from the RMI server and it’s methods is invoked from the RMI client. To facilitate this interaction, the local JVM may require Java bytecode related to the remote object. This feature leads to the attack vectors covered in the next blog post.

The RMI registry is a key component of RMI system and provides a centralized directory. The RMI distributed applications use the RMI registry to obtain a reference to a remote object. Each time the RMI server creates an object, it registers this object with the RMI registry. To invoke a remote object, the RMI client looks up and fetches the remote object reference by its name in the RMI registry and then invokes a method of the remote object on the RMI server. This procedure is demonstrated in Figure 1.

Figure 1 — http://www.cs.cmu.edu/afs/cs.cmu.edu/user/pzheng/www/KDI-IIM/RMI_Image1.jpg

The original RMI solution includes two key intermediary objects: stub and skeleton. The RMI uses stub and skeleton object for communication with the remote server object. A remote server object is an object resides on the RMI server virtual machine and its method can be invoked from the RMI client.

The stub is an object that resides at the client side and represents the remote object on the RMI server. The stub initiates a connection with remote virtual machine and transmits the message from the RMI client caller to the RMI server, waiting for the response. After gets the response, the stub is responsible for processing it and returning the result to the caller.

The skeleton is an object on the RMI server side. When the skeleton receives the incoming message from the stub, it extracts the method name and parameters from the message, invokes the corresponding method on the actual object on the RMI server, marshals result and sends the response to the stub.

Put all elements together and create the classic RMI architecture illustrated as Figure 2:

Figure 2
  1. The RMI server creates a remote object implementing the server object interface, and registers it at the RMI registry (using bind() or reBind() methods).
  2. The RMI client looks for the server object at the RMI registry (using lookup() method).
  3. The RMI client obtains the server stub stored in the registry, the stub refers to the server object.
  4. The RMI client caller invokes a method on the remote object, the method is actually invoked on the server stub.
  5. The method parameters are bundled into a message. If the parameters are objects, they are serialized. This process is known as marshalling. The server stub sends the message to the server skeleton object residing in the RMI server.
  6. The skeleton object un-marshals the method name and parameters from the message and invokes the method with the parameters on the server object which it is associated with the server stub.
  7. The server object executes the method and returns result back to the skeleton.
  8. The skeleton marshals the result in a message and send the message to the stub.
  9. The stub un-marshals the returned message to extract the result and transfer it to the RMI client caller.

It is worth noting that one thing that you must do to develop an RMI application is to define the server object interface. The interface defines what remote methods/variables are going to be exported from the remote server object.

RMI has got rid of the use of skeleton files. Skeletons in the above RMI architecture were replaced by general server-side dispatch code in JDK 1.2 (released 1998). Likewise, statically generated stubs were replaced by dynamic proxies in JDK 5 (released 2004). The RMI system operates in the more lightweight way.

2. Create Java RMI Applications

For better understanding of how the RMI works, two CentOS 7.9 servers are used to set up a demonstration environment. The CentOS servers use Java SE Runtime Environment 1.6.0_29 (6u29) for both RMI server and RMI client, as the old version of JDK deploys lenient remote method invocation security policy, and it can be reused by the future RMI exploitation demonstrations in the next blog post. For simplicity, the demonstration places the RMI server and RMI registry on the same Linux box.

2.1 Prepare Java Environment

To install the specific historical version of JDK, you can go to https://www.oracle.com/java/technologies/javase-java-archive-javase6-downloads.html to get the download address for the targeted JDK after you log into oracle official site (I got the download URL with AuthParam from the browser’s network tab after entering “Developer tools” mode).

Execute the wget on the CentOs servers. One execution example is provided as below:

[root@demo ~]# wget -O jdk-6u29.bin https://download.oracle.com/otn/java/jdk/6u29-b11/jdk-6u29-linux-x64-rpm.bin?AuthParam=1671893419_f213a470eff1bb72f9921ec7b5087375

The AuthParam varies in your case or you can get the JDK bin file in your own way.

Install the bin by executing the following commands:

[root@demo ~]# chmod +x ./jdk-6u29.bin
[root@demo ~]# ./jdk-6u29.bin

Check the java version to ensure it is 1.6.0_29.

[root@demo ~]# java -version
java version "1.6.0_29"
Java(TM) SE Runtime Environment (build 1.6.0_29-b11)
Java HotSpot(TM) 64-Bit Server VM (build 20.4-b02, mixed mode)

2.2 Developing the RMI Server Program

Defining the Remote Interface

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 case, HelloService interface is defined in HelloService.java.

HelloService.java

package rmi.api;
import java.rmi.*;

public interface HelloService extends Remote{
public String sayHello(String message) throws RemoteException;
}

The same interface is included by both the RMI server and the RMI client application serving as the contract for the communication between these two parties.

Developing the Implementation Class (Server Object)

With the remote interface providing the description of all the methods of a 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.

HelloServiceImpl.java

package rmi.api.imp;
import java.io.Serializable;
import java.rmi.*;
import java.rmi.server.UnicastRemoteObject;
import rmi.api.HelloService;

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService{

private static final long serialVersionUID = 1L;
public HelloServiceImpl() throws RemoteException {
super();
}

public String sayHello(String message) throws RemoteException {
System.out.println("It is from serve, the message received from the client is " + message);
return message;
}
}

HelloServiceImpl extends java.rmi.server.UnicastRemoteObject, it means when your object is constructed, the UnicastRemoteObject constructor is called. This hooks up the object to the RMI internal infrastructure that handles socket listening and remote method dispatch. In other words, it "exports" the HelloServiceImpl object.

Creating RMI Server

On the RMI server, RmiServer is implemented in the RmiServer.java.

RmiServer.java

package rmi.server;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import rmi.api.imp.HelloServiceImpl;

public class RmiServer {
public static void main(String[] args) {
try {
System.out.println("RMI Server Starts...");
//This creates a registry local to the RMI server.
LocateRegistry.createRegistry(8888);

//create a stub of the remote object
HelloServiceImpl hImpl = new HelloServiceImpl();
System.out.println("Bind the hImpl to helloService");

//This command bind our stub to a unique key helloService, As a result, the remote object is now available to any client that can locate the registry.
Naming.bind("rmi://localhost:8888/helloService", hImpl);

System.out.println("Waiting to be invoked by Client...");
Thread.sleep(10000000);
System.out.println("RMI Server Stops");
} catch (Exception e) {
System.out.println("error: " + e);
}
}
}

Compiling the code

The above source code files are placed in the following layout for your reference.

On the RMI server, compile the java file and execute it under rmi-server directory.

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

[root@demo rmi-server]# java rmi/server/RmiServer
RMI Server Starts...
Bind the hImpl to helloService
Waiting to be invoked by Client...

Now the RMI server is waiting for any incoming request for the remote invocation from the RMI client.

2.3 Developing the RMI Client Program

Assume you have installed Java SE Runtime Environment 1.6.0_29 (6u29) as instructed in 2.1 On the client server, the only things needed are to define the same remote interface as on the RMI server and create the RMI client program.

Defining the Remote Interface

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

HelloService.java

package rmi.api;
import java.rmi.*;

public interface HelloService extends Remote{
public String sayHello(String message) throws RemoteException;
}

Create RMI Client

On the RMI client, the RMI client is create via RmiServer.java.

RmiClient.java

package rmi.client;
import java.rmi.*;
import java.util.Arrays;
import rmi.api.HelloService;

public class RmiClient {
public static void main(String[] args) {
String host = "localhost:8888";
if (args.length > 0)
host = args[0];
try {
String names[]=Naming.list("rmi://" + host + "/helloService");
System.out.println("I am client, it is service list: "+Arrays.asList(names));

HelloService h = (HelloService) Naming.lookup("rmi://" + host + "/helloService");

System.out.println("It is on client, the result from server is "+h.sayHello("John"));
} catch (Exception ex) {
System.out.println("error: " + ex);
}
}
}

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

On the RMI client server, compile the java file and execute it under rmi-client directory.

As you have put the RMI server and Client on the different server, when launching 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.

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

[root@demo rmi-client]# java rmi/client/RmiClient 192.168.0.95:8888
I am client, it is service list: [//192.168.0.95:8888/helloService]
It is on client, the result from server is John
[root@demo rmi-client]#

The RMI server receives the method invocation from RMI client, the console looks like as below:

[root@demo rmi-server]# java rmi/server/RmiServer
RMI Server Starts...
Bind the hImpl to helloService
Waiting to be invoked by Client...
It is from serve, the message received from the client is John

It is observed the sayHello method is executed on the RMI server, the result is sent to client.

The source code is accessible from github.

Reference:

https://www.baeldung.com/java-rmi

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/