Request Membership
Categories
Posts By Month
Bloggers
Related Links
Input Validation RSS

WordPress 2.5 Cookie Forging Explained

WordPress 2.5.1 came out recently. It includes a critical security fix for a cookie integrity bug that would allow an attacker to impersonate other users, including WordPress admins, by manipulating the contents of an HTTP cookie. Whenever I read about a vulnerability predicated on the user identity being embedded into a client-side token (as opposed to a pseudorandom session identifier), I like to dig a little deeper to see what’s going on.

How does the authentication mechanism work?

The advisory describes the structure of the WordPress authentication cookie as follows:

The new cookies are of the form:

"wordpress_".COOKIEHASH = USERNAME . "|" . EXPIRY_TIME . "|" . MAC 

Where:

COOKIEHASH:  MD5 hash of the site URL (to maintain cookie uniqueness)
USERNAME:    The username for the authenticated user
EXPIRY_TIME: When cookie should expire, in seconds since start of epoch
MAC:         HMAC-MD5(USERNAME . EXPIRY_TIME) under a key derived
             from a secret and USERNAME . EXPIRY_TIME.

So you login to WordPress with your username and password, and then the login page issues you a cookie such as the one below:

Set-Cookie: wordpress_52440d615a927011d57374216b3ff789=
  admin%7C1209329209%7C7d5e9e67d8f74a2b657b2e63437a1241; path=/blog/

As expected, the cookie contains the username, expiration in epoch time, and an MD5 hash (the %7C’s are the URL-encoded form of the ‘|’ character). The wp_generate_auth_cookie() function generates the cookie as follows:

$key = wp_hash($user->user_login . $expiration);
$hash = hash_hmac('md5', $user->user_login . $expiration, $key);
$cookie = $user->user_login . '|' . $expiration . '|' . $hash;

Each subsequent request that your browser makes to WordPress contains the authentication cookie, which the software then verifies to make sure you are who you say you are. This occurs in the wp_validate_auth_cookie() function:

list($username, $expiration, $hmac) = explode('|', $cookie);
.
.
$key = wp_hash($username . $expiration);
$hash = hash_hmac('md5', $username . $expiration, $key);

if ( $hmac != $hash )
  return false;
$user = get_userdatabylogin($username);
.
.

As you can see, the function parses out the username, expiration, and HMAC from the cookie. It then generates the expected hash value, based on the concatenation of username and expiration, and compares it to the HMAC in the cookie. If the values match, it fetches the user object corresponding to the username in the cookie, and you’re authenticated.

So how can this be attacked?

The authentication mechanism assumes that an attacker cannot calculate the HMAC. However, this assumption is broken because the two inputs used to calculate the HMAC (username and expiration) are not clearly delineated. To illustrate, let’s say I register a new user account and for the username, I select admin0. Now I login to the application and I get back my authentication cookie, which would look something like:

admin0|1209331305|HMAC_FUNCTION("admin01209331305")

Do you see where we are going with this? Now that we know the expected value of HMAC_FUNCTION for the string “admin01209331305″, we can re-use it to our advantage. We simply remove the 0 from the end of the username and prepend it to the expiration time, keeping the HMAC value the same:

admin|01209331305|HMAC_FUNCTION("admin01209331305")

The HMAC calculation checks out, and as far as WordPress is concerned, you’ve just authenticated as the admin user.

How was it fixed, and does it matter to me?

The fix was straightforward: use a delimiter to ensure there is no ambiguity between the username and the expiration when calculating the HMAC. Now when I login as the admin0 user, my cookie looks like this:

admin0|1209331305|HMAC_FUNCTION("admin0|1209331305")

You can’t re-use the calculated HMAC_FUNCTION for “admin0|1209331305″ in any useful way, because there’s no longer ambiguity between the username and expiration values.

As stated in the original advisory, you should upgrade ASAP if your WordPress instance is configured to permit account creation.

8 Comments »

Hello. MY wp is also 2.5, but cookie looks like :
wordpress_ba52b2fd4ea180a4841306bc0ad6d3b2
admin%7C1211057307%7C6e0e9bec639167e4b2cfde09892e9d08
http://www.forgedeuphoria.com/blog/
1536
1528536960
29931615
160032496
29928799
*
Why it is different to
admin0|1209331305|HMAC_FUNCTION(“admin0|1209331305″) ???

Comment by Amana — May 3, 2008 @ 4:00 pm

@Amana: Replace each occurrence of %7C in your cookie with the ‘|’ (pipe) character and you will see that yours does look similar to what I’ve described. Your expiration timestamp is 1211057307 and the result of the HMAC is 6e0e9bec639167e4b2cfde09892e9d08.

Comment by Chris Eng — May 5, 2008 @ 12:47 pm

Thanks for explaining this. Im a great believer that security disclosure helps make us better developers.

Comment by Paul @ Web Design Ireland — May 5, 2008 @ 4:47 pm

[...] un articol stufos despre cum stă [...]

Pingback by (1+2)+(1+4)+(1+6)=multe paranteze si cifre » WordPress 2.5.1 is available! Please update now. — May 6, 2008 @ 9:07 am

I did some backtracking and I must be missing something or I have misunderstood something because this all seems imposible without knowing SECRET_KEY and SECRET_KEY constants if those aren’t defined a random string is generated.
$key = wp_hash($user->user_login . $expiration); great however
function wp_hash($data) { $salt = wp_salt();
lets have a look at wp_salt();

function wp_salt() {
global $wp_default_secret_key;
$secret_key = ”;
if ( defined(’SECRET_KEY’) && (” != SECRET_KEY) && ( $wp_default_secret_key != SECRET_KEY) )
$secret_key = SECRET_KEY;

if ( defined(’SECRET_SALT’) ) {
$salt = SECRET_SALT;
} else {
$salt = get_option(’secret’);
if ( empty($salt) ) {
$salt = wp_generate_password();
update_option(’secret’, $salt);
}
}

return apply_filters(’salt’, $secret_key . $salt);
}

So without knowing those constants how can you feed function hash_hmac($algo, $data, $key, $raw_output = false) the param $key?

Comment by devnull — May 7, 2008 @ 4:36 am

@devnull: You’re missing the point. I don’t have to feed all the parameters into the HMAC function. It doesn’t matter if I know SECRET_KEY or not because at no point am I (as an attacker) required to provide the key. The point is that the HMAC function will calculate the same value for [username=admin, expiry=01209331305] as it will for [username=admin0, expiry=1209331305]. I can use WordPress to do that calculation for me — legitimately — and then re-use that value.

This is sometimes referred to as an oracle attack. We use the application against itself by asking it to perform a useful cryptographic operation for us — in this case, calculating an HMAC for one user that we can then use to impersonate another user. We don’t care what the key is because we have a valid HMAC and that’s all that matters.

Comment by Chris Eng — May 8, 2008 @ 4:20 pm

[...] WordPress 2 5 Cookie Forging Explained Posted by root 7 minutes ago (http://www.veracode.com) Chris eng senior director of security research middot chris wysopal co founder and chief wordpress 2 5 1 came out recently it includes a critical security fix for a cookie comment by paul web design ireland may 5 2008 4 47 pm mail will not be published re Discuss  |  Bury |  News | WordPress 2 5 Cookie Forging Explained [...]

Pingback by WordPress 2 5 Cookie Forging Explained | Cellulite Creams — June 8, 2009 @ 9:36 pm

Thanks for this Nice post, Really usefull all of us. just bookmarked this post in my digg profile, hope you will update more post soon.
I really liked your blog!

Regards,
Shaza

Comment by Crossing Arbogast — July 9, 2009 @ 1:17 am

RSS feed for comments on this post. TrackBack URI

Leave a comment

 

Powered by WordPress