When building a new PHP web application, most developers will choose to base it on an existing framework, rather than building it from the ground up themselves. Frameworks have a number of benefits, such as decreasing the time required to develop an application, making it easier to use modern design patterns such as MVC, and providing a large number of helper functions and classes to the developer. The use of frameworks can also increase the security of an application, as the framework can handle things like automatically adding CSRF tokens to forms, encoding output before displaying it, or pushing the developer to safe functions to access the database rather than building their raw queries. Frameworks can also take care of areas such as session handling and access control - where developer mistakes frequently lead to vulnerabilities.

However, basing your application on a third party framework introduces a number of problems. The framework code needs to be kept up to date, which is often difficult as updates may be disruptive or break application functionality. Frameworks may also become unsupported, leaving the developer with the options of migrating the entire application to a new framework or running unsupported code and hoping for the best. And finally, frameworks may introduce serious security vulnerabilities into the applications developed with them. Frameworks are an attractive target for attackers, as a single vulnerability could allow them to exploit a number of off-the-shelf products developed in the framework, as well as any bespoke sites using it.

CodeIgniter

CodeIgniter is one of the older PHP frameworks, first released in 2006. It's still widely used, although recently it seems to be losing popularity to newer frameworks like Laravel and Phalcon. CodeIgniter has its own session handling code that is rather different from other frameworks. Rather than just storing a session ID in the cookie, it stores a serialized PHP array containing the session ID, UserAgent and IP address of the visitor, and a "last activity" timestamp. When the user requests a page, this session cookie is checked, the array is unserialized and if the UserAgent or IP doesn't match, the session will be destroyed. To prevent the user from tampering with the session variables, or performing a PHP object injection attack through the cookie, a checksum is appended to the end of the array. This checksum is a hash of the array and a secret application specific key - if this checksum is invalid the session cookie is discarded. A timing attack was recently published against this protection based on the fact that PHP's strcmp() function doesn't return in constant time, allowing the hash to be brute-forced one character at a time.

CodeIgniter also provides the option to encrypt the user's session cookie, preventing the user from reading the cookie and removing the possibility of the above timing attack. If the PHP Mcrypt library is available, this encryption will be done using 256bit AES (note that some weaknesses have been identified in this encryption code). However, if the Mcrypt library isn't available, then CodeIgniter silently falls back to using a custom, XOR based encryption scheme. And as the first rule of cryptography is "don't roll your own", the words "custom encryption scheme" are never a good sign.

Session Encryption

The session array is created, and then serialized. This string is then passed to the CI->encrypt->encode() method, which checks for the existence of Mcrypt, and if it's not available calls the _xor_encode() method (shown below).

function _xor_encode($string, $key)
{
	$rand = '';
	while (strlen($rand) < 32)
	{
		$rand .= mt_rand(0, mt_getrandmax());
	}
	$rand = $this->hash($rand);
	$enc = '';
	for ($i = 0; $i < strlen($string); $i++)
	{
		$enc .= substr($rand, ($i % strlen($rand)), 1).(substr($rand, ($i % strlen($rand)), 1) ^ substr($string, $i, 1));
	}
	return $this->_xor_merge($enc, $key);
}

This function generates a random string using the mt_rand() function (which is not cryptographically secure, and has a number of known weaknesses) and hashes this string (using SHA-1 by default, but possibly MD5). It then creates a new string by alternating characters of this hashed key ($rand) with characters from the serialized array that have been XORed with the respective character. This string is then passed to the _xor_merge method().

function _xor_merge($string, $key)
{
	$hash = $this->hash($key);
	$str = '';
	for ($i = 0; $i < strlen($string); $i++)
	{
		$str .= substr($string, $i, 1) ^ substr($hash, ($i % strlen($hash)), 1);
	}
	return $str;
}

The application-defined key is hashed (default SHA-1, possibly MD5) and the input string is then XORed with this hash. The final output is then returned, base64 encoded and sent to the user in a cookie. This process is then reversed to read the cookie supplied by the user (with the _xor_decode() method).

Breaking the Encryption

