Blog: How Tos

Time based username enumeration

Felipe Solferini 09 Nov 2021

Back in the day, it used to be easy to enumerate email addresses from forgotten password forms. Differences in the response made it easy to check if accounts existed.

After that, you could brute force the password if there weren’t lockouts in place, or if there were, you could lockout a lot of user accounts. Password stuffing from breach data has made compromise easier, though it’s still useful to enumerate accounts in more targeted attacks from time to time.

I was reminded of blind SQL injection, where differences in response time can reveal whether an injected statement was successful or not. This got me thinking about using similar techniques to enumerate usernames.

The issue

A web or mobile application will often include a forgotten password function:

In a classic enumeration attack, an attacker would be looking for differences in the response: “This username is not registered” or “You have been sent a password reset email” are typical. If there is no difference in the content of the response then we need to go deeper.

So we analyse the time the response takes to be processed. Why?

If the account exists, a token will be created and a reset email request generated. This takes more processing time than simply responding with a ‘Does not exist’ error.

If we make multiple requests for accounts that we know do and do not exist, we can compare the response times.

In this case, a response where the account exists takes more than a second to respond when a user was valid. In contrast, responses for invalid requests would take a couple of hundreds of milliseconds, at most.

Here we see identical responses in the content of the response. Nothing useful here.

But here we see a very clear discrepancy between accounts that exist (sorted to appear at the top of the list) and accounts that don’t. The valid ones have a much longer response time.

It is important to try reduce the stress on the server to minimise any noise, so it is recommended to run Intruder without any concurrency and some delay between requests:

It is also recommended that the valid usernames are kept apart from each other in the payload list, to reduce the risk of false positives:

What to do?

The application should be modified to use queues / promises to handle the generation and delivery of reset tokens. If that’s not possible, try to make sure the response time for existing and non-existing accounts is roughly the same by introducing delays. This approach is not a silver bullet and could, if not implemented correctly, be exploited if the server is under heavy load.

Whilst you’re there, it’s also worth checking whether you have an account lockout for multiple failed attempts. If so, consider a time out instead, otherwise you risk an attacker actively locking out users and creating a customer telephone support overhead.