Google

Understanding WordPress Auth Cookies

Written on:April 20, 2014
Comments are closed

Introduction

recently published vulnerability prompted me to take another look at the wp_validate_auth_cookie WordPress function which is responsible for validating authenticated user requests and ultimately controls access to to your WordPress installation. This post is not about that specific vulnerability (more info here) but rather about how WordPress generates and validates authentication cookies to authorize user requests. If you’re a WordPress user, I encourage you to read on to see what stands between malicious actors and access to your site.

Note: All WP code examples are from version 3.9.

The WordPress Auth Cookie

When a user accesses any post-authentication resources (Dashboard, plugins management, user management, etc.) their authentication details are passed via an auth cookie and validated by the wp_validate_auth_cookie() function.

wpauth_2

This auth cookie is composed of the following components:

wpauth_1

Note: The | character (URL-encoded to %7C) serves as a delimiter.

1) Cookie ID

What I’m calling the auth “cookie ID” is defined in the file default-constants.php:

wpauth_5

It’s simply a concatenation of “wordpress_” and a value called COOKIEHASH which is also defined in the same file:

wpauth_7

As you can see, COOKIEHASH is  nothing more than an MD5 of your site’s URL. Here’s a verification using my test site’s URL:

wpauth_6

2) Username

This element is self-explanatory. Shortly, you’ll see how the username is used when constructing other elements of the cookie hash.

3) Expiration

The expiration value is a UNIX timestamp that is set when the cookie is created in wp_set_auth_cookie and defaults to 2 days (defined in pluggable.php).

wpauth_3

Here’s the default timestamp that was generated for this demo cookie.

wpauth_4

As you’ll see in a bit, although a single timestamp is generated for a given auth cookie, the function responsible for validating a cookie will actually accept any valid UNIX time stamp as long as it’s not expired. This means it’s a predictable value that can be dictated by the user.

4) Hash

So far all components of the cookie (cookie id, username, and expiration) are predictable. Predictability is obviously a bad thing when it comes to an authentication cookie, which is where the hash element comes in. To see how this hash is generated we can turn to the wp_generate_auth_cookie() function from the pluggable.php file located in the wp-includes directory.

wpauth_8

As you can see, the hash component of the cookie is an md5 value composed of the username and expiration, both of which are predictable. To address this, the PHP hash_hmac function employs the use of a shared secret key. This key is a hash of the username (predictable), expiration (predictable), scheme (predictable, defaults to ‘auth’) and pass_frag.

The pass_frag is a 4 character sub string of the user’s password hash (from characters 8 to 12). Here’s the pass_frag from my test user ‘admin’ account:

wpauth_9

The user’s password hash is composed of character sets a-z, A-Z, 0-9 and two special characters –forward slash (/) and period (.). Since the pass_frag is only 4 characters in length, the total number of possible combinations (entropy) is (26+26+10+2)4 = 16,777,216. This may look like a big number but brute forcing all possible combinations is rather trivial. It should be noted that for the purposes of constructing and validating a user’s auth cookie, the entropy provided by the user’s password is the same whether that user has a 4 character password or a 40 character password.

So, given the predictability of all of the individual elements that make up the hash, what makes the final hash (and resulting cookie) unpredictable? The answer lies in the wp_hash function used for construction of the key (found in pluggable.php). Let’s take a look:

wpauth_10

Though there’s not much to this function it does perform one critical step to ensure cookie unpredictability — it salts the data using the wp_salt function. This function constructs a salt from the AUTH_KEY and AUTH_SALT values found in the wp-config.php file.

wpauth_11

These two values are are concatenated together and returned to the wp_hash() function as the final salt value.

wpauth_12

Given the size and pseudo-randomness of this salt, the key (and resulting hash) are no longer predictable, despite the predictability of their individual components. In other words, this salt is what makes the hash unpredictable and ultimately, what makes the resulting cookie acceptable for use as an authentication verifier.

