An account enumeration attack involves an attacker attempting an action, such as authentication or password reset, and looking for differences between responses to gain information on the system. Learn how to harden your SSO solution against these attacks.
Security and Single Sign On (SSO) is a complicated space, filled with dry RFCs and conflicting or outdated recommendations or practices. Often developers find themselves facing a steep learning curve to bring a SSO solution to production. To fully grasp this space, you must not only have a deep understanding at the protocol level, but also of how systems might be vulnerable end- to- end.
You can use the most cutting-edge encryption on your system, but a chain is only as strong as its weakest link. If your UI/API is vulnerable to something like SQL injection, then all that work might be for nothing. While a lot of frameworks such as EF protect you against this, it’s easy to forget about additional attack vectors when they’ve been handled for you for so long. And when, for whatever reason, you must roll your own implementation, it’s easy to leave yourself vulnerable.
At RSK we often see these vulnerabilities with the migration from IdentityServer3 to IdentityServer4. One of the major differences between IdentityServer3 and IdentityServer4 is the UI. IdentityServer3 had a built- in Login UI that was rather inflexible, so with version 4 the UI was removed so developers have total control to create their own and brand it accordingly. Because of this, we often see solutions vulnerable to attacks, such as account enumeration.
What is Account Enumeration?
An account enumeration attack involves an attacker attempting an action, such as authentication or password reset, and looking for differences between responses to gain information on the system.
Here we have a simple application with a login page. In this first scenario, I am an attacker and I’ve got a list of usernames and passwords I want to test against this application.
I attempt a login and get an “Invalid username” error:
I try another account and get an “Invalid password” message:
Here I can see from the two different responses that one tells me an account exists, while the other does not. This is the simplest form of account enumeration possible. After an attempted authentication the application tells the user whether the account they used is valid or not.
You might question what the danger of this is. There is not much you can do from the login page without a password. In fact, it could be useful to users who are bad at remembering all the different accounts they have and be a deliberate UX decision.
After all who cares if someone can verify an account on my tiny website? Well knowing if an account exists is already 50% of the puzzle to an attacker.
A common technique used with stolen credentials is password spraying. Attackers will have a list of known credentials and try as many as possible against multiple sites. As users often use the same emails and password combinations across different sites, one breach might be all that is needed. Simply figuring out if someone is signed up to an account or not allows potential attackers to know if they are wasting their time password spraying, cutting down the number of accounts they need to try. Some sites might also be of a more sensitive nature, and simply confirming whether an account exists or not might be all the information an attacker requires in order to further exploit users.
So, to fix this we make the error message intentionally vague as “Invalid username and password”:
On the surface it seems like the issue is fixed, right?
Beyond Error Messages
Now at first glance it may seem like there is no difference between the responses for accounts that exist and those that do not. However, if we examine the actual HTTP requests and responses themselves you might spot something.
First the account that we know exists on this site shows a response time of 386ms:
And now the account that we know does not exist shows a quicker response time of 195ms:
If we look at exactly what this login function is doing, we can see that our application will only hash and attempt to contact the database and validate the password if the account exists. This might seem like the correct thing to do in development. After all, you want your site to be fast and responsive, so avoiding unnecessary database calls is a natural thing to do.
public async Task<TUser> Login(string username, string password)
{
TUser user = await userManager.FindByNameAsync(username);
if (user == null || user.IsDeleted)
{
throw new LoginUserException(outcome: LoginResult.InvalidCredentials);
}
if (!user.EmailConfirmed)
{
await mailerService.SendAccountReregisterEmail(user.Email);
throw new LoginUserException(outcome: LoginResult.InvalidCredentials);
}
if (user.IsBlocked)
{
throw new LoginUserException(outcome: LoginResult.InvalidCredentials);
}
if (await userManager.IsLockedOutAsync(user))
{
throw new LoginUserException(outcome: LoginResult.InvalidCredentials);
}
if (!await userManager.CheckPasswordAsync(user, password))
{
await userManager. AccessFailedAsync(user);
if (await userManager.IsLockedOutAsync(user))
{
await mailerService.SendAccountLockoutEmail(user.AccountLockoutEmail(user.Email, user.LockoutEnd); }
}
throw new LoginUserException(outcome: LoginResult.InvalidCredentials);
}
await userManager.ResetAccessFailedCountAsync(user);
return user;
}
However, the time it takes to respond is slightly shorter when the account does not exist due to this. This might not seem like much, but this sample site is using an in-memory database. In a production instance this difference would be easier to spot:
Password hashing algorithms are deliberately slow to prevent brute force attacks. Depending on how strong the password hashing algorithm being used is, this should be anywhere from 1-1.5 seconds.
If we modify the code to always hash and check the password on every account, we’re always going to get back similar response times.
Lockouts
In the next account enumeration scenario, I have tried too many passwords for this account, and I’ve received a lockout message. My sample site is set to lock out a user and notify them that their account has been locked after 5 failed login attempts. This message tells potential attackers that there is an account with my email on the site:
We can fix this vulnerability by locking the account and notifying the user by email, but showing them the same vague “Invalid username or password” message in the UI:
Just Login?
It is not just the login page that can be attacked. Any public facing flow can be vulnerable.
If we attempt to reset a password for one of our accounts and see an “Invalid account” message, we are still just as vulnerable to account enumeration as before:
A generic success message should be returned even if the account does not exist. There should be no differences on the page between a successful or failed password reset.
Emails and Response Times
Looking at the response times of a password reset request for an account that exists and one that does not, you may see that again responses times are different.
On a successful password reset request the server will send an email to the given address. On a failed request, no account exists and therefore no email is sent. This response time difference is then exploitable by enumeration. The simplest and most effective way to deal with this is to always send an email, regardless of whether there is an account for the email or not, informing the email address owner that someone has requested a password reset for their email. This could also be a UX improvement, since people often have multiple email addresses so letting them know that no account exists for this email might save you a support ticket down the line.
You might have seen a email like this before when in this situation:
“A password reset was requested for this email address, but you do not have an account. If you expected this email, try a different email”
Cookies
Our final account enumeration scenario is around cookies. In my site I have implemented a password reset flow. Users can enter an email address and be sent an email containing a code to enter into the page they were just on. A cookie is set on the browser containing the password reset code, binding the reset request to the browser session.
If we look in the Post request made when we request a password reset and zoom into the response headers, you can see the Set-Cookie
header:
But when no account exists, there’s no code generated, and I don’t set a cookie. So, no Set-Cookie
header is returned:
This again is open to account enumeration. The fix here would be set a cookie in both cases. Keep in mind however, different sized cookies may also be exploitable. We recommend padding out both cookies to a certain size.
Summary
There can be many different vectors of attack for enumeration. Some attack vectors are more obvious than others, and some will depend on how you have implemented your public flows.
The key things to look for include:
- Error messages
- Differences in the page/HTML
- Response time
- Cookies
However so far, we’ve only looked at the UI. As I mentioned earlier, any public facing flow can be vulnerable to account enumeration, including APIs. It’s important to cover every endpoint that could be vulnerable. It only takes one poorly made system to open the flood gates for attackers.
Some of these changes might negatively affect the UX of your site, and that is the trade-off that you must evaluate as a developer. Before single sign on, these attacks might not have been as useful. But now with SSO there is one account and site to attack, potentially breaching everything. This makes the account a prime target and any information gleaned is much more valuable to attackers. We recommend evaluating and hardening your security solutions against every possible attack vector before pushing to production.
Alongside IdentityServer4 components and custom buildouts, Rock Solid Knowledge also provide other services such as production support and health checks. We designed these health checks to give customers the confidence in their solution before they move to production.
During a health check our Identity Team review your system architecture from security standpoint and examine the provided codebase in detail. We then produce a report with graded recommendations. Each recommendation has a priority and impact rating from low to high. These recommendations focus on the security aspect of a code base, rather than the code itself.
Account enumeration is one of the many different types of exploits or weaknesses we look for when evaluating IdentityServer4 solutions. Recommendations for account enumeration are usually classed as a high priority but low impact. This means we recommend you fix them before moving to production, but the overall impact on the code or time cost is low.
If you are interested in getting an IdentityServer4 health check or learning more about our other services and consultancy, please email us at [email protected]