Introduction During a recent security audit, something related to password reset functionality caught my attention. It appealed that password reset tokens generated for a given account in the same second were always the same. Such behaviour suggested that password reset token is generated from a few factors, but only one of them – time wasn't static.
Discovery and verification The first sign of weak password reset token entropy was the fact that tokens generated for the same user during the same second were the same. That meant that time was the only factor that changed in the token itself. Other factors were always the same and either static or related to a given user. With that discovery, I began investigating further how the token could be constructed.
The first step was to obtain a valid timestamp that was used by the web server. In order to do that, I sent the following HTTP request that requested password reset action:

Server confirmed that password reset token was sent and returned date in its header:

The date returned by the server will be used in further steps – it would be the one used in the process of generating the password reset token.
The reset token received in the e-mail was:

The reset token was a result of a MD5 hashing function, so the first thing that I've checked was conjunctions of timestamp/date with just an e-mail address. Results unluckily weren’t positive – generated token must've had at least a third factor. An example of some tried conjunctions are shown below:

Knowing that there's a third unknown factor, we'll use "advanced password recovery" software, which is hashcat. Provided that we know part of the original string, we'll use a combinatory attack mode that will combine two dictionaries – the first dictionary will consist of known permutations of date and e-mail address, the second will be just a standard dictionary used to break hashes (in this example rockyou.txt). The final command is presented below:

After running the command, I noticed that hashcat successfully broke the hash:

It's visible that third factor was just a word secret.
The tested application creates a token from three factors – user's e-mail address, time of password reset request response and pepper (which is a secret added to an input during hashing with a cryptographic hash function). In this case, developers choose secret to be a dictionary word, which is fairly easy to crack.
In order to verify that the vulnerability was identified correctly and identified password reset token structure is correct, I tried to exploit the whole process and gain access to my test account, by generating new password reset token and creating it from scratch.
First, I issued a standard password reset request:

Then I copied the date from the response and transformed it into timestamp:

After that, I created my own token and used MD5 hash function:

The next step was verifying whether I will be able to successfully reset password with created token: Server responded with confirmation of password reset – confirming that vulnerability was properly identified and can be easily exploited:

Server responded with confirmation of password reset – confirming that vulnerability was properly identified and can be easily exploited:

The last step was doing the same thing, but with the admin's account. I requested password reset for an application admin account and was able to successfully reset its password – gaining the highest possible privileges in the application from an unauthorized user.
Recommendations When generating password reset token ensure that generated tokens or codes are: • Randomly generated using a cryptographically safe algorithm,
• Sufficiently long to protect against brute-force attacks,
• Stored securely,
• Single use and expire after an appropriate period.