CVE-2008-1930: Wordpress 2.5 Cookie Integrity Protection Vulnerability

Bookmarked!

This exercise explains how you can exploit CVE-2008-1930 to gain access to the administration interface of a Wordpress installation.

Free
Tier
Medium
< 1 Hr.
9


Introduction

This course details the exploitation of an issue in the cookie integrity mechanism of WordPress.

This issue was found in 2008 and allowed an attacker to gain administrator access to a WordPress instance if user registration is enabled.

This issue is a great example of what can go wrong with cryptographic functions, and I thought it would make for a good quality exercise.

The issue
Introduction

This functionality was used to remember users after they close their browser.

A cookie "AUTH_COOKIE" (named wordpress_...) is created by the application and sent back to users. Only the application is able to generate this cookie since it's generated using the WordPress "secret key".

The code

The vulnerable function is wp_validate_auth_cookie included in the file wp-includes/pluggable.php (line 470 to 499). The full code of the function is below:

function wp_validate_auth_cookie($cookie = ''){
  if(empty($cookie)){
    if(empty($_COOKIE[AUTH_COOKIE]))
      return false;
    $cookie = $_COOKIE[AUTH_COOKIE];
  }

  list($username, $expiration, $hmac) = explode('|', $cookie);
  $expired = $expiration;

  // Allow a grace period for POST and AJAX requests
  if(defined('DOING_AJAX') || 'POST' == $_SERVER['REQUEST_METHOD'])
    $expired += 3600;

  if($expired < time())
    return false;

  $key = wp_hash($username . $expiration);
  $hash = hash_hmac('md5', $username . $expiration, $key);

  if ($hmac != $hash)
    return false;

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

  return $user->ID;
 }

First the code retrieves the cookie AUTH_COOKIE if no cookie was provided during the function call.

If no cookie was provided and the cookie AUTH_COOKIE is empty, the function returns false and the authentication fails.

Once the cookie is retrieved, it gets split into 3 values:

  • $username: the username.
  • $expiration: its expiration date.
  • $hmac: the signature of the previous values to ensure it's a genuine cookie.

The following code performs this action, | (%7C) is used as a separator:

list($username, $expiration, $hmac) = explode('|', $cookie);

Then the code ensures the $expired value (based on the value $expiration) is greater than the current time:

if ($expired < time())
  return false;

The code ensures that the signature is correct:

$key = wp_hash($username . $expiration);
$hash = hash_hmac('md5', $username . $expiration, $key);

if ($hmac != $hash)
  return false;

The function wp_hash provides the encryption, it's based on WordPress SECRET_KEY and uses $username and $expiration to generate a unique key.

You can check this function's behavior in the file wp-includes/pluggable.php starting line 1071.

Once the hash is validated, the current user $user is retrieved using the value $username:

$user = get_userdatabylogin($username);
if (! $user)  return false;
    return $user->ID;

If you look at the code quickly, everything seems perfect:

  • The cookie expired at a given time.
  • Only the application can generate the key used to sign the cookie, and this key is unique and not predictable.
  • The cookie is signed based on a unique key and cannot be tampered with (theoretically).
The vulnerability

The issue comes from this line:

$hash = hash_hmac('md5', $username . $expiration, $key);

It's possible to generate a collision between two chosen values. For example, the following values will give the same hash:

$username $expiration HMAC($username.$expiration)
admin1 1353464343 1ba7d82099dd6119781b54ecf8b79259
admin 11353464343 1ba7d82099dd6119781b54ecf8b79259

We see that it's possible to get a collision between two hashes even if the usernames are different.

The collision is interesting because it's possible to have a valid hash generated by the application for a user (admin1) and use it to pretend to be another user admin.

The $expiration value will become even bigger for the user admin since we added the final 1 from admin1.

Exploitation

As we saw above, an attacker is able to get the application to generate a valid hash for a user (admin1) and reuse this signature for the user admin.

To exploit this vulnerability, you need to be able to create a user named admin1 for example (any username followed by an integer will actually work).

This can be done using the registration page: http://vulnerable/wp-login.php?action=register.

If you create a user admin1 and log in with this user. You should receive a valid cookie:

HTTP/1.1 200 OK
[...]
Set-Cookie: wordpress_test_cookie=WP+Cookie+check; wordpress_177e685d5ab0d655bdbe4896d7cdadf4=admin1%7C1353464343%7C1ba7d82099dd6119781b54ecf8b79259
[...]

Once you log in, you should see the traditional WordPress page:

Normal version

Now that we have a valid cookie we can use this vulnerability to gain access to the admin account:

  • Using the valid cookie: admin1%7C1210158445%7C49718d2581bd399916b90a088a11ec84.
  • We can generate a new valid cookie for the user admin: admin%7C11210158445%7C49718d2581bd399916b90a088a11ec84.

If you're using Firefox, you can use the following extension to modify your cookies: Cookie manager +.

After reloading the page, you should be able to see the "admin version" of the website:

Admin version

Patch

The patch for this vulnerability was pretty simple.

To avoid the vulnerability, WordPress' developers added a check to ensure that $username and $expiration were correctly separated.

To do so they introduced the following change:

$hash = hash_hmac('md5', $username . '|' . $expiration, $key);

With this simple |, it's not possible for an attacker to tamper with the cookie and still get a valid signature, since $expiration and/or $username are not simply concatenated to generate the signature.

Conclusion

This exercise explained how CVE-2008-1930 vulnerability works, and how it was possible to use it to gain access to WordPress administration pages.

To me this issue perfectly represents a common pattern in most interesting vulnerabilities:

"The devil is in the details".

And that even a ridiculously small change can make a lot of difference between secure and vulnerable code.

And since Code Review is mostly a matter of "déjà vu", you will have another thing to be on the lookout for, when you search for vulnerabilities.

I hope you enjoyed learning with PentesterLab.