On a recent pen test, I ran across a web app that implemented poor "forgot password" functionality. After a lot of manual testing and a custom python script, I ended up gaining administrative access to the portal. This is another example of a type of vulnerability that does not show up on vulnerability scans and why hiring pen testers is a better idea than simply relying on the output of those scans.
This particular app accepted a username and password as you can see here:
When I clicked on the forgot password button, I was presented with this:
Interesting. A hint question that is blank. I wondered if I would get a valid question with a valid account? Well, I didn't have a valid account but had a few ideas :) After a few obvious guesses, we see a valid question:
This is pretty interesting for a number of reasons. First, there are different responses for valid accounts and invalid accounts which means we could enumerate accounts. More on that later. The other interesting thing is the question is about the user's "favorite color." How many possible colors are there? Maybe 20? Hard to figure this one out? Nope! If the user was smart when they saw this, they'd simply use an answer that was NOT a color but it became apparent that most did not adopt that strategy. After about 30 seconds, I had guessed the right color and was presented with a password change box?!
Hopefully you can see why this is a fundamentally broken way to handle password resets. Not only can we enumerate the users, we can also brute force the answers to easy questions, and finally, we can reset the password to anything we want. There is no email link to the users inbox to verify anything at all.
Now that I understood the functionality of the web app, I wrote a python script to automate the process of enumerating valid user accounts. The script reads a list of usernames from a text file, sends the login name to the "forget password" function of the app. Then, it parses the response from the server using a regex function in python. If the parsed response has anything after the "Hint Question:" we know we probably have a valid user account.
I've sanitized the script and tried to generalize it so that others can use it in the future. I built in basic error handling. If the connection dies while you're brute forcing, the script will know the last word that was tried and pick up from there when you fire the script off again.
For this to work, you need to find the difference in responses from the server for a good query and a bad query. When you figure that out, put a string of text into the regex portion of the script that is unique to the good query. The way I did this was review both the good and bad responses from the server and used the comparison tool built into burp.
When you run the script the output will look like this:
In my situation, I was able to enumerate 82 valid user accounts. Of those 82 accounts a large chunk of them had easily guessed password hint questions. I was able to run through each of them and ended up getting an account that had full administrative control over the app. Game Over.
After gaining admin control I also found another very old problem, masked passwords on web forms. After de-masking all the passwords, I now had over a hundred usernames and passwords which were used on other sites and services.
My script can be found here. Keep in mind, I suck at programming and there might be better ways to do this but at a minimum, it can be used as a framework.