Hacking Node.JS and MongoDB
THE SQL INJECTION PRIMER
The first thing you learn when studying SQL Injection is how to create true statements. Let's consider the following example SQL statement that is used to authenticate the user when the username and the password are submitted to the application:
SELECT * FROM users WHERE username = '$username' AND password = '$password'
If this statement is not prepared or properly handled when constructed, an attacker may be able to place ' or 1=1--in the username field in order to construct a statement that looks more or less like the one bellow, which is known as the classic login bypass via SQLI:
SELECT * FROM users WHERE username = '' or 1=1--' AND password = ''
Even today, this classic attack and its variations are wildly used to detect the presence of improper handling of SQL statements.
THE MONGODB INJECTION PRIMER
Now, even though SQL Injection is still a popular attack vector, it is no longer as widespread as it used to be. Many modern web applications opt in to use a much simpler storage mechanism such as the one provided by NoSQL databases like MongoDB. NoSQL databases not only promise simplified development but also improved security by eliminating the SQL language entirely and relaying on much simpler and structured query mechanism that is typically found in the face of JSON and JavaScript.
The SQL statement that we used above to query the user login details will be written like this in MongoDB:
db.users.find({username: username, password: password});
As you can see we no longer deal with a query language in the form of a string therefore one would think that injection is no longer possible. And of course, as it is always the case with security, they will be wrong because there are many factors at play.
For example, if we assume that the username field, or parameter if you like, is coming from a deserialized JSON object, manipulation of the above query is not only possible but inevitable. Such as, if one supplies a JSON document as the input to the application, an attacker will be able to perform the exact same login bypass that was before possible only with SQL injection:
{ "username": {"$gt": ""}, "password": {"$gt": ""} }
The actual vulnerable handler of the request will look more or less like this:
app.post('/', function (req, res) { db.users.find({username: req.body.username, password: req.body.password}, function (err, users) { // TODO: handle the rest }); });
In the above ExpressJS handler, the username and password fields are not validated to ensure that they are strings. Therefore, when the JSON document is deserialized, those fields may contain anything but strings that can be used to manipulate the structure of the query. In MongoDB, the field $gt has a special meaning, which is used as the greater than comparator. As such, the username and the password from the database will be compared to the empty string ""and as a result return a positive outcome, i.e. a true statement.
The request to exploit this vulnerability will look more or less like the one bellow. Use this link to open the request inRest:
POST http://target/ HTTP/1.1 Content-Type: application/json { "username": {"$gt": ""}, "password": {"$gt": ""} }
TAKING NODEJS AND MONGODB EXPLOITATION FURTHER
In the example above I deliberately choose to use JSON as the transport mechanism because it makes this attack easier to explain. While, it is not unusual to see JSON documents as the communication mechanism, they are not as widespread as url-encoded key-value pairs, simply known as urlencoding. One would think that if you just use body and query parameters in urlencoding format than you will be safe.
In ExpressJS we can still achieve the bypass effect without using JSON at all but simple query strings. For example, we can submit a request like the one illustrated bellow. Use this link to open the request in Rest:
POST http://target/ HTTP/1.1 Content-Type: application/x-www-form-urlencoded username[$gt]=&password[$gt]=
The string username[$gt]= is a special syntax used by the qs module (default in ExpressJS and the body-parsermiddleware). This syntax is the equivalent of making an JavaScript object/hash with a single parameter called $gtmapped to no value. In essence, the request above will result into a JavaScript object that looks like the one illustrated bellow:
{ "username": {"$gt": undefined}, "password": {"$gt": undefined} }
If you compare this object with the one that we used in the previous example, you can see that logically they are the same.
Once again, we have defeated the authentication mechanism to login as the first user in the database which is probably the administrator. This technique can be extended even further with additional MongoDB operators as per the official documentation here.
LEARNING BY EXAMPLE
I believe that the best way to learn new things is to experience them first hand. Therefore, I have created two projects that illustrate this vulnerability in practice. The first project uses normal urlencoded form values while the second project uses JSON (AngularJS).
TESTING FOR MONGODB INJECTION
I hope you now have a good idea the kind of attacks we can perform against NodeJS and MongoDB applications. In practice, I have seen a great success using the technique above and its variations when performing manual investigations. However, I am also a big fan of automation so, for the fans
of Websecurify Suite.
I have personally incorporated these tests in both Formfuzz and Jsonfuzz tools.
To make it simpler, I have implemented a new escapemode command
called use_mongodb_payloads. Once you execute the command, the fuzzer will not only use MongoDB specific payloads but also do mutation of the query to achieve maximum depth.