Published in · 15 min read · 5 days ago
--
step by step walkthrough on NoSQL Injection room (free room)
Prerequisites
- Burp Suite: The Basics
- Burp Suite Repeater
- Burp Suite: Intruder
- SQL Injection
Learning Objectives
- Understand what NoSQL is
- Understand how NoSQL databases work, store data, and are interfaced with
- Learn about the different types of NoSQL injection attacks
- Learn how to practically exploit a NoSQL injection vulnerability
In this room, you will learn about NoSQL Injection. While SQL-based databases are a popular choice for data storage of web applications, several database options that are not based on SQL also exist. Database solutions such as MongoDB, a NoSQL database solution, have seen a significant rise in popularity in recent years. However, the fundamental principle of injection attacks remains the same. If we have the ability to inject into the database query itself, we have the opportunity to manipulate it!
Task 1: I am ready to learn about NoSQL Injection attacks!
Ans 1: No answer needed
Before we can learn about NoSQL injection, let’s first take a look how NoSQL databases work. In this room, we will focus on MongoDB. Although there are other NoSQL solutions, the principles about injection attacks in MongoDB can be applied to any NoSQL database.
MongoDB
Much like MySQL, MariaDB, or PostgreSQL, MongoDB is another database where you can store data in an ordered way. MongoDB allows you to retrieve subsets of data in a quick and structured form. If you are familiar with relational databases, you can assume MongoDB works similarly to any other database. The major exception is that the information isn’t stored on tables but rather in documents.
You can think of these documents as a simple dictionary structure where key-value pairs are stored. In a way, they are very similar to what you would call a record on a traditional relational database, but the information is just stored differently. For example, let’s say we are creating a web application for the HR department, and we would like to store basic employee information. You would then create a document for each employee containing the data in a format that looks like this:
{"_id" : ObjectId("5f077332de2cdf808d26cd74")"username" : "lphillips", "first_name" : "Logan", "last_name" : "Phillips", "age" : "65", "email" : "lphillips@example.com" }
As you see, documents in MongoDB are stored in an associative array with an arbitrary number of fields.
MongoDB allows you to group multiple documents with a similar function together in higher hierarchy structures called collections for organizational purposes. Collections are the equivalent of tables in relational databases. Continuing with our HR example, all the employee’s documents would be conveniently grouped in a collection called “people” as shown in the diagram below.
Multiple collections are finally grouped in databases, which is the highest hierarchical element in MongoDB. In relational databases, the database concept groups tables together. In MongoDB, it groups related collections.
Querying the Database
As with any database, a special language is used to retrieve information from the database. Just as relational databases use some variant of SQL, non-relational databases such as MongoDB use NoSQL. In general terms, NoSQL refers to any way of querying a database that is not SQL, meaning it may vary depending on the database used.
With MongoDB, queries use a structured associative array that contains groups of criteria to be met to filter the information. These filters offer similar functionality to a WHERE clause in SQL and offer operators the ability to build complex queries if needed.
To better understand NoSQL queries, let’s start by assuming we have a database with a collection of people containing the following three documents:
If we wanted to build a filter so that only the documents where the last_name is “Sandler” are retrieved, our filter would look like this:
['last_name' => 'Sandler']
As a result, this query only retrieves the second document.
If we wanted to filter the documents where the gender is male, and the last_name is Phillips, we would have the following filter:
['gender' => 'male', 'last_name' => 'Phillips']
This would only return the first document.
If we wanted to retrieve all documents where the age is less than 50, we could use the following filter:
['age' => ['$lt'=>'50']]
This would return the second and third documents. Notice we are using the $lt operator in a nested array. Operators allow for more complex filters by nesting conditions. A complete reference of possible operators can be found on the following link:
Task 2.1: What is a group of documents in MongoDB is known as?
Ans 2.1: collection
Task 2.2: Using the MongoDB Operator Reference, what operator is used to filter data when a field isn’t equal to a given value?
Ans 2.2: $ne
Task 2.3: Following the example of the 3 documents given before, how many documents would be returned by the following filter: [‘gender’ => [‘$ne’ => ‘female’] , ‘age’ => [‘$gt’=>’65'] ]?
Ans 2.3: 0
Injection is Injection
While it may seem complex to think about NoSQL Injection, when we boil down injection attacks to their very essence, we can understand the similarities between SQL Injection and NoSQL Injection.
The root cause of an injection attack is that improper concatenation of untrusted user input into a command can allow an attacker to alter the command itself. With SQL injection, the most common approach is to inject a single or double quote, that terminates the current data concatenation and allows the attacker to modify the query. The same approach applies to NoSQL Injection. If untrusted user input is directly added to the query, we have the opportunity to modify the query itself. However, with NoSQL Injection, even if we can’t escape the current query, we still have the opportunity to manipulate the query itself. Therefore, there are two main types of NoSQL Injection:
- Syntax Injection — This is similar to SQL injection, where we have the ability to break out of the query and inject our own payload. The key difference to SQL injection is the syntax used to perform the injection attack.
- Operator Injection — Even if we can’t break out of the query, we could potentially inject a NoSQL query operator that manipulates the query’s behavior, allowing us to stage attacks such as authentication bypasses.
In this room, our main focus will be on Operator Injection and the different ways it can be leveraged. This is because it is not as common to find Syntax Injection cases as most libraries used to create the queries apply filters that prevent you from injection into the syntax. However, since user input can vary, these same filters can often be vulnerable to Operator Injection. However, we will cover a Syntax Injection example at the end of the room.
How to Inject NoSQL
When looking at how NoSQL filters are built, bypassing them to inject any payload might look impossible, as they rely on creating a structured array. Unlike SQL injection, where queries were normally built by simple string concatenation, NoSQL queries require nested associative arrays. From an attacker’s point of view, this means that to inject NoSQL, one must be able to inject arrays into the application.
Luckily for us, many server-side programming languages allow passing array variables by using a special syntax on the query string of an HTTP Request. For the purpose of this example, let’s focus on the following code written in PHP for a simple login page:
<?php
$con = new MongoDB\Driver\Manager("mongodb://localhost:27017");
if(isset($_POST) && isset($_POST['user']) && isset($_POST['pass'])){
$user = $_POST['user'];
$pass = $_POST['pass'];
$q = new MongoDB\Driver\Query(['username'=>$user, 'password'=>$pass]);
$record = $con->executeQuery('myapp.login', $q );
$record = iterator_to_array($record);
if(sizeof($record)>0){
$usr = $record[0];
session_start();
$_SESSION['loggedin'] = true;
$_SESSION['uid'] = $usr->username;
header('Location: /sekr3tPl4ce.php');
die();
}
}
header('Location: /?err=1');
?>
The web application is making a query to MongoDB, using the “myapp” database and “login” collection, requesting any document that passes the filter ['username'=>$user, 'password'=>$pass]
, where both $user and $pass are obtained directly from HTTP POST parameters. Let's take a look at how we can leverage Operator Injection in order to bypass authentication.
If somehow we could send an array to the $user and $pass variables with the following content:
$user = ['$ne'=>'xxxx']
$pass = ['$ne'=>'yyyy']
The resulting filter would end up looking like this:
['username'=>['$ne'=>'xxxx'], 'password'=>['$ne'=>'yyyy']]
We could trick the database into returning any document where the username isn’t equal to ‘xxxx,’ and the password isn’t equal to ‘yyyy’. This would probably return all documents in the login collection. As a result, the application would assume a correct login was performed and let us into the application with the privileges of the user corresponding to the first document obtained from the database.
The problem that remains unsolved is how to pass an array as part of a POST HTTP Request. It turns out that PHP and many other languages allow you to pass an array by using the following notation on the POST Request Body:
user[$ne]=xxxx&pass[$ne]=yyyy
So, let’s fire up our favorite proxy and try to test this. For this guide we will be using Burp Proxy.
Task 3.1: What type of NoSQL Injection is similar to normal SQL Injection?
Ans 3.1: Syntax
Task 3.2: What type of NoSQL Injection allows you to modify the behavior of the query, even if you can’t escape the syntax?
Ans 3.2: Operator
Bypassing the Login Screen
First of all, let’s open the website on http://MACHINE_IP/ and send an incorrect user/pass to capture the request on Burp:
The original captured login request looks like this:
We now proceed to intercept another login request and modify the user and pass variables to send the desired arrays:
This forces the database to return all user documents and as a result we are finally logged into the application:
Task 4: When bypassing the login screen using the $ne operator, what is the email of the user that you are logged in as?
so, let’s go to the login page and tern on burb suit proxy, then click login
add [$ne] after ‘password’, then click forword
Ans 4: admin@nosql.int
Logging in as Other Users
We have managed to bypass the application’s login screen, but with the former technique, we can only login as the first user returned by the database. By making use of the $nin operator, we are going to modify our payload so that we can control which user we want to obtain.
First, the $nin operator allows us to create a filter by specifying criteria where the desired documents have some field, not in a list of values. So if we want to log in as any user except for the user admin, we could modify our payload to look like this:
This would translate to a filter that has the following structure:
['username'=>['$nin'=>['admin'] ], 'password'=>['$ne'=>'aweasdf']]
Which tells the database to return any user for whom the username isn’t admin and the password isn’t aweasdf. As a result, we are now granted access to another user’s account.
Notice that the $nin operator receives a list of values to ignore. We can continue to expand the list by adjusting our payload as follows:
This would result in a filter like this:
['username'=>['$nin'=>['admin', 'jude'] ], 'password'=>['$ne'=>'aweasdf']]
This can be repeated as many times as needed until we gain access to all of the available accounts.
Note: The jude
user above is not an actual user, but an example of how an additional username can be added.
Task 5.1: How many users are there in total?
now after we found the username admin, let’s exclude it and look for the rest. Use [$nin][] after ‘user’
here we found pedro
let’s try more time and exclude both admin and pedro
we got john
one more
we got secret
so, finally that was the last one
Ans 5.1: 4
Task 5.2: There is a user that starts with the letter “p”. What is his username?
Ans 5.2: pedro
Extracting Users’ Passwords
At this point, we have access to all of the accounts in the application. However, it is important to try to extract the actual passwords in use as they might be reused in other services. To accomplish this, we will be abusing the $regex operator to ask a series of questions to the server that allow us to recover the passwords via a process that resembles playing the game hangman.
First, let’s take one of the users discovered before and try to guess the length of his password. We will be using the following payload to do that:
Notice that we are asking the database if there is a user with a username of admin and a password that matches the regex: ^.{7}$
. This basically represents a wildcard word of length 7. Since the server responds with a login error, we know the password length for the user admin isn't 7. After some trial and error, we finally arrived at the correct answer:
We now know the password for user admin has length 5. Now to figure out the actual content, we modify our payload as follows:
We are now working with a regex of length 5 (a single letter c plus 4 dots), matching the discovered password length, and asking if the admin’s password matches the regex ^c....$
, which means it starts with a lowercase c, followed by any 4 characters. Since the server response is an invalid login, we now know the first letter of the password can't be "c". We continue iterating over all available characters until we get a successful response from the server:
This confirms that the first letter of admin’s password is ‘a’. The same process can be repeated for the other letters until the full password is recovered. This can be repeated for other users as well if needed.
Task 6.1: What is john’s password?
let’s forword the request to the Repeater with the username of john
first, let’s figure out the length of his password by using the regex ^.{7}$
after trying several times, I got the length, it’s 8 characters
now let’s try to figure the password itself by using the regex ^c…….$
note: depending on the length of the password, you have to add more dots (.) in the regex ^c…….$, so because we have 8 characters password, I put the first char as what I want to guess and add 7 dots after it.
that took me some time, but I found it
Ans 6.1: 10584312
Task 6.2: One of the users seems to be reusing his password for many services. Find which one and connect through SSH to retrieve the final flag!
we need to repeat the same process with each user, but TryHackMe gave us a hint and said that the user is pedro, although that saved us time, but we still need to figure out the password. I found that the password is coolpass123, so let’s ssh into it and find the flag
Ans 6.2: flag{N0Sql_n01iF3!}
Finding Syntax Injection
Now that we have covered Operator Injection, let’s take a look at a Syntax Injection example. A Python application is running to allow you to receive the email address of any username that is provided. To use the application, authenticate via SSH using ssh syntax@ip
along with the credentials below:
Once authenticated, you can provide a username as input. Let’s start by simply providing admin
:
ssh syntax@10.10.170.89
syntax@10.10.170.89's password:
Please provide the username to receive their email:admin
admin@nosql.int
Connection to 10.10.170.89 closed.
We can start to test for Syntax Injection by simply injecting a '
character, which will result in the error seen in the response below:
syntax@10.10.170.89's password:
Please provide the username to receive their email:admin'
Traceback (most recent call last):
File "/home/syntax/script.py", line 17, in <module>
for x in mycol.find({"$where": "this.username == '" + username + "'"}):
File "/usr/local/lib/python3.6/dist-packages/pymongo/cursor.py", line 1248, in next
if len(self.__data) or self._refresh():
File "/usr/local/lib/python3.6/dist-packages/pymongo/cursor.py", line 1165, in _refresh
self.__send_message(q)
File "/usr/local/lib/python3.6/dist-packages/pymongo/cursor.py", line 1053, in __send_message
operation, self._unpack_response, address=self.__address
File "/usr/local/lib/python3.6/dist-packages/pymongo/mongo_client.py", line 1272, in _run_operation
retryable=isinstance(operation, message._Query),
File "/usr/local/lib/python3.6/dist-packages/pymongo/mongo_client.py", line 1371, in _retryable_read
return func(session, server, sock_info, read_pref)
File "/usr/local/lib/python3.6/dist-packages/pymongo/mongo_client.py", line 1264, in _cmd
sock_info, operation, read_preference, self._event_listeners, unpack_res
File "/usr/local/lib/python3.6/dist-packages/pymongo/server.py", line 134, in run_operation
_check_command_response(first, sock_info.max_wire_version)
File "/usr/local/lib/python3.6/dist-packages/pymongo/helpers.py", line 180, in _check_command_response
raise OperationFailure(errmsg, code, response, max_wire_version)
pymongo.errors.OperationFailure: Failed to call method, full error: {'ok': 0.0, 'errmsg': 'Failed to call method', 'code': 1, 'codeName': 'InternalError'}
Connection to 10.10.170.89 closed.
The following line in the error message shows us that there is Syntax Injection:
for x in mycol.find({"$where": "this.username == '" + username + "'"}):
We can see that the username variable is directly concatenated to the query string and that a JavaScript function is being executed in the find command, allowing us to inject into the syntax. In this case, we have verbose error messages to give us an indication that injection is possible. However, even without verbose error messages, we could test for Syntax Injection by providing both a false and true condition and seeing that the output differs, as shown in the example below:
ssh syntax@10.10.170.89
syntax@10.10.170.89's password:
Please provide the username to receive their email:admin' && 0 && 'x
Connection to 10.10.170.89 closed.ssh syntax@10.10.170.89
syntax@10.10.170.89's password:
Please provide the username to receive their email:admin' && 1 && 'x
admin@nosql.int
Connection to 10.10.170.89 closed
Exploiting Syntax Injection
Now that we have confirmed Syntax Injection, we can leverage this injection point to dump all email addresses. To do this, we want to ensure that the testing statement of the condition always evaluates to true. As we are injecting into the JavaScript, we can use the payload of '||1||'
. Let's use this to disclose sensitive information:
ssh syntax@10.10.170.89
syntax@10.10.170.89's password:
Please provide the username to receive their email:admin'||1||'
admin@nosql.int
pcollins@nosql.int
jsmith@nosql.int
[...]
Connection to 10.10.170.89 closed.
The Exception to the Rule
It is worth noting that for Syntax Injection to occur, the developer has to create custom JavaScript queries. The same function could be performed using the built-in filter functions where ['username' : username]
would return the same result but not be vulnerable to injection. As such, Syntax Injection is rare to find, as it means that the developers are not using the built-in functions and filters. While some complex queries might require direct JavaScript, it is always recommended to avoid this to prevent Syntax Injection. The example shown above is for MongoDB; for other NoSQL solutions, similar Syntax Injection cases may exist, but the actual syntax will be different.
Task 7.1: What common character is used to test for injection in both SQL and NoSQL solutions?
Ans 7.1: ‘
Task 7.2: What is the email value of the super secret user returned in the last entry?
Ans 7.2: Syntax@Injection.FTW
Defenses
To defend against NoSQL Injection attacks, the key remediation is to ensure that there isn’t any confusion between what is the query and what is user input. This can be resolved by making use of parameterised queries, which split the query command and user input, meaning that the engine cannot be confused. Furthermore, the built-in functions and filters of the NoSQL solution should always be used to avoid Syntax Injection. Lastly, input validation and sanitization can also be used to filter for syntax and operator characters and remove them.