You know the deal. A password recovery form takes in a username and responds with either, "Check your email for a link to reset your password" or "Invalid username attempted." Maybe it's the registration page and when you attempt to register an existing username you get, "Sorry, the username you requested already exists. Please choose another." Using inference based on the application's responses, one can ascertain the existence of usernames. Here are some examples on purposely vulnerable training applications:
|ACUNETIX ART Registration Form Username Disclosure|
|CrackMeBank Registration Form Username Disclosure|
|Google Gruyere Registration Form Username Disclosure|
|Altoro Mutual Login Form Username Disclosure (Valid Username)|
|Altoro Mutual Login Form Username Disclosure (Invalid Username)|
|WordPress User Harvesting Google Search|
|Example WordPress User Harvesting (author=1)|
|Example WordPress User Harvesting (author=2)|
|CrackMeBank User Harvesting Attack Using Burp Intruder|
- First I want to know if the application has an account lockout policy. If so, I try to determine how many invalid login attempts it takes to trigger a lockout. I don't want to anger my client and lockout hundreds of their users so I limit the attempted passwords to one or two less than it takes to lock accounts out.
- Second, I follow the application's password length and complexity policies so I'm not wasting my attempts. If the minimum password length is 8, I use passwords of eight or more characters. If the password complexity policy requires mixed-case, alphanumeric, and special characters, I use password values that meet those requirements.
- Third, I utilize widely available lists of passwords that are in order of prevalence and have been observed in password dumps gained from various, successful real-world hacks. I just pick the top few passwords that match my target application's password composition policies. Mark Burnett has a really cool tag cloud image of the top 500 passwords according to his research. Please excuse some of the more tasteless values as it appears many in our species are still operating on a fairly base level, obsessed with genitalia and reproductive activities.
|Mark Burnett's Top 500 Password Tag Cloud|
|CrackMeBank Horizontal Dictionary Attack Using Burp Intruder|
Imagine another scenario, which I ran across earlier this year in an assessment: I compromise dozens of accounts using this horizontal dictionary attack on a heavily-used site that allows users to store their payment information. Against PCI regulation, the application outputs the full PAN with name, address, exp date, and security code on the "edit my payment information" page. Instead of being mister-good-guy-pen-tester I'm some kid who holds a nasty grudge against this company. Instead of using the payment information to buy a plasma and some air jordans, I dump the user creds and cardholder data on pastebin.com. I don't compromise the web server at all but the company is subject to PCI fines and is mentioned in the news causing some damage to the brand.
Enough with the fear mongering then. What are some simple things we can do to stop this type of thing? As always, I like to see controls at multiple levels, starting at the root cause, so that if one fails, another steps in to save the day. From secure coding (avoiding logic flaws), to CAPTCHAs, and then even WAFs let's break it down:
- Use CAPTCHAs: At the very least, use decent CAPTCHAs on registration and password/username recovery forms. This makes it pretty difficult to get that initial dictionary attack running in order to harvest usernames. I know there are business and useability reasons to avoid CAPTCHAs but use them on your login forms if you can get away with it. CAPTCHAs don't solve underlying vulnerabilities but they prevent automated attacks.
- Be Discerning: Require more than one piece of unique information to initiate password/username recovery. Ask for a username, email address, zip code, and last four digits of a phone number. I remember one utility payment application I assessed that simply asked for a username and zip code. I simply looked up all the zip codes that the utility company serviced, picked a few of the most populated ones, and harvested away.
- Don't Be Too Friendly: Avoid detailed errors about what the user did wrong if possible. On a login form, it definitely is not necessary to provide two different error messages based on whether the username or password were incorrect. For password/username recovery, be generic. "Something went wrong" will suffice most of the time.
- Enforce Password Requirements: Make sure users are required to enter secure passwords. Eight characters or more, with alphanumeric, mixed-case, and special characters required makes my job a lot harder than allowing "123456." You might even go ahead and blacklist really bad passwords. Most really bad passwords won't meet the requirements above but it's worth noting that "P@55w0rd" is not much better than "password."
- Use Two-Factor Auth: If you're serious about protecting your users, use two-factor authentication. It's not all about a physical token any more as there are plenty of other options. They each have their flaws but they do stop this type of attack.
- Lock Them Out: Enforce account lockout after 3-5 invalid login attempts.
- Throttle or Block Offenders: You shouldn't see 1000 or 100 or even 25 HTTP requests/second from the same session. Use your WAF, IPS, or even a web server module to limit concurrent connections if possible.