From the CodeIgniter source we know quite a lot about the structure of the plaintext that will be encrypted. We know it'll be a serialized PHP array (so the first characters are "a:"), and we know it'll contain up to 120 characters of our UserAgent. We also know that it'll be XORed with a 40 byte hexadecimal key that will be the same every time. Based on this, we can perform the following attack (note - I'm not a crypto person, so there may be a much faster/more elegant attack possible):

  1. Make a request to the webserver using a known UserAgent (a long string of "z" characters was used, at least 40 are needed), and record the cookie.
  2. URL and base64 decode the cookie to get the ciphertext.
  3. Generate a list of all possible 4 character hexadecimal keys (0000 - ffff) and try to XOR decode the first four characters of the cookie with them (undoing the _xor_merge() method), and then XOR decode the first character with the second character and the third character with the fourth character (undoing _xor_encode()).
  4. If the decoded string is "a:" (the start of a PHP serialized array), then the first four characters were correct. Note that collisions are possible throughout the process resulting in false positives. We now have 10% of the (hashed) key, so can decrypt 10% of the string (including 10% of our UserAgent, which should have a number of occurrences of the string "zz".
  5. Generate a new list of all possible 2 character hexadecimal strings, and append each of these to the known 4 characters of the key. Try and decode the session with each of these keys, looking for the string "zzz" (indicating that these two characters are correct).
  6. Repeat this process adding 2 more bytes to the key each time, and looking for 1 more "z" character until the entire cookie is decrypted.

Throughout this process false positive keys may be identified, which allow another "z" character to be found, but then do not have any following keys that add a further "z". In this case the 2 characters added must be discarded, and another 2 characters must be found.
This attack was tested against a number of different systems using CodeIgniter, and decryption times were found to be between 4 seconds and 4 minutes. It should be noted that this attack is largely performed offline; only a single request is made to the target webserver.
Once the hashed key has been obtained (we don't need to break the hash), it is possible to decrypt any session cookie created by the server in real time. It is also possible to re-encrypt cookies that the application will accept as valid.

PoC

Scripts to perform this attack can be found on the Dionach GitHub repo:

break.py
This is the main script, and given a target URI will attempt to obtain a cookie and brute force the session key. If this script finds a cookie but fails to decrypt it then the server has Mcrypt installed, and is not vulnerable to the attack.
testkey.py
This script will instantly decode a session cookie using the provided (hashed) key (which needs to be added to the script once it has been obtained using break.py.
encrypt.php
This script is based on the encryption functions used by CodeIgniter, and will take an unencrypted cookie (a serialized PHP array) and encrypt it using the provided key (which needs to be added to the script).

The steps to attack an application are as follows:

  1. Use break.py to crack the encryption key used by the application
  2. Add this key to testkey.py and encrypt.php
  3. Browse to the website to create a valid cookie for your IP/UserAgent
  4. Decrypt this cookie with testkey.py
  5. Make any desired modifications to the cookie manually, or by patching encrypt.php to add/modify array elements before re-encrypting.
  6. Re-encrypt the session cookie and paste it back into your browser.

Impact

There are three main impacts from this vulnerability.
IP/UserAgent Restriction Bypass
Because the IP and UserAgent are stored in the cookie, an attacker stealing a cookie using XSS will be unable to use it, as their IP and UserAgent will not match the values in the cookie. By decrypting the cookie and modifying these values, an attacker would be able to use this cookie to steal a users' session.
Object Injection
The decrypted cookie is passed to an unserialize() function, which could allow an attacker to perform an object injection attack. Inspection of the classes available in CodeIgniter did not reveal any that would be exploitable; however there may be classes used by the applications built on the framework that would allow an attacker to exploit this.
Writing Arbitrary Session Variables
After the session cookie is decrypted any values stored into it are loaded into the CodeIgniter session array ($this->userdata). This allows an attacker to set any session variables they wish. Investigation of a random off-the-shelf project based on CodeIgniter found that this lead to an authentication bypass vulnerability, allowing an attacker to authenticate as a target user with just their email address.

Resolution

This issue was reported to EllisLab (the company that owns CodeIgniter) on 28th May 2014. They made the decision to require the use of Mcrypt, and to remove the use of the _xor_encode() method. A fixed version (2.2.0) was released on 5th June 2014, which removed the _xor_encode() method and required the use of Mcrypt. If upgrading immediately is not an option, then installing the PHP Mcrypt library (usually available in a package called php-mcrypt or php5-mcrypt) and ensuring that this library is used by PHP will resolve this issue.

Further Research

There are a large number of projects built on CodeIgniter which may be exploitable using this vulnerability. Developers of these projects need to verify whether their software is exploitable, and if so take steps to update their version of CodeIgniter, and to make Mcrypt a requirement for their application. There are many potential targets out there, so if you do find an exploitable CodeIgniter based application, then please disclose the vulnerability to them responsibly.

Lessons

There are a number of lessons that can be taken from this vulnerability:

  1. Don't roll your own encryption.
  2. Don't trust the contents of a session cookie.
  3. Don't unserialize() user-supplied input, even if you think it's safe to.
  4. Don't silently fall back to weak encryption if strong encryption isn't available.
  5. Make sure that any frameworks or libraries you use have undergone appropriate security testing.

Comments

You'd think they'd give a bit better of an indication into the severity of the problem on the homepage, change log, or anywhere really.. Apparently they're assuming (?) the vast majority of users are on 3.0..

Yes, it's ridiculous that the CodeIgniter homepage says, "oh everybody is on version 3 now" when that version isn't even a stable release!

<blockquote>Because the IP and UserAgent are stored in the cookie, an attacker stealing a cookie using XSS will be unable to use it, as their IP and UserAgent will not match the values in the cookie.</blockquote> If you can run JS at the target web site, you can easily get the user agent using <code>navigator.userAgent</code>. The IP address is easily obtained if the XSSed browser can be forced to send only a single request to your server.

You're absolutely right that the IP address and UserAgent can be easily obtained through XSS. Spoofing the UserAgent is trivial, but the IP address presents a bit more of a problem, because it's much harder to spoof. You could get around this by doing something like force the victim's browser to make the requests for you (CSRF-style, although anti CSRF tokens will make this tricky), or even tunneling your traffic through their browser session with something like BeEF). These kind of security measure aren't going to stop an attacker with time and knowledge, but they do make it quite a lot harder than just stealing cookies.

Add new comment