WordPress auth cookie forgery

on 11 April, 2014

11 April, 2014

The use of non-strict comparison in WordPress’s cookie validation code could allow an attacker to forge authentication cookies by exploiting PHP’s type juggling system or by measuring timing differences between requests. Both attacks are a bit impractical, but rather fun.

The code in question is as follows (slightly simplified):

// $username is the user the cookie is claiming to be for
// $expiration is when the cookie says it expires
// $pass_frag is part of the salt used to hash the user's password (from the database)
// AUTH_KEY and AUTH_SALT are secrets defined in a configuration file
$key = hash_hmac('md5', $username . $pass_frag . '|' . $expiration, AUTH_KEY . AUTH_SALT);
$hash = hash_hmac('md5', $username . '|' . $expiration, $key);

// $hmac is the MAC value submitted with the cookie
if ( $hmac != $hash ) {
// invalid cookie
}

The bug is the use of the non-strict != operator and it introduces two separate issues:

  • MAC verification bypass via PHP’s loose string comparison
  • Potential for timing attack to determine expected MAC value

Abusing type juggling

When using a non-strict comparison (==, !=, or <>) PHP will perform type juggling prior making the actual equality check. For example, when comparing a string with a number, the internal API function for performing comparisons, compare_function(), will convert both operands to numbers and then check for equality:

var_dump('abcdefg' == 0); // true
var_dump('1abcdef' == 1); // true
var_dump('2abcdef' == 2); // true

Since both arguments in the comparison we’re looking at are strings this doesn’t apply. However, PHP will also perform a numeric comparison if both arguments are strings and they both happen to look like numbers! This is done by the ‘smart’ string comparison function:

var_dump("000000" == "0"); // true
var_dump("000000e1234" == "0"); // true
var_dump("0e1234" == "0e4321"); // true

(The ‘e’ means times ten to the power of the number that follows, e.g. 2e1 is 20. All of the examples above are equal because 0 multiplied by anything is still 0.)

So, if we can create a WordPress auth cookie that has a zero-like MAC then we can bypass the verification check by providing “0” as the value to compare with. Our approach to forge the cookie will be to pick a valid $username and supply an $hmac value of “0”. The trick is then to find a value of $expiration that cause the MAC verification code to generate a MAC for comparison that is a numeric string that converts to 0.

I wrote a program to perform this search given some randomly generated secret key values. After approximately half a billion different expiration times were tried a zero-like string was produced! Given these secret values in wp-config.php:

define('AUTH_KEY', '<Nu.RQZ g3-={T%RBH5$ G6[.<-Eiab|wyftx_i~h6@YJq=wDcVF`SM#*?i~Fw`5');
define('AUTH_SALT', 'x36)N}uKsh~?^z>6?QXyy&1,ZexFu}>@QjmChBn?U|2d+rjM*-,Oq9Sf<npg-r>V');

and a target administrator user with the login “admin” and a $pass_frag of “FaIc”. Then an expiration time of “1818693471” will produce a value for $hash of “0e768261251903820937390661668547”.

php > $username = 'admin';
php > $pass_frag = 'FaIc';
php > $expiration = '1818693471';
php > $hmac = '0';
php > define('AUTH_KEY', '<Nu.RQZ g3-={T%RBH5$ G6[.<-Eiab|wyftx_i~h6@YJq=wDcVF`SM#*?i~Fw`5');
php > define('AUTH_SALT', 'x36)N}uKsh~?^z>6?QXyy&1,ZexFu}>@QjmChBn?U|2d+rjM*-,Oq9Sf<npg-r>V');
php > $key = hash_hmac('md5', $username . $pass_frag . '|' . $expiration, AUTH_KEY . AUTH_SALT);
php > $hash = hash_hmac('md5', $username . '|' . $expiration, $key);
php > var_dump($hash, $hash == $hmac);
string(32) "0e768261251903820937390661668547"
bool(true)

Some rough maths suggests that the average number of requests required to hit a zero-like MAC is just over 300,000,000. For this reason, any attempt at exploiting the vulnerability is likely to take a rather long time. However, if the target is valuable enough it may be worth the effort for an attacker.

Timing attack

The other problem is that PHP’s comparison operators do not run in constant time. This is done for efficiency reasons as it allows the comparison to break as soon as any mismatch is found. Whilst this is good for most cases it becomes a problem in cryptographic code.

An attacker can submit requests containing a cookie with a MAC that does not match but with a different first character for each guess. The request that takes slightly longer than the rest contains the correct value for the first character as the equality check had to perform an extra byte comparison.

Obviously the timing difference is tiny and other factors such as network lag may cause other requests to look slower. For this reason a successful timing attack must involve thousands of requests for each guess and use of statistical methods to find the correct byte value. Previous research into current techniques for exploiting timing attacks has shown that it’s possible to measure differences of less than 40ns over a LAN and less than 25μs over the internet. This is probably not enough resolution to perform this attack in practice. However, these techniques will continue to be improved, so it is prudent to put a fix in place.

The fix

The fix that I suggested was to double up calls to HMAC:

if ( hash_hmac('md5', $hmac, $key) !== hash_hmac('md5', $hash, $key) ) {
return false;
}

This works because an attacker can no longer predict the final value of their guess. Whilst this mitigates both of the issues presented here, the equality check was changed to the strict version just in case.

This issue was assigned CVE-2014-0166 and the fix was released on 8th April 2014.