It should be apparent that because all the cookie elements except the salt are predictable and under the control of the end user, it is imperative that the AUTH_KEY and AUTH_SALT values are kept secure. In 2012, codeseekah wrote a blog post about this very issue. Let’s take another look at the repercussions of key/salt predicability or disclosure…

The Importance of Keys and Salts

When an auth cookie is passed by a user it’s validated by the wp_validate_auth_cookie() function before providing any access to the requestor. Here are some of the key elements of that function.

wpauth_13

First, notice how the expiration value passed in the cookie is checked to see if it is less than the current date. If so, the cookie is considered expired and the user is not authenticated. As mentioned before, any value can be passed for expiration as long as it is a valid UNIX timestamp that is later than or within the expiration period (default two days). That means that you can technically construct a cookie with an expiration date in the future and this validation function will accept it.

The function also grabs the password hash associated with the username passed in the cookie (assuming it’s valid) and extracts the four character pass_frag. It then reconstructs the cookie (in the same manner used by wp_set_auth_cookie), creates a hash, and compares it to a hash created from the cookie passed by the user. As long as they match, the user is authenticated and provided access to the requested resource (assuming they are authorized).

With this in mind, consider these two points:

  • All values used to reconstruct the auth cookie except the key/salt from wp-config are under the control of the requesting user.
  • Unlike many other web applications, WordPress does not employ session management so this authentication check is wholly dependent upon the valid construct of the auth cookie and does not require a corresponding user session.

These are important points because if the key and salt values used in constructing the hash portion of the cookie are disclosed, one can create a valid, persistent auth cookie for any user of their choosing. To demo this impact of disclosure, I’ve changed the auth_key and auth_salt values in my wp-config file to DisclosedKey and DisclosedSalt respectively.

Assuming I’m a malicious actor that has access to these values, I can now reconstruct a valid auth cookie for a user of my choosing and gain access to the WordPress installation. The only piece of information I still need to generate is the target user’s pass_frag.

I’ve written a basic python script (available for download below) that can generate all possible pass_frag combinations and create a valid WordPress cookie.

Wpcookiegen
wpcookiegen.py
Version: Beta
4.2 KiB
965 Downloads
Details

wpauth_14

For demo purposes, I’ve reduced the total character space from 16+ million to just over 50,000 (by reducing the individual character sets).

wpauth_15

The script generates all possible auth cookies (using all 4 character combinations of pass_frag) for a given user, given the key and salt. Here’s an example for the admin user:

wpauth_17

You can feed these values directly into Burp Intruder and brute force requests until a valid cookie is discovered. Notice I’ve prepended the generated cookies with a numerical identifier and the corresponding pass_frag. A simple match/replace payload processing regex will strip this extraneous information from the cookie before sending it to the target (^[0-9]*:[0-9a-zA-z\.\/]{4}:).

Notice in my 50,000 request brute force demo below, I found a valid cookie about 1/10 of the way through my file (in less than 3 minutes, testing against a local installation of WordPress).

wpauth_16

With the salt and key values known, it would take at most 16,777,216 million requests to access any one user’s account. This applies to every user of that WordPress installation! While many WordPress installations will have some sort of brute force protections against the authentication page, they will not likely have such protections for all URLs/pages, making this brute force attack feasible (again, assuming that the secret key and salt are known).

Another thing to note is that the only variable component of a user’s auth cookie is the the expiration time stamp and since WordPress accepts any expiration that is not expired (including any date in the future), there are many valid authentication cookies for any one user — one for each future timestamp.

wpauth_18

In addition, since WordPress doesn’t manage sessions, these valid cookies will provide access to a user’s account whether they are logged in or not. Depending on how far in the future you choose your expiration time stamps, the session keys can be valid for years.

So, how big of a problem is this? In reality, it shouldn’t be a problem at all if your keys and salts are constructed with sufficient entropy and you protect access to your wp-config file.

Protecting Your Keys and Salts

When it comes to protecting your secret keys and salts, Codeseekah made several good recommendations, some of which I’l reiterate here:

