Mass Account Pwning or How we hacked multiple user accounts using weak reset tokens for passwords

Riyaz Walikar
Appsecco
Published in
6 min readSep 20, 2016

--

We chained a couple of vulnerabilities, reverse engineered tokens, used a common attacker technique to get more sample data and finally gain access to the user accounts of a fairly large international travel company. This post talks about the attack and also serves as a cautionary tale about what can go wrong when developers decide to cook up their own “random” tokens rather than relying on what is available in the framework.

During a recent pentest, we came across a seemingly secure application that supported user login and password resets. As part of the testing we generated several password reset requests using accounts that we had created for the test. We then stacked the password reset tokens for different accounts and discovered a pattern in their creation.

We used Burp Intruder to generate the password reset requests and Burp Sequencer to analyze the tokens that were generated. Using automation at a time like this can be real time saver.

Secondly, when poking around web applications, we always do some Google dorking. If you are not familiar with what is Google Dorking, it is the technique of crafting nifty search phrases using special Google search operators to restrict search results to find interesting things. Although not very difficult to perform, this is an often underappreciated technique. The data that was required to complete the attack was discovered using specially crafted Google searches.

In fact, one of the main security testing that we use at Appsecco for our client work is to passively find information, weaknesses, vulnerabilities and attack vectors, without ever sending any data to the actual applications.

In the case of this application, the “Forgot Password” functionality accepted a username and was designed to generate a link that was sent to the registered email address, if the username was found on the server. The user would then click on the link and would be taken to a page where a new password would be accepted. Pretty standard stuff, right?

The discovery and analysis

The glitch in the mechanism was that for multiple password reset attempts for a given username the same seemingly random password reset token was generated. The password reset was attempted again after a period of 25 hours to check if the token was time bound but it turned out that the token remained the same. The token appeared to contain parts of the user’s account_id and the username.

We generated password reset tokens for multiple accounts, stacked them and performed a quick analysis. After examination it was noticed that the tokens were generated using the following (pseudo) algorithm:

account_id + '|' + [first 2 characters of the username] + [first 3 characters of account_id] + [one of a-z or 0–9]

For example:

account_id = 432jump2016 username = jumpingjackpassword reset token =  432jump2016|ju432h 

If we just have the account id of a user, we could generate the password reset token and we didn’t even need access to the password reset email!

A quick Google search for account id returned 1000s of results.

Using specific search operators we could collect account_id values from the search results.

site:app.example.org inurl:profile inurl:prodid

The search result looked something like this. Obviously changed by me to protect the guilty and innocent.

https://app.example.org/profile/432jump2016?prodid=efda782gh

We ended up building a Google Custom Search Engine to get updated results but I guess that is a story for another day :)

The exploit

We wrote a python script to generate all possible token combinations and sent them to the server. The script (given below) read from a file called tokens.txt that contained all possible string values that could be generated using the algorithm. The output was a list of URLs for all the accounts that were tested.

import requests
url = "https://app.example.org/User/ChangePass?token="
i = 1
with open('tokens.txt') as f:
for line in f.read().splitlines():
url = url + line.strip()
r = requests.get(url)
if int(r.headers['content-length']) > 0:
print "["+str(i)+"]:"+url
i = i + 1

We were able to gain valid working tokens for all the accounts that we attempted this on!

We were very tempted to but we did not change the password of any user except for accounts that we had created. These accounts contain lots of user specific sensitive information like complete names, home addresses, scans of personal documents and reports.

This would have been considered a serious breach if attackers had got here first since Personally identifiable information (PII) is heavily regulated in most countries.

Is this common?

Well, in a way it is pretty common in the cases where developers implement their own algorithm to build things instead of using safe libraries out there.

http://xkcd.com/221/

Password reset functionalities can be broken either due to weaknesses in implementation or the simplicity of the user’s settings. For example, the simplest case of a password reset mechanism is when a user provides his username and the application asks the user a ‘secret question’ whose answer only the user is expected to know. Most users for the sake of simplicity end up using really simple question and answers. Given the prevalence of social media, it is not very difficult for an attacker to figure out the answer to a rudimentary question like What is your favorite pet’s name? or something that would require additional Googling like Where did you meet your spouse?

A more advanced version of this mechanism is to have multiple questions but then again the protection that this offers is only as good as the answers that the user has setup.

So, what can a secure password reset functionality look like?

If you are an application developer or owner, inquire if your application enforces the following in the password reset mechanism. This checklist is derived from the OWASP Forgot Password Cheat Sheet:

  1. Accept the username as an input parameter.
  2. Present a message that say something on the lines of “Password reset link has been sent to your registered email address” regardless of whether the username was found or not.
  3. Send a short lived (ideally expiring within hours) unique, one time use link to the registered email of the user if the username is found.
  4. This unique link contains a token that is generated at runtime using enough complexity (alphanumeric), length and randomness to prevent this from being guessed. The token should never be reused.
  5. Allow the user to reset the password when that link is visited while disallowing the user to use the last 3 passwords and enforcing password complexity.
  6. Redirect the user back to the login page where the user can authenticate with the new password and proceed.

The idea is to use an out of band channel, like email or 2 factor auth via mobile etc., for communication while ensuring that the password reset token meets all criteria of randomness and complexity. The bit about the token being random and complex is important to avoid exactly the kind of attack that we discussed in this post. The more random and complex the token, the lesser the chances of someone figuring out how the token is generated or used.

That brings us to the end of this post. Let us know how you test for the “Forgotten Password” feature in web applications in the comments below or if we could have done something different!

Happy Hacking!!

Thanks for reading this article. If you enjoyed it please let us know by clicking that little heart below.

--

--