From 0 to RCE: Cockpit CMS

Our team searched for bugs in the source code of Cockpit, an open-source content management system. Here is the description of Cockpit from its official site:

Cockpit is a headless CMS with an API-first approach that puts content first. It is designed to simplify the process of publication by separating content management from content consumption on the client side.

Cockpit is focusing just on the back-end work to manage content. Rather than worry about delivery of content through pages, its goal is to provide structured content across different channels via a simple API.

While investigating the Cockpit source code, we discovered numerous vulnerabilities. Attackers could exploit them to take control of any user account and perform remote code execution.

In this article, I will talk about the technical details and demonstrate how these vulnerabilities can be exploited.

Extracting user account names

In the source code, we found two methods vulnerable to NoSQL injection, which can be used to extract application usernames. Neither of these methods requires authentication.

NoSQL injection in /auth/check (CVE-2020-35846)

Let’s consider the check method of the Auth controller responsible for authenticating app users:

Auth::check method

and the authenticate function of the cockpit module:

authenticate function

As you can see, the code does not check the type of the user parameter, which allows embedding an object with arbitrary MongoDB operators in the query.

This is blind injection, so for successful exploitation you need to find a way to return the result of the condition.

Having analyzed the method source code, we developed a technique. In essence, we pass an array (instead of a string) in the password parameter. This results in a warning, displayed by the password_verify function, about an invalid value type:

authenticate function

Now I will demonstrate a few more ways to exploit blind NoSQL injection:

1. Using the $eq operator

The $eq operator matches documents where the value of a field equals the specified value.

For example, you can use it to bruteforce names with a dictionary.

The condition is met: a user with the name admin has been found
The condition is NOT met: no user with the name admini has been found

2. Using the $regex operator

Provides regular expression capabilities for pattern matching strings in queries

You can use it to bruteforce the names of all application users.

The condition is met: a user whose name begins with the characters ad has been found
The condition is NOT met: no user whose name begins with the characters ada has been found

We can speed up bruteforcing by adding the $nin operator to the query, which will exclude any users that have already been found:

$nin selects the documents where the field value is not in the specified array

The condition is met: a user whose name starts with the character j has been found
The condition is NOT met: no user whose name begins with the character a has been found (the only user with this name is admin, but that user has been excluded from the search)

We can tweak this by adding a fixed quantifier to the regular expression for finding or limiting the length of the string:

The condition is met: a user whose name starts with the character a and contains 4 more characters has been found
The condition is met: a user whose name starts with the characters ad and contains 3 more characters has been found
The condition is NOT met: no user whose name starts with the character a and contains 12 more characters has been found

3. Using the $func operator of the MongoLite library (used by default)

This non-standard operator allows calling the criterion function $b (any PHP function with a single parameter), which takes a single argument equal to field $a (in this case, the user field):

By passing the PHP function var_dump or var_export as the argument, we will turn blind injection into classic in-band injection. With a single query, we can get the names of all app users:

NoSQL injection in /auth/requestreset

requestreset method of the Auth controller responsible for creating the password reset token:

Auth::requestreset method

As in the previous case, there is no type check for the user parameter. Exploitation is similar, but without any difficulties such as password or CSRF token verification:

Extracting password reset tokens

Cockpit, like many other web applications, allows resetting account passwords.
We discovered two methods that are vulnerable to NoSQL injection and allow obtaining the password reset token for any user.

NoSQL injection in /auth/resetpassword (CVE-2020-35847)

resetpassword method of the Auth controller, which is responsible for changing the user password using the reset token:

Auth::resetpassword method

There is no type checking for the token parameter, so you can extract existing tokens with the following query:

NoSQL injection in /auth/newpassword (CVE-2020-35848)

newpassword method of the Auth controller, which is responsible for displaying the user password reset form:

Auth::newpassword method

And, again, there is no type checking for the token parameter. The query is similar to the previous one:

User account compromise

Now, being able to get password reset tokens, we can compromise any user account we are interested in. This takes just a few steps:

1. Access /auth/requestreset to generate a token for resetting the password of the selected user:

2. Extract tokens by using one of the methods just described (/auth/resetpassword or /auth/newpassword):

3. Extract user account data (username, password hash, API key, password reset token) using the /auth/newpassword method and the password reset tokens obtained in the previous step:

Extracting user account admin
Extracting user account loopa

With this data in hand, we can then:

  1. Use the application with the API key.
  2. Bruteforce the account password from the hash.
  3. Change the account password by using the /auth/resetpassword method:

Remote Code Execution

Easy RCE

Having compromised the administrator account, we can upload a web shell using Cockpit’s standard Finder component in order to achieve remote code execution:

Uploading the web shell _shell.php to the Cockpit root directory
Executing commands on the server using the web shell

PHP injection in the UtilArrayQuery::buildCondition method of the MongoLite library

Let’s consider the method registerCriteriaFunction of the Database class, which creates a condition function for the specified criteria (filters) of the document:

Database::registerCriteriaFunction method

and the associated function buildCondition of the UtilArrayQuery class:

UtilArrayQuery::buildCondition function

Make note of the $key variable, which contains the field name. Its content is plugged into the future string literal as-is, without being escaped.
So by controlling the content of the $key variable, we can escape from the string literal (break it) with a single quote in order to inject arbitrary PHP code.
To demonstrate the vulnerability, we will use the /accounts/find method (authentication required). This method supports custom criteria (filters), which means it will allow us to place arbitrary content in $key:

pwned

Conclusion

In this article, I have demonstrated several ways to exploit blind NoSQL injection, a way for an unauthenticated user to take over any account, and remote code execution in the MongoLite library.

Everyone should update to the latest version (>= 0.12.0) right away.

The disclosure timeline:

  • October 14, 2020 – Vulnerability information (#1) sent to developer
  • October 15, 2020 – Bugfixes released (commit 79fc963, commit 33e7199)
  • October 26, 2020 – Patch released (commit 2a385af)
  • March 15, 2021 – Vulnerability information (#2) sent to developer
  • March 17, 2021 – Bugfix released (commit b40d6bd)