joomla3.4.6rce 分析复现
环境搭建
Joomla环境搭建下载:https://github.com/joomla/joomla-cms/releases/tag/3.4.6
php5.5.9
漏洞分析
loadSession方法分析
public function loadSession(JSession $session = null) { if ($session !== null) { $this->session = $session; return $this; } //判断seesion的值是否为空 // Generate a session name. $name = JApplicationHelper::getHash($this->get('session_name', get_class($this))); //调用JApplicationHelper类里面的gethash方法 创建一个新的seesion // Calculate the session lifetime. $lifetime = (($this->get('lifetime')) ? $this->get('lifetime') * 60 : 900); //计算这个session的存活时间 // Initialize the options for JSession. $options = array( 'name' => $name, 'expire' => $lifetime ); switch ($this->getClientId()) { case 0: if ($this->get('force_ssl') == 2) { $options['force_ssl'] = true; } break; case 1: if ($this->get('force_ssl') >= 1) { $options['force_ssl'] = true; } break; } $this->registerEvent('onAfterSessionStart', array($this, 'afterSessionStart')); // There's an internal coupling to the session object being present in JFactory, need to deal with this at some point $session = JFactory::getSession($options); $session->initialise($this->input, $this->dispatcher); $session->start(); // TODO: At some point we need to get away from having session data always in the db. $db = JFactory::getDbo(); // Remove expired sessions from the database. $time = time(); if ($time % 2) { // The modulus introduces a little entropy, making the flushing less accurate // but fires the query less than half the time. $query = $db->getQuery(true) ->delete($db->quoteName('#__session')) ->where($db->quoteName('time') . ' < ' . $db->quote((int) ($time - $session->getExpire()))); $db->setQuery($query); $db->execute(); } // Get the session handler from the configuration. $handler = $this->get('session_handler', 'none'); if (($handler != 'database' && ($time % 2 || $session->isNew())) || ($handler == 'database' && $session->isNew())) { $this->checkSession(); } // Set the session object. $this->session = $session; return $this; }
我们在来看看这个 librariesjoomladatabasedrivermysqli.php
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 这个函数的用法 作为回调函数
(1)普通使用: function a($b, $c) { echo $b; echo $c; } call_user_func_array('a', array("111", "222")); //输出 111 222
这里由于seesion反序列化后 将会成为一个JDatabaseDriverMysqli类对象,不管中间如何执行,最后都将会调用__destruct魔法函数,__destruct将会调用disconnect,disconnect里有一处敏感函数:call_user_func_array。但很遗憾的是,这里的call_user_func_array的第二个参数是我们无法控制的,但是,我们可以进行回调利用:
所以 我们这里主要利用到这个函数的第二个用法 调用类里面的方法
(2)调用类内部的方法: Class ClassA { function bc($b, $c) { $bc = $b + $c; echo $bc; } } call_user_func_array(array('ClassA','bc'), array("111", "222"));
然后刚好 我们这里仔细观察init方法 到这里我们的思路就要清晰一些了 回调simple类里面的init方法 给cache_name_function定义一个值 从而造成远程代码执行
只要满足条件$this->cache=true && $parsed_feed_url['scheme'] !== Null,将其中第二个call_user_func的第一个参数cache_name_function赋值为assert,第二个参数赋值为我们需要执行的代码,这样就可以构成一个可利用的“回调后门“,达到任意代码执行效果。
那么我们在回过头来看 seseeion在数据库中提取出来加载到javamysql类的过程 从来调用函数执行恶意代码
在 loadSession 方法中会去实例化 JSessionStorageDatabase 类(下图第737行),而该类继承自 JSessionStorage 类,在实例化时会调用父类的 __construct 方法。在父类 __construct 方法中,我们看到使用了 session_set_save_handler 函数来处理 session ,函数中的 $this 指的就是 JSessionStorageDatabase 类对像。接着,程序开启了 session_start 函数。
然后我们继续观察 这里 程序的逻辑 当用户登陆失败时候 joomla会见登陆失败的用户数据存在seesion中 然后302到登陆页面
在执行重定向代码时,程序会直接 exit() ,然后就会开始调用前面说到的 JSessionStorageDatabase 类的 write 方法,将用户 session 写入数据库。当我们再次发送请求时,程序会将上次存储在数据库的 session 取出来,这里在反序列化 session 的时候就会有问题。具体 write、read 的代码如下。
接下来的操作就是简单的字符逃逸了 明天再来看看字符串逃逸
我们从read write函数中可以看见我们可以明显看到在 read 函数处理后,原先54个字符长度的 ' ' 被替换成27个字符长度的 chr(0).'*'.chr(0) ,但是字符长度标识还是 s:54 。所以在进行反序列化的时候,还会继续向后读取27个字符长度,这样序列化的结果就完全不一样,
虽然处理后的username字段减少一半 但是继续向后读取到54个字符串为止,然而后面的就是password字段,所以passsword字段这样就被逃逸出来了,我们可以在这里进行对象注入
思路 使用 000 溢出,来逃逸密码 value 重新构建有效的对象 发送 exp 触发 exp 在数据库中 s:8:s:"username";s:54:"