1) Limit access to your wp-config.php file. Only trusted admins should have access to this sensitive file. In fact, access to this file will also provide access to your DB credentials.

2) Keep your WordPress installation (including all plugins) up-to-date to prevent exploit-based disclosure or unauthorized access.

3) Consider changing your keys and salts periodically. Keep in mind this will require all users to re-authenticate, which can be an effective way of terminating access if you suspect a compromise. There’s an API to generate pseudo-random salts/keys here: https://api.wordpress.org/secret-key/1.1/salt/. You don’t have to use an automatic generator, but whatever you do be sure not to change these to weaker values and decrease entropy!

4) If you feel that a salt alone is not sufficient, you might consider concatenating another value to the end of the salt created via the wp_hash() function (such as salt=wp_salt($scheme) . ‘UNIQUE_VALUE’;). This would increase entropy and would also mean a component of your salt would be dictated in two WordPress configuration files vs. just one.

5) Using the same technique as above, you may also consider reducing the predictability of other components of your auth cookie. For example, add another parameter when generating the MD5 hash for the COOKIEHASH portion of the cookie ID (e.g. define(‘COOKIEHASH’,md5($siteurl . ‘UNIQUE_VALUE’)));

Important: Remember to back up your WordPress installation before making any changes to config files!

Conclusion

My intent for this post was to demonstrate two things:

1) How WordPress uses authentication cookies to validate a user request in lieu of traditional session-management.

2) The importance of protecting your secret keys and salts.

While most of the components of these auth cookies are predictable and controllable by the user, the secret keys and salts are unique to the WordPress installation and must be protected as their disclosure will put all WordPress installation user’s accounts at risk of unauthorized access.

Please note that I am not suggesting there’s an exploitable vulnerability in this WordPress cookie-based authentication implementation. While a session-based approach would certainly shorten the lifespan of an auth cookie and the potential exposure of a user’s account, this approach is a valid solution, provided you keep your keys and salts secret!

As always, thanks for reading. Feedback welcome. – Mike

14 Comments add one

  1. tesla says:

    Sorry but (26+26+10+2)^4 is 16,777,216 not 167,777,216.

  2. tesla says:

    By the way, nice post. Thanks you.

  3. tesla says:

    Hi Mike, sorry to bother you again, but with your post I was able to understand the wpsalt, I was stuck at the $data and so I couldn’t finish my php code (wpcookie_hijacking.php). I found myself digging in wp core (yesterday) as soon as I noticed that the logged in cookie was changed with the latest update (wp 4).
    Thanks again.

    edit: I wrote the above part yesterday, I didn’t submit because my code in php (using curl_init()) was to slow (about 64 requests in 40 seconds), so I re-wrote the code in perl.
    code at: linuxlog.in/bLog/?p=918

  4. KoF says:

    nice post

  5. Toby says:

    Hi Mike

    sorry for a silly question

    How do you go about using the regex (^[0-9]*:[0-9a-zA-z\.\/]{4}:) ?

    in intruder payload processing match/repleace, what exactly do i match and replace with (^[0-9]*:[0-9a-zA-z\.\/]{4}:). ?

    can you please answer.

    Regards

    keen student.

    • Mike Czumak says:

      Not silly at all…on the Burp Intruder module click on the Payloads tab and scroll down to “Payload Processing”. Click “Add” and choose “Match/replace” as the payload processing rule. Then you can enter your Match regex and whatever you want to replace it with in the respective fields. – Mike

  6. Toby says:

    Thanks sir. helpful.

  7. abushoaib says:

    Very helpful post. thank you

  8. Sharni says:

    Hey Mike,
    Useful info n great post!
    Wanted to know, whether is it required to hash the username in the cookie?

    • Mike Czumak says:

      Thanks. Technically you can modify the underlying PHP code to do anything you see fit so nothing is really required. That said, it is the way in which out-of-the-box WP session cookie generation works and you should use caution if you plan on modifying the functions, especially as you make updates to the WP installation.