Web App assessments are probably one of the most popular penetration tests performed today. These are so popular that public bug bounty sites such as Hacker One and Bug Crowd offer hundreds of programs for companies wanting to fix vulnerabilities such as XSS, SQL Injection, CSRF, etc. Many companies also host their own bounty programs for reporting web vulnerabilities to a security team. Follow us in our 4-part mini series of blog posts about web security:
To begin, let’s start with some abstract thinking. When we think about attempts to discover web vulnerabilities, we like to think about attack surface. If you are looking for needles in haystacks, it helps if you have access to all of the hay first. This brings us to a common functionality that puts lots of needles in off-limit haystacks:
While vulnerabilities can be classified many different ways, for the purposes of this post let’s use the following two:
Unauthenticated – The vulnerability can be actively exploited without first needing to login or authenticate to the affected application. In other words, “public” areas of the web site/service.
Authenticated – An authenticated state is required for exploitation of the vulnerability. This could come in the form of a username:password, logon token, session cookie, etc.
Normally, through the nature of “more functionality = more flaws”, most vulnerabilities for web applications are authenticated. Some vulnerabilities fall into one of these categories by definition, such as a cross-site request forgery (CSRF) or authentication bypass; but even these vulnerabilities are usually only found with an authenticated context in the first place. Unfortunately when we think about authentication, we commonly associate it with trust. A “hacker” isn’t one of your customers. They don’t login, or use profile pictures, or file uploads, or that hidden query form you built for a good friend. They don’t have a username/password, and you wouldn’t give them one anyways; all they do is break stuff. So if most of my vulnerabilities are only found/exploited by an authenticated user, how can an attacker gain authorized access into my application? This depends on whether the application has open registration or closed registration.
Open registration means that anyone can sign up for a free account (yay, we’re in!) and closed registration means that a user must either be registered by someone else or register using a predefined set of information in order to gain authorized access.
If your application uses an open registration model, an attacker can create a free account and start testing in an authenticated context immediately. We recently tested an application that had a host of high-risk vulnerabilities (including SQL injection) that couldn’t be identified by an unauthenticated user but were exploitable by a user with a free trial account.
Open registration applications should implement an authorization scheme by assigning registered users a low-privileged user “role” that has limited access to the site’s content. User roles are great, but you have to ensure that everything in your application abides by the security controls enforced in the user’s role. Do NOT try to protect content, functionality, or anything else on the application using what is commonly known as “security through obscurity”, because it usually doesn’t work. We’ll be posting an article or two soon that show how we’ve been able to see through the developer’s attempts at obscuring some pretty serious vulnerabilities.
If you have user roles within your application, this means that you probably have an administrative role that has access to much more of the site’s content than a normal user. Attackers will try to determine which content they cannot access, and then try to figure out if the content is protected by access controls, or whether they are being protected simply by hiding it from the normal user’s default views. ALWAYS protect sensitive content through access controls instead of by hiding the content from the low-privileged user.
(Paypal.com is an example of an application that uses open registration, “sign up for free”)
Sweet, my site has closed registration, so that means they have to verify their identity and be pre-registered in our system in order to create an account. This should prevent a lot of attackers from exploiting the vulnerabilities locked behind my login portal…right? Answer: Maybe. If the vast majority of your functionality, and associated vulnerabilities, are only found or exploitable in an authenticated context, then an attacker is going to try to gain authenticated access by the easiest means available. An attacker’s first order of business will be to test your anti-automation protections on the registration, password reset, and login pages. (You have those right?) This is used to determine whether a credential brute-force attack is possible. An attacker will take the path of least resistance when attacking an application, and guessing an existing user’s username and password is frequently much easier than guessing the specific information needed to register a new user’s account, phishing for credentials, or paying for a legitimate account. If an attacker is able to guess an existing user’s account credentials, they not only get access to that user’s data but also gain anonymity by using the compromised user account to discover and exploit additional vulnerabilities within the application.
(bankofamerica.com is an example of an application using closed registration. It requires an applicant to obtain a Bank of America card or account prior to registration)
So an attacker is going to attempt to enumerate usernames, passwords, and other related information from my website. There are many common protections web developers implement to protect against these attacks, some better than others. let’s go through them and discuss effectiveness.
Common Portal Protections
- Account Lockout
- IP blacklisting
- IP rate-limiting
- Username rate-limiting
- Session Control and CSRF embedded tokens
- Ambiguous server responses
- Strong Password Policy
- Dual-Factor Authentication
At the top of each protection section, we’ve provided a simple chart to show whether the type of protection is likely to prevent certain types of attackers from conducting either username discovery, or brute-force login attacks. Here’s the Legend:
– The protection will most likely prevent a successful attack from the specified source.
– The protection will most likely NOT prevent a successful attack from the specified source.
– The protection will most likely prevent a successful attack from the specified source, but there are exceptions/bypasses that could make this attack exploitable.
– The protection will most likely NOT prevent a successful attack from the specified source and can potentially be used to perform additional attacks against the users or support personnel of the application.
The chart may make it appear that CAPTCHAs are the silver bullet of portal protection, but everything depends of whether or not the application has implemented CAPTCHAs correctly. The results of the chart also only apply when ALL aspects of your portal are protected by CAPTCHAs. I wrote more about proper CAPTCHA implementation in this blog post.
If implemented correctly, an attacker will likely move on to another site that has less protections.
A very popular choice in many of the applications we test, but there are a couple of issues that you need to watch out for when implementing account lockouts:
Username Enumeration using account lockout response messages
If the server responds with a message indicating that the account has been locked, an attacker can leverage this to determine valid usernames. One of the ways to implement account lockouts on an application is to never display an account lockout message to the user, but instead send an account lockout email to the user’s email address on file notifying them that their account has been locked due to excessive login attempts. The server should still only display the message “Either your username or password was incorrect” when an account has been locked so that the attacker cannot determine if they have guessed a correct username. The gotcha in this scenario is that you must rate-limit the account lockout emails being sent to the user’s email address or else you might end up sending several thousand lockout emails to your poor user if the attacker is trying several thousand passwords and doesn’t care (or doesn’t check) that the application says that the user’s account has been locked. Also be mindful of response times, if your application provides insight into whether extra actions are being performed for a specific request, this could tip an attacker off. More info about this in a later blog post.
In our opinion, the best protection is to lock out any username that is submitted for login more than 5 times even if the username is not registered in the system. Some of the best applications we test have implemented this protection and it makes it nearly impossible to determine if a username is valid or not.
Denial of Service using account lockout
The larger issue with account lockout protections is that a malicious attacker can cause targeted denial of service attacks against users of the application. These types of targeted attacks can potentially cause a monetary hit, customer loss, or swamp their support personnel with requests from users. Let’s discuss the impact on the “availability” of the application when an application enforces different types of account lockout:
Denial of Service against different types of account lockout:
- Permanent Account Lockout with self-recovery (requires the user to reset their password)
- An attacker can lockout the user’s account once every X hours forcing a user to reset their account. Brute-Force risk is reduced if the user is able to change their username to a new value unknown to the attacker during the unlock process.
- Permanent Account Lockout without self-recovery (requires the user to contact a support agent to unlock their account)
- An attacker can lockout the user’s account once every X hours and because there is no self-service recovery option available to the user, they have to contact a support representative of the company in order to unlock their account. This could cause a denial of service situation on both the user and the company’s support staff. The impact is worse if the support staff have limited availability (Mon – Fri 8-5), or if they are suddenly swamped with calls from thousands of users who have had their accounts locked.
- Temporary Account Lockout with self-recovery (1 – 30 minutes)
- An attacker can lockout the user’s account every X minutes indefinitely to keep the user permanently locked out of their account.
- The attacker can still guess a few passwords every time the account unlocks. If it unlocks every 30 minutes, and the application allows 5 attempts before locking the account, an attacker can still try around 240 passwords a day in an attempt to maintain a lock on the account. If a correct password is guessed, the attacker can then access the user’s account and will likely be able to change the user’s password and email address to permanently lock the original owner out of their own account.
- Risk is reduced if the user is able to reset their own password and unlock their account at the same time, but unless they are given the ability to change their username, an attacker will still be able to keep their account locked and they will be frustrated at needing to constantly reset their password.
- Temporary Account Lockout without self-recovery (typically 1 – 30 minutes)
- An attacker can lockout the user’s account every X minutes indefinitely (…)
- The attacker can still guess a few passwords every time the account unlocks (…)
- Because there is no self-service recovery option available to the user, they have to contact a support representative of the company in order to unlock their account. If a company has instituted a temporary account lockout, they may not have the support necessary to unlock the user’s account because they are relying on the “temporary” nature of the account lockout to allow the user eventual access to their account. This situation would likely allow the attacker to maintain a permanent lock on the user’s account and allow them to continue guessing passwords until the the correct password is found.
We can’t write a section like that without showing an example of account lockout gone wrong can we?
Here’s an example of the response we received from a recent assessment when we locked out our test account using incorrect passwords. Notice that the server responds to the login request with an account lockout message and tells us to try again in 20 minutes. Awesome, this means we know the username is valid, and that the application only locks out the account for 20 minutes.
Wait a minute, immediately after locking the account, we tried the account’s actual password and….received a different response from the server…
Didn’t we just lock the account? Why is the application now saying that our login information is invalid rather than saying that we’ve been locked out?
It turns out that the application only returned a message stating that the account had been locked if you attempted to login using an incorrect password. If you tried the account’s correct password, the server would respond saying that the login information was invalid. Well, well, well…so even though we’ve locked the account, we can still guess the user’s correct password? Sounds too good to be true! I can lock the user’s account, keep them locked out until I’ve discovered their password, then wait the 20 minutes for the account to unlock and then login as them? Yeah.
Here’s the output from our custom script used to bypass the CSRF protections on the login page and obtain the server’s responses to each login request:
All of the above scenarios require an attacker to be able to identify valid usernames on the application, so that’s why it’s so important that you aren’t giving away usernames for free. Check back next week to read about how we discover valid usernames.
This is a fun one. We try to be stealthy, but being blacklisted does occasionally happen during an assessment. When this happens, a simple program like hidemyass or torguard can switch our IP to another in just a few seconds and we’re right back on track. Since we’ve been talking about denial of service in this post already, let’s consider the potential problems with IP Blacklisting:
If the application permanently blacklists an IP address found to be conducting automated login attempts, a malicious attacker might decide to do some open source research about the company and find out who some of their biggest customers might be, do some more research and find out what IP addresses are used by their customers, and then start spoofing those IP addresses in automated login attempts. End result: the company can potentially blacklist the IPs for some of their biggest customers. This does end up being a blind attack since the IP address is spoofed and won’t ever return to the attacker, but it is a valid theoretical attack vector for targeted denial of service. Alternatively, the attacker could just start sending automated login attempts while spoofing whole IP ranges in the hopes of overloading the security device’s blacklist and cause the site to become inaccessible to large swathes of the internet at a time.
If you are implementing IP blacklisting, a temporary enforcement will help prevent the long-term risks of spoofed IP Denial of Service attacks, but unfortunately you won’t be able to prevent a recurring attack from a determined attacker unless you also implement a whitelist for IPs that should never be blocked. In the whitelist scenario, if an attacker is able to gain access to an IP address in the whitelisted range, your blacklisting becomes immediately ineffective.
We’ve run across several sites that perform rate-limiting on login requests, but this protection is ultimately ineffective. The application may allow only 5 login attempts per minute, but an attacker can easily determine your rate limits and make the login script abide by the application’s rules. There are 1440 minutes in a day, so that means an attacker could attempt 7200 passwords every 24 hours against a single username. However, a competent attacker will be able to bypass the IP restrictions by multi-threading the login script and using multiple proxies to perform automated login requests. By using multiple proxies, an attacker can perform as many requests as they could in a non-rate-limited application. It does add an additional level of complexity for the attacker to circumvent, so it can still be useful when preventing script kiddies and amateur hackers from attacking your application.
We’ve also seen some applications that don’t rate-limit based upon the login submission’s IP address, but instead rate-limit on the username submitted in the login request. Assuming an attacker just wants access to one of many possible accounts, this can be bypassed by cycling through the hundreds/thousands of usernames and performing login requests in a different loop order:
Typical loop order for credential brute force guessing attacks:
# This sequence tries all of the passwords on a single username before moving on to the next username in the list for username in usernames: # Switch the username for password in passwords:# Switch the password login(username, password)
username rate-limited loop order for credential brute force guessing attacks:
# This sequence tries all of the usernames with a single password before moving on to the next password in the list for password in passwords: # Switch the password for username in usernames: # Switch the username login(username, password)
Session Control and CSRF embedded tokens
Some applications try to prevent automated login attempts by requiring the user to have multiple session cookies in place prior to submitting a login request, or to submit a one-time-use (CSRF) token embedded in the login page. If the submission does not contain the proper session cookie combination/tokens, or is trying to reuse the same session cookie combinations/tokens on multiple login attempts, the application causes the login attempt to fail. The ideology behind this type of protection stems from the flawed notion that automated attacks do not following the “proper” sequence of page loads when submitting a request, but instead will simply repeat the same request over and over again while changing the username/password. That thinking is true for script kiddies and amateurs, but will not prevent a knowledgeable attacker from figuring out your required sequence.
While it can be a PITA for an attacker to figure out the application’s required configuration, this type of protection is just an implementation of “security through obscurity”. If the attacker can determine which sequence of page loads will set the required session cookies, they can simply repeat the required sequence of requests in order to obtain the proper session cookies prior to each submission. Alternatively, they can get the login page and retrieve the one-time-use token embedded therein for the same purpose. This process can slow down an automated credential brute-forcing attack, but proper threading makes “slowing it down” a moot point.
Ambiguous Server Responses
Server responses should NOT change based upon the validity of the submitted username or email address. If a user attempts to login with the username “jsmith”, but “jsmith” is not a registered username in the application, the server should respond in exactly the same way that it would respond if the user attempted to login with an incorrect password to the registered username of “jasonsmith”. The same goes for server responses on a password reset request, or a “forgot my username” request. Here are examples of some ambiguous server response messages:
- Login Request:
- “Invalid credentials”
- “Username or password was incorrect”
- Password Reset Request:
- “If the username/email submitted is registered with our application, a password reset email will be sent to the account’s registered email address shortly”
- “Check the submitted account’s email address for a password reset email”
- Forgot username Request:
- “If the submitted email address is registered in our system, you should receive an email containing your account’s username shortly”
- “Check your email address for your registered username”
Example of an application that allows an attacker to identify a valid username when logging in:
Valid Username Invalid Username
When implementing ambiguous responses as a security control, you must also ensure that your sever response times are not allowing an attacker to infer valid usernames. See our Username Discovery post to see how server response times can be used to determine valid usernames even with ambiguous server response messages.
Strong Password Policy
A strong password policy protects yourself just as much as protecting the user from their own stupidity. By allowing users to choose weak passwords, you are opening up an easy means for an attacker to gain authorized access into your application. Many users choose the path of least resistance when it comes to choosing passwords, so requiring them to choose good passwords can help prevent an attacker from gaining access to user’s accounts (and authorized access to your application) by guessing the user’s password of “password”. Strong password policies will help some users to choose strong passwords, but if their “strong” password ends up being something like “P@ssword123”, then I’m afraid they’re still going to be compromised if other brute-force login preventions are not in place.
Dual-Factor authentication is becoming more and more popular as a security control on login portals due to their effectiveness in preventing brute-force logins. An application that has implemented dual-factor authentication will prevent an attacker from gaining authenticated access to their application even if they can guess or obtain the user’s username and password.
Some applications perform this secondary check by requiring the user to answer security questions. You must be careful when using this method because some users choose security questions that are easily discover-able through open source research. Ensure that your server responses aren’t giving away the answers to these questions:
This application uses security questions as a method of dual-factor authentication, but has no anti-automation controls and also reveals when one question is correctly guessed.
Burp Intruder was used to brute-force guess the user’s security questions. Notice that the user’s answers become conspicuous when viewing the error messages in the server responses
It is important to implement brute-force automation controls on these secondary auth checks. We recently conducted an assessment on an application that had implemented a secondary PIN entry for a user’s account when their credentials were guessed correctly. The PIN was 4 digits and did not have any form of anti-automation controls in place for brute-forcing it. There are only 10,000 possible 4-digit combinations, so all of the accounts PINs were guessed in a matter of minutes. Actually, most of the accounts on this particular application were created prior to the dual-factor PIN implementation so when we correctly guessed their password of “password”, the application prompted us to set up the account’s security PIN in order to protect their account from being accessed by an attacker…wow 🙂
Probably the best dual-factor implementation is through token generators (like facebook’s code generator, or authy, or duo, etc) that require the user to input a code generated from an app, or accept a push notification to their mobile device. These types of dual-factor authentication apps make it extremely difficult for an attacker to gain authenticated access to the application, but most companies don’t implement them because it makes the application too difficult to use for the average user.
So which protections are the best? The more the better!
Implement CAPTCHAs, dual factor authentication, strong password policies, ambiguous server responses, session controls, IP blacklisting/rate-limiting, and any other protections discussed here or elsewhere. The best applications we test use a combination of many of these controls to prevent an attacker from discovering usernames, guessing credentials, and brute-forcing any form of dual-factor authentication challenges. Just make sure that you implement these controls correctly. This post has highlighted some of the mistakes that developers can make when implementing these controls, but there are a myriad of other mistakes that can be made when attempting to secure your portal.
Finally, there’s always a trade-off between “ease-of-use” and “security”. Many of these controls are viewed by the marketing/sales teams as “detrimental to our cash-flow” and you’ll have a fight on your hands when you try to implement them. If you need some help convincing the naysayers, or if you’re just curious to see where your application’s security stands, fill out the request form at the top of this page, or contact us at (801) 855-6599 for a quote on getting your application tested.