The dangers of type coercion and remote management plugins

Remote management plugins are a popular tool among WordPress site administrators. They allow the user to perform the same action on multiple sites at once, e.g. updating to the latest release or installing a plugin. However, in order to perform these actions the client plugins need to provide a lot of power to a remote user. Therefore it is very important that the communication between the management server and the client plugin is secure and cannot be forged by an attacker. I decided to take a look at some of the available plugins for weaknesses that would allow an attacker to completely compromise a site running one of these plugins.

ManageWP, InfiniteWP, and CMS Commander

These three services share the same code base for the client plugin (I believe it’s originally authored by ManageWP and used by the other two with some tweaks) and so they were all vulnerable to a signature bypass that would allow remote code execution.

Instead of requiring the user to provide administrator credentials, the management server registers a secret key with the client plugin which is used to compute a MAC for each message. A MAC is calculated by taking a message and passing it through a MAC algorithm along with a shared secret key. The message is then sent with the MAC attached. Finally, the recipient can recalculated the MAC of the sent message and check it against the MAC that was attached. MACs are used to verify the authenticity and integrity of a message, i.e. it was sent by the management server and has not been changed in transit. This is a good approach to securing communications, but implementation flaws in the client plugin for these three services lead to a critical security vulnerability.

An incoming message is authenticated by helper.class.php like this:

// $signature is the MAC sent with the message  
// $data is part of the message  
if (md5($data . $this->get_random_signature()) == $signature) {  
    // valid message  
}  

The use of non-strict equality means that type juggling occurs before comparison. The output of the md5() function is always a string, but if $signature is an integer then the type coercion can make it relatively easy to forge a matching MAC. For example, if the true MAC starts with “0″ or a non-numeric character then the integer zero will match, if it starts “1x” (where “x” is non-numeric) then the integer 1 will match, and so on.

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

Unfortunately, an attacker can provide an integer as a signature. In init.php incoming requests are parsed by using base64_decode() and then unserializing the result. The use of unserialize() means that an attacker can provide types for the input data. An example of a forged, serialized message is:

a:4:{s:9:"signature";i:0;s:2:"id";i:100000;s:6:"action";s:16:"execute_php_code";s:6:"params";a:2:{s:8:"username";s:5:"admin";s:4:"code";s:25:"exec('touch /tmp/owned');";}}

This message provides the signature as the integer 0 and uses the execute_php_code action provided by the plugin to execute arbitrary PHP code.

$signature = 0;  
// $data is the action concatenated with the message ID  
$data = 'execute_php_code' . 100000;  
if (md5($data . $this->get_random_signature()) == $signature) {  
    // valid message if the output of  
    // md5() doesn't start with a digit  
}  

This example forgery is not guaranteed to work straight away. First of all the id key needs to have a value greater than previous legitimate messages (the use of increasing message IDs is being used to prevent replay attacks). Secondly, a matching integer is required for the signature. These two requirements can be bruteforced relatively quickly:

for i from 100,000 to 100,500:
    for j from 0 to 9:
        submit request with id i and signature j

This pseudocode will attempt to send fake messages with very large IDs and for each ID that is attempted ten single digit signatures are used to try and get a match.

This flaw can be fixed by using strict equality (===) and performing other sanity checks on incoming signatures. Fixes were released only changing to strict equality by ManageWP on 19th February, InfiniteWP on 21st February, and CMS Commander on 28th February.

A couple of other problems were reported, but they have not yet been acted on. First, the secret suffix MAC construction used (the secret key is appended to $data and then hashed) has some weaknesses and HMAC should be used instead. Secondly, only the action and message ID are used to create a signature. This means that an active network attacker could change the parameters in a message and the signature would still be valid (e.g. change an execute_php_code message to execute malicious code). To prevent this the MAC should cover the entire message.

(Note that the MD5 based MAC algorithm is a fallback and these client plugins attempt to use openssl_verify() where it is available.)

Worpit

Worpit is another remote management service, but it uses a client plugin built from scratch. It is also vulnerable to a type coercion bug that allows an attacker to log in with administrator privileges.

This client plugin provides a method of remote administrator login “using temporary tokens only configurable by the Worpit package delivery system”. The plugin checks that the token provided in the request matches the one stored in the database:

if ( $_GET['token'] != $oWpHelper->getTransient( 'worpit_login_token' ) ) {  
    die( 'WorpitError: Invalid token' );  
}  

The token is deleted from the database once used. This means that most of the time there is no token in the database. Therefore the call to the getTransient() method is likely to return false. Non-strict comparison is used which means that any ‘falsey’ value, such as the string 0, will be treated as a valid token. An example URL to login as administrator:

http://victim/?worpit_api=1&m=login&token=0

From here the site is completely compromised as the attacker has full privileges to install malicious plugins or edit existing ones.

Again the fix is to use strict equality (!==) and to perform other sanity checks on the provided token and the one retrieved from the database. A fix was released on 10th February.

Conclusion

Always remember to check that user input is of the expected type and use strict comparison in security critical functions, such as checking authentication tokens.

This entry was posted on 01 March 2013.

Lying to Laravel

A vulnerability in the authentication system of the Laravel PHP framework allowed attackers to authenticate as any registered user.

The authentication Driver base class uses a method called recall() to retrieve a “remember me” cookie from the user. The cookie’s contents is decrypted, split on the ‘|’ character, and the first piece is taken as a user ID. This ID is then looked up in the database and if a matching record is found then the visitor is deemed to be authenticated as the corresponding user.

/**
 * Attempt to find a "remember me" cookie for the user.
 *
 * @return string|null
 */
protected function recall()  
{  
    $cookie = Cookie::get($this->recaller());
    
    // By default, "remember me" cookies are encrypted and contain the user  
    // token as well as a random string. If it exists, we’ll decrypt it  
    // and return the first segment, which is the user’s ID token.  
    if ( ! is_null($cookie))  
    {  
        return head(explode('|', Crypter::decrypt($cookie)));  
    }  
}  

However, encryption isn’t authentication. An attacker is able to choose any ciphertext they wish and have the application attempt to decrypt it. So, ‘all’ that has to be done to authenticate as a user with a single digit ID is to find a ciphertext that decrypts to a digit followed by a ‘|’. Any random string can follow the pipe as it will be ignored by Laravel.

Finding an appropriate ciphertext takes a relatively short amount of time with a bruteforce search. The attacker chooses two random strings with the same length as the block size of the cipher — Laravel uses Rijndael-256 so this is 32 bytes. These will be the initialisation vector (IV) and a block of ciphertext. A double loop is then run to try every combination of bytes for the first two bytes of the IV. On each iteration, the concatenation of the IV and ciphertext block are submitted to the target as the value of the “remember me” cookie. A check on the response from the application is used to determine if authentication occurred successfully, e.g. check for a string like “Welcome ”. If the cookie test returns true then the search is complete as a valid cookie has been found. Otherwise, the first two bytes of the IV are reset and the loop continues. The following is some pseudo-code that performs this search:

iv <- 32 byte random string
ciphertext <- 32 byte random string

for i from 0 to 255:
  for j from 0 to 255:
    iv[0] ^= i
    iv[1] ^= j

    if check_cookie(iv + ciphertext):
      return iv + ciphertext

    iv[0] ^= i
    iv[1] ^= j

This attack will take a maximum of 256 * 256 = 65536 requests to create an authenticator for a user with a single digit ID. From there it’s possible to authenticate as any single digit ID by working out which ID was forged and using this information to modify the first byte of the IV appropriately. The ID xor’d with the first by of the IV produces the first byte from the output of the block cipher. This value can then be xor’d with the target digit to get the new byte for the IV.

Adding extra digits requires a maximum of 256 further requests per go. For example, to go from one to two digits you would modify the second byte of the IV to change the ‘|’ into a digit and then loop until a ‘|’ is produced at the third byte.

The solution to this problem is to append a message authentication code (MAC) to cookies. This allows the application to verify that it created any received cookie data before it is used. Laravel has added MACs to cookies in 3.2.8 (released 26th September) and is using HMAC-SHA1 as the MAC algorithm.

Interestingly, at the time of discovery, both the Laravel documentation and the entry on Wikipedia listed “cookie tampering prevention” through the use of a “signature hash” as a feature. Unfortunately this was not actually the case.

In the future, it would be good to see Laravel’s Crypter class have MACs built in so that all encrypted messages are verified before decryption. Examples of this type of behaviour can be seen in Zend Framework 2 and Ruby on Rails.

This entry was posted on 13 October 2012.

I captured the flag!

Last week, Stripe launched their second capture the flag competition. Unlike the CTF they held in February, this version involved web-based vulnerabilities and exploits instead of lower-level problems. Also unlike the first CTF run by Stripe I was able to complete the last level and capture the flag!

I know that there are many write-ups of all of the different levels already, so I just want to talk about the technical details of my favourite level.

Welcome to the penultimate level, Level 7.

WaffleCopter is a new service delivering locally-sourced organic waffles hot off of vintage waffle irons straight to your location using quad-rotor GPS-enabled helicopters. The service is modeled after TacoCopter, an innovative and highly successful early contender in the airborne food delivery industry. WaffleCopter is currently being tested in private beta in select locations.

Your goal is to order one of the decadent Liège Waffles, offered only to WaffleCopter’s first premium subscribers.

Log in to your account at https://level07-2.stripe-ctf.com/user-xxxxxxxxxx with username ctf and password password. You will find your API credentials after logging in.

Looking at the code for level 7 showed that the /orders API endpoint didn’t have any obvious way to bypass signature verification or order premium waffles as the ctf user. So, I started to look elsewhere. Almost immediately I spotted that although the /logs/:id route requires authentication it doesn’t stop authenticated users from viewing the API request logs of other users. By visiting /logs/1 it’s possible to view the requests of a premium user ordering some waffles. Unfortunately they hadn’t ordered the target Liège waffle so it seems that all that can be done is replay requests for unwanted waffles, right?

At this point I had to leave to walk to work. This turned out to be a good thing, on this ‘ponder wander’ I realised that, whilst investigating the benefits of HMAC in the past, I had read about attacks on the signature creation method that was being employed. If it’s possible to take an old request from a premium user, change the ordered waffle to the target and then create a valid MAC then the password can be found.

Length extension attacks

def verify_signature(user_id, sig, raw_params):
    # get secret token for user_id
    try:
        row = g.db.select_one('users', {'id': user_id})
    except db.NotFound:
        raise BadSignature('no such user_id')
    secret = str(row['secret'])

    h = hashlib.sha1()
    h.update(secret   raw_params)
    print 'computed signature', h.hexdigest(), 'for body', repr(raw_params)
    if h.hexdigest() != sig:
        raise BadSignature('signature does not match')
    return True

Above is the code used by the WaffleCopter server to verify a signed request. The signature for a request is calculated by taking the body of the request (the message), appending it to the user specific secret and computing the SHA-1 hash of the result. This is a weak method of creating a MAC because for some hash functions, like SHA-1, it is possible to compute H(m || padding(m) || m') for any m' given H(m) where where m is unknown (|| denotes concatenation).

Length extension is possible on all hash functions that make use of the Merkle–Damgård construction to build a hash function which accepts arbitrarily sized input from a one-way compression function that only accepts fixed size inputs.

Merkle-Damgård hash construction

The image above shows the construction of a hash function from a one-way compression function f which takes fixed size two inputs and produces a fixed sized output the same size as one of the inputs. First, the input message is broken up into blocks of the correct size. The compression function is repeatedly applied to successive blocks of the input message along with the output of the previous function application. The first message block is compressed along with a public initialization vector (IV) that is algorithm specific. The final block is padded as necessary so that it is the appropriate size for the compression function. The result of the final compression is the hash for the input message.

It should now be pretty easy to see how H(m || padding(m) || m') can be calculated for an arbitrary m' given H(m). It is computed by producing H(m') but with the value of H(m) being used in place of the hash function’s standard initialization vector.

With this knowledge it is now possible to create a valid MAC when MAC = H(K M) where K is a secret and M is the message. All that needs to be known is the length of K and any message M and its associated MAC C. This is done by working out the padding that the hash function would add automatically during the computation of C and appending it to M: M' = M || padding. The amount of padding to use is determined by adding the lengths of K and M modulo the block size required by the compression function. Extra data can then be appended to create M'' = M' || extra. The valid MAC for our new message M'' is H(M'') when using C as the value of the IV.

So, back to level 7 of the Stripe CTF. Take one of the signed API requests for a premium user:

count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo|sig:11de8a0bc70d80ffe6b25d34c0711cd450ebdd29

M is the piece before the pipe and C is the piece after “sig:”. The extra data to extend the message with is &#038;waffle=liege so that the requested waffle parameter in the original message is overwritten with the name of the target. By using the length extension attack described above it’s possible to create M'':

count=10&lat=37.351&user_id=1&long=-119.827&waffle=eggo\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x28&waffle=liege

and a valid MAC:

37c9dcaef9a370feb2daf97211a1bbb273812753

The password for level 8 could then be found by POSTing the concatenation of M'', “|sig:” and the new MAC to the orders API endpoint. The request will be verified as having been made by user 1 (a premium user) ordering a Liège waffle.

During the CTF I created the extended message and its MAC by taking a Python implementation of SHA-1 and hacking up it’s internal state. This was much harder than it could have been as it turns out that there is an existing tool for creating extended messages and new MACs.

Overall the CTF was thoroughly enjoyable and I hope that Stripe (and others) continue to create these fun security challenges in the future.

This entry was posted on 30 August 2012.

Trac Cookie Revocation

Last week there was some spam posted to the WordPress core bug tracker. The accounts involved can easily have their access to WordPress.org blocked and their passwords changed to invalidate the cookies used to access the WordPress.org forums. This also stops them from logging into the bug tracker again. However, the trac_auth cookie — the cookie that Trac uses to authenticate a browser to a particular user account after valid login details are provided — is not tied to a user’s password; instead the cookie stores a random string that can be verified against the auth_cookie table in the Trac database. This means that until this cookie expires, when the user closes their browser or 10 days after initial sign-on, the spammer is able to remain authenticated to their WordPress.org profile on core.trac.wordpress.org and can continue to post their spam on the bug tracker.

So, because I was curious (and I had to find something to do other than revision for a machine learning test on Tuesday), I decided to write a short Trac plugin to allow a TRAC_ADMIN user to revoke user cookies. The plugin registers a new administration panel and provides the HTML template for its display. The new panel is just a simple form with a single input for entering the target’s username. After the form is submitted the plugin handles the POST request to delete the row in the auth_cookie table which matches the given name. On future requests to the Trac instance the value stored in the user’s trac_auth cookie will no longer match an entry in the database and so the user will not be authenticated.

Revoking a trac_auth cookie

The code can be found on github. Currently the master branch and the 0.1 tag can be run with Trac versions greater than 0.12 whilst the develop branch contains a small update to make use of enhancements to the Trac database API in the unreleased Trac 0.13.

I found the process of writing a Trac plugin to be quite interesting and fun. Using my (basic) python knowledge again was a good experience too. I already have a project lined up which requires a Trac plugin to be developed, so this was also a nice, easy way to set up the necessary development environment in advance and get myself acquainted with the API.

This entry was posted on 26 October 2011.