WordPress 2.5 Cookie Forging ExplainedApril 26, 2008 – 7:31 AM
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:
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:
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:
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.