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.