One of the common issues reported by web application penetration testers is username/account enumeration, typically involving an unauthenticated person trying to identify valid usernames in the system. Knowing these valid usernames can assist the attacker in crafting further attacks against the system, such as password guessing attacks, phishing attacks, and denial of service attacks via account lockouts. The following are popular resources for testing for this issue:

All three of these are missing an important approach to testing for enumeration that I have had a lot of success with in the penetration tests that I have done: timing attacks! Specifically, one can often determine whether a guessed username is valid or not simply by looking at how long the system takes to respond to an authentication/password reset request and comparing that to the amount of time it takes for the system to respond to a request for a valid account. This implies that if an attacker can guess one valid username, then he can guess many more using this same technique. Best of all, it can be fully automated by the attacker.

Why Does This Work?

Many systems typically reveal accounts via trivial inspection, simply by looking at the error message or error code returned by the server as suggested in the above links. Additionally, a number of system designers have decided that user friendliness is more important than account enumeration issues, for example, Atlassian. What remains are mostly systems designed by people that are aware of security requirements and trying to do what the security people recommend. One such security recommendation is that a salted hash function is not enough to protection user passwords: instead, you need a slow one-way function such as PBKDF2, bcrypt, scrypt.

I fully agree with the above recommendation, but along with it we have to recognise that we have opened up an account enumeration vector via timing attacks unless you have carefully protected against it (remark: the crackstation link above does talk about timing attacks, but the one they describe is completely different and quite impractical compared to the one we discuss). This comes because of the typical implementation: when the system goes to verify the user’s credentials, it doesn’t do the slow function computation for accounts that do not exist, whereas it does for accounts that do exist. It’s not too hard to think of a fix for this issue

It is possible that there are similar issues via password reset requests. How long does a system take to send an email used to reset a user’s password? Does this leak whether accounts exist? My initial experiments suggest that yes it does, but for this blog we will focus on authentication only.

How to Test for It?

There are of course many ways to test for it, so I’ll just give you a couple of my favourites.

Let’s start out with some examples using curl, and since the cool people are using json, we shall too. On Linux, the code below demonstrates the time to send a json request for an invalid user name (“nosuchuser”) to example.com.

time curl -H "Content-Type: application/json" -X POST -d '{"username":"nosuchuser","password":"wrongpass"}' https://www.example.com/login

If we know there is an “admin” user at the site, we can do a similar test for this user. What we guess for the password does not matter:

time curl -H "Content-Type: application/json" -X POST -d '{"username":"admin","password":"wrongpass"}' https://www.example.com/login

Tip: If you are using the Burp intercepting proxy, then you can get the curl commands very easily. Just intercept the login attempt, right-click on the request, and select “Copy as curl command”. You can then paste it in the terminal window, but you will need to insert “time ” at the beginning.

On a test system, we are looking for a big timing difference between the two requests. Keep in mind that the time per request will vary from request to request due to numerous factors, but if we see that the slowest time for nosuchuser is much faster than the fastest time for the known user (“admin” in this case), then we know the site is vulnerable. Starting out, you might try say 5 timings for each of a known user and an unknown user: if the site is vulnerable, the timings will leave no doubt in your mind. Once you are sure the site is vulnerable, a single timing for a guessed username will immediately reveal its validity.

I have not tried this on a live system, but depending upon the number of users and servers, I could guess that in some cases (systems with many users and many servers), the signal-to-noise ratio may not be so large, so multiple queries may be needed.

What about Python code? Below is some code using the requests library that I have used for a few pentests in a similar manner as to the curl:

import json import sys import requests import urllib import time # Login to serverurl # Raises an exception if things don't work. def login( auth_url, username, password ): headers = {'Content-Type': 'application/json' } login_values = {"username":username, "password":password} r = requests.post( auth_url, data=json.dumps(login_values), headers=headers ) if r.status_code == 200: return r.cookies else: raise ValueError(json.loads(r.text)['Message']) auth_url="https://www.example.com/login" username="testuser" password="wrongpass" starttime = time.time() * 1000 try: cookie = login( auth_url, username, password ) print "Login succeeded, got cookie :-)" except: print "Failed to login!" endtime = time.time() * 1000 print "Time in milliseconds: ", endtime-starttime

Recommendations

As I alluded to above, there is a trivial fix for this: for accounts that do not exist, do a “dummy” password validation check. This means computing the PBKDF2 / bcrypt / scrypt / whatever on the input data and whatever else you would do for an invalid password before you reject the login attempt.

However, I want to caution the reader because there are more issues here when one uses something like PBKDF2 on the server side, such as denial of service. That’s why I wrote a research paper about this topic.

Last, I want to say that I think account enumeration becomes less of an issue if we have better protections for authentication than just a single password. There have been ideas for better balancing security and usability, such as allowing a single password only when a user is coming from an IP address or a device that he has authenticated with before, and requiring two-factor authentication otherwise. See this and this for examples.