Account enumeration is a type of vulnerability that allows an attacker to verify whether or not a user exists in a particular application. There are several techniques that allow for such distinction and, roughly speaking, if the application behaves in a different way when providing a registered user or an unregistered user, it is said that the application is vulnerable to the account enumeration vulnerability.
This type of vulnerability, as we will see during this blogpost, is more commonly found in features such as “login”, “forgot my password” and “register” — what, however, does not exclude other scenarios.
And why should I worry about that?
Although apparently innocuous, the behavior that makes it possible to enumerate users brings with it an advantage for an attacker in attacks like Brute Force and Social Engineering (e.g. Spear Phishing). Knowing the valid users in an application makes the brute force process more assertive, since part of the login+password challenge is already known. The same happens in the case of Social Engineering attacks, in which an attacker seeks to compromise one (or a restricted set of) victim(s) through a fraudulent interaction; either by email, or SMS, phone, instant message, etc. Knowing that the potential victim has an account in a certain service (and what their login would be) tends to make social engineering more credible.
In addition, several applications use personal data such as CPF (Brazil’s equivalent to social security number) and email as logins in the authentication process. This behavior increases the impact of an account enumeration, since, in certain contexts, this information can be considered sensitive, such as: the enumeration of CPFs/SSNs in private banking, online gambling sites, adult relationship sites, etc.
Finally, there is another aggravating factor: the search for passwords leaked in public leaks (like those seen in recent years) becomes much more assertive.
Knowing that a set of users is registered in an X application, it would be enough for the attacker to search, on leaked bases, for passwords associated with the listed users and test their reuse.
And what is this blogpost all about?
The aim of this blogpost is to illustrate how account enumeration can occur in web applications, from the classic example to some tricks we’ve learned over the years (and of course show how to avoid this).
The authentication form and its various forms of account enumeration
Case 1 — the classic example
The classic example of account enumeration occurs when the application displays two different messages at the time of login, depending on whether or not the user exists in the application. This problem occurs basically because of the way the login function is implemented, in which, depending on whether or not the user exists, one message is chosen over the other.
The code below is vulnerable to account enumeration through the distinction between error messages, in which, if the login provided (in this case the login is the email) is correct and the wrong password, the error message “Invalid password” is displayed, and, if the login is wrong, the message displayed is “invalid email”.
And what would the corrected version of the above code look like? Simply change the error message so that it is the same regardless of whether the user exists or not. This makes it impossible for an attacker to infer whether a user exists in the application using this technique. Follow the corrected code for case 1:
Ok… the argument that this case is hardly seen in modern applications is valid, unless the enumeration of users is intended by the application designer for usability purposes. In fact, the materialization of this case depends on an active effort by the developer to create distinct error messages, which tends to make this case less and less common (although we, who work as pentesters, see this error happen in a more recurrent way than many imagine). And so it goes…
Case 2 — enumeration based on response time
Well, even in the corrected example of case 1, in which the application, regardless of whether the user provided exists or not, brings exactly the same page (and obviously the same error message), it is possible to use a side channel to perform account enumeration.
This usually occurs because the application takes different paths during its execution, depending on the user provided. If one of these paths has a considerably higher computational cost, the response time for the login request can also be considerably higher. What would be considerably higher? The minimum so that, between two requests, it is possible to differentiate which path was taken. Our experience shows that a constant delay of at least 30 milliseconds is enough for an automated account enumeration.
And this is exactly what happens in case 1. If we analyze carefully, we can verify that the hash function used is only executed if the provided user exists in the application (due to the conditional user.exists). Hash functions used to store passwords must be computationally expensive to mitigate offline brute force attacks. Therefore, it is possible to infer that the response time to process a request for which a given user exists in the application is substantially longer than it is in case a user that does not exist is provided. Because of this, through the response time, it is possible to enumerate valid users.
To those who take an extra interest in the subject, I recommend the paper “Time Trial Racing Towards Practical Remote Timing Attacks” by Daniel Mayer and Joel Sandin.
As an example, we took a case we found during a pentest held here in Tempest (note: the vulnerability has already been corrected and the data anonymized). Notice in the image below that the response time for the request, when a valid user was submitted, was 1211 milliseconds:
In contrast, when an unregistered user was submitted, the response time was only 4 milliseconds:
It is also worth noting that, in both requests, the application returns the same 2591 bytes, which makes it impossible to enumerate users through case 1.
“Great! But how to solve the problem in the corrected code of case 1?” Simple: just perform the hash operation regardless of whether the user exists in the application or not. This way, the hash will always be computed and the response time will be approximately the same regardless of the user provided. The code would look like this:
Unlike case 1, I suspect that many of you readers (especially those less familiar with security) were unaware of this type of situation.
Protip — It is relatively common in microservice architectures to perform authentication in 2 phases: first check if the user exists and, if so, perform the login+password check. Given the latency in the consumption of services, this behavior almost always makes it possible to enumerate users through the technique described above.
Case 3 — account blocking
Back to the surface, let’s examine how a “safety measure” can end up being used to compromise the safety of the application.
You’ve probably already come across a login mechanism where, if you make a mistake after N attempts, the account is blocked (sometimes temporarily, sometimes indefinitely). An example of how this mechanism is implemented can be seen in the following code:
What the application designer wants when blocking a user is to prevent the user from suffering a brute force attack, which, let’s face it, is a noble goal. However, by carrying out this blocking in the way described above, some unwanted side effects arise: an attacker could cause a denial of service against a user (deliberately blocking his account), as well as the application could allow for the enumeration of registered users.
“Great! The first side effect I understood, but how could an attacker exploit this behavior to enumerate the users of the application?” Elementary, my dear reader:
1 — The attacker would choose a certain login that he supposes exists in the application;
2 — Using this login, he would try to get authenticated N + 1 times (where N is the maximum number of attempts until blocking, which in most cases occurs after 5 attempts);
3 — If the message “User blocked” is displayed, it means that the user exists in the application; otherwise, it means the user does not exist.
“Great! But how to correct it?”
Well, in general, it is not recommended to block users after a certain amount of invalid logins, but this is a discussion for another blogpost (who knows another day I’ll write about it). So, the recommended thing to avoid this kind of account enumeration would be simply not to block the user.
“Great, but my boss said that I have to block the user for ‘security’ reasons and it doesn’t matter what I say, so what?” So, ultimately, what is enabling the enumeration is the error message, and you don’t need to inform the guy who does not know the login password that the account is blocked. So, to make it impossible to enumerate users in case 3, simply use the same error message, regardless of whether the user is blocked or not. Once the user logs in with the correct login and password, inform that the account is blocked and follow the procedure to perform the unblocking. The corrected code would look like this:
Case 4 — presenting CAPTCHA after N invalid authentication attempts
There is a way for account enumeration that is very similar to case 3, but, instead of blocking the user, the application displays a CAPTCHA after N authentication attempts, but this occurs only for registered users
The vulnerable code is basically the same and the fixes pervade several possibilities. Some approaches are more sophisticated, such as the use of browser fingerprint, others more boring to implement, such as counting authentication attempts, even for users who don’t exist in the application, and some more conservative (from a security perspective), which is always to display the CAPTCHA regardless of whether the user exists or not.
Case 5 — present the 2AF!
This may be the least common case among those demonstrated here and only occurs in applications that use a multi-factor approach to perform authentication. The most common example is when the application, after receiving a valid login, requests the user to present the second authentication factor. If the user provides an invalid login, the application displays a error message. Trivial would be to imagine how to perform the account enumeration in this case. What would a vulnerable code look like?
And the correction? Well, the correction for this scenario is also trivial: either you request the second authentication factor only after the correct password is provided, or you request login, password and second factor on the same screen.
Case 6 — huge passwords
Are you tired, reader? So am I, but the end is approaching, this is the last case of account enumeration in the authentication mechanism. 🙂
Besides being the last example, this is the rarest and most bizarre one. To be honest, I’ve only seen it 2 or 3 times in my life… But I always test it, because let’s suppose the fourth one appears, right?
And what would that scenario look like? It’s simple! Some frameworks and hash functions (many made at the in house style) can’t process passwords with large sizes. By big here imagine something bigger than 10000 bytes. What usually happens is that, when submitting a valid user and a large password, the application tries to process that password and eventually “crashes”, resulting in a 500 error. In practice the 500 response serves as a side channel to identify whether or not the provided user exists in the application.
Here comes the same question: and how to correct it? There are some approaches, some nice ones and some “improvised” ones. Ideally, you should not use hash functions that cannot handle arbitrary sized data. But, in the real world, where you have to kill one lion a day, you can simply use case 2 correction, always computing the hash regardless of whether the user exists or not. The side effect would be that your application would always return a 500 error, but, given the situation, stick the solution and be happy. A last option would be to simply “slice” the password so that it respects the limits of the hash function. The application would have a limitation on the size of the password, which, depending on the entropy of the password, would not be a problem.
Besides the authentication form
Case 7 — I forgot my password (a.k.a. password reset) and sign up
Although we have so far exposed 6 different ways of listing users through the login form, it is important to note that these vulnerabilities are not limited to it. Other features in which, historically, we find several account enumerations are the “forgot my password” mechanism and the “sign up” mechanism.
Similarly to what happens in case 1, it is quite common to find “password recovery” mechanisms that display error messages if the user that has been informed for the recovery does not exist in the system. The registration mechanisms, in turn, do the opposite, displaying an error message if the user is already registered.
The fix of the vulnerability at the password reset is obvious: regardless of the login provided, the message displayed should always be the same, something like “a password reset link has been sent to the registered email address”. Moreover, it is important that this message is displayed before the reset email is sent, otherwise a possibility to enumerate users is opened again, for example, from the time needed for the email to be sent (the SMTP server’s response time is too long… have you figured it out!?).
The correction of the “sign up”, in turn, is not so obvious and requires a slightly higher cost of implementation, in addition to having a restriction: it is necessary to have as the login (username) of the application an email address or telephone number.
The solution is to provide a means of communication, such as an email address or telephone number, while filling out the registration form. Once the means of communication is known, a unique and random link must be sent, which, when clicked, displays a form with the registration data to be filled out. If the user provides a previously registered email or phone number, the application should send a message to this email/phone number alerting about the new registration attempt. Boring, right? That’s part of it.
If you’ve come this far, you may have noticed how difficult it is to avoid an account enumeration. Any small deviations can generate a side channel and end up “leaking” the registered users. Of course, for many applications, having an account enumeration is not a big problem, some applications even assume this as a usability requirement and there is no reason to panic.
However, if your application is not within that group and requires a higher level of security, I suggest that you don’t neglect this possibility. The composition of low-impact vulnerabilities can generate a large and unnecessary concern.
Finally, in addition to the recommendations cited in each case, it is worth remembering that it does not cost much to use CAPTCHAs in login forms, password reset and registration functions. Besides avoiding the automation of brute force attacks, it makes it impossible to enumerate mass users.
I hope you make good use of this reading.