Author: Badcode, sebao (知道创宇404安全实验室)

Date: 2017-03-17

0x00 漏洞概述

1. 漏洞简介

Roundcube 是一款被广泛使用的开源的电子邮件程序,在全球范围内有很多组织和公司都在使用。 在服务器上成功安装 Roundcube 之后,它会提供给用户一个web 接口,通过验证的用户就可以通过 Web 浏览器收发电子邮件。 1.1.8 版本之前和 1.2.4 版本之前的 Roundcube 邮件正文展示中存在存储型跨站脚本漏洞。官方已发布升级公告

2. 影响版本

1.1.x < 1.1.8
1.2.x < 1.2.4

0x01 漏洞复现

Payload来自.mario

payload

使用telnet 以html的形式发送一封包含 payload 的邮件到目标邮箱中,或者直接使用Roundcube发送一份html的邮件,发送的时候用burpsuite抓包将_message的值改成payload即可(也可使用其他方式发送邮件, 要注意的是 payload 是否会被转义)。

telnet

当用户查看邮件即可触发跨站脚本漏洞。

0x02 漏洞分析

首先看一下整个处理的流程,当用户查看邮件即action=show时,index.php会包含/program/steps/mail/show.inc,这里会调用$OUTPUT->send('message',false),跳到/program/include/rcmail_output_html.php,在前端的输出页面就是在这里产生的。在这里会调用xml_command()这个函数循环遍历模板输出所需的数据(label,button,message headers,message body等),拼接返回。其中取message body时会跳到rcmail_message_body函数。

先看下rcmail_message_body函数,在/program/steps/mail/fun.inc1169-1329行。

function rcmail_message_body($attrib)  
{
  ......

                // fetch part body
                $body = $MESSAGE->get_part_body($part->mime_id, true);

                // message is cached but not exists (#1485443), or other error
                if ($body === false) {
                    rcmail_message_error($MESSAGE->uid);
                }

                $plugin = $RCMAIL->plugins->exec_hook('message_body_prefix',
                    array('part' => $part, 'prefix' => ''));

                $body = rcmail_print_body($body, $part, array('safe' => $safe_mode, 'plain' => !$RCMAIL->config->get('prefer_html')));

                if ($part->ctype_secondary == 'html') {
                    $container_id = 'message-htmlpart' . (++$part_no);
                    $body         = rcmail_html4inline($body, $container_id, 'rcmBody', $attrs, $safe_mode);
                    $div_attr     = array('class' => 'message-htmlpart', 'id' => $container_id);
                    $style        = array();

                    if (!empty($attrs)) {
                        foreach ($attrs as $a_idx => $a_val)
                            $style[] = $a_idx . ': ' . $a_val;
                        if (!empty($style))
                            $div_attr['style'] = implode('; ', $style);
                    }

                    $out .= html::div($div_attr, $plugin['prefix'] . $body);
                }
                else
                    $out .= html::div('message-part', $plugin['prefix'] . $body);
            }
        }
    }
 ......

    return html::div($attrib, $out);
}

ctypesecondary的值是邮件的Content-type的第二部分(text/html),当ctypesecondary为html时,也就是邮件是html类型的,会调用rcmail_html4inline函数对邮件的内容进行处理。这就是为什么要以html的形式发送邮件的原因。跟进到rcmail_html4inline函数

function rcmail_html4inline($body, $container_id, $body_class='', &$attributes=null, $allow_remote=false)  
{
    $last_style_pos = 0;
    $cont_id        = $container_id . ($body_class ? ' div.'.$body_class : '');

    // find STYLE tags
    while (($pos = stripos($body, '<style', $last_style_pos)) && ($pos2 = stripos($body, '</style>', $pos))) {
        $pos = strpos($body, '>', $pos) + 1;
        $len = $pos2 - $pos;

        // replace all css definitions with #container [def]
        $styles = substr($body, $pos, $len);
        $styles = rcube_utils::mod_css_styles($styles, $cont_id, $allow_remote);

        $body = substr_replace($body, $styles, $pos, $len);
        $last_style_pos = $pos2 + strlen($styles) - $len;
    }

    ......
    return $body;

可以看到,当邮件内容包含style标签时,会把style标签内的内容当成css样式,调用mod_css_styles函数处理,过滤css样式中的危险标签。这也是整个漏洞触发的关键点,style标签的价值就体现在这。继续跟进

    public static function mod_css_styles($source, $container_id, $allow_remote=false)
    {
        $last_pos = 0;
        $replacements = new rcube_string_replacer;

        // ignore the whole block if evil styles are detected
        $source   = self::xss_entity_decode($source);
        $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
        $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');

        if (preg_match("/$evilexpr/i", $stripped)) {
            return '/* evil! */';
        }

     ......
        return $source;
    }

到了此处,可以看到,对$source调用了xss_entity_decode函数处理,后面的代码都是对$source进行判断和过滤。

    public static function xss_entity_decode($content)
    {
        $out = html_entity_decode(html_entity_decode($content));
        $out = preg_replace_callback('/\\\([0-9a-f]{4})/i',
            array(self, 'xss_entity_decode_callback'), $out);
        $out = preg_replace('#/\*.*\*/#Ums', '', $out);

        return $out;
    }

xss_entity_decode对css的内容进行解码,对&lt;img/src=x onerror=alert(1)//进行解码,返回<img/src=x onerror=alert(1)//

原路返回,返回到rcmail_message_body函数,此时$body的内容如下,最后返回到rcmail_out_html.php,拼接其他数据输出,包含payload的页面就被完整的输出到前端。

body

0x03 补丁分析

patch

patch2

$content被解码之后,对返回前的$out使用strip_tags过滤,去除 HTML 和 PHP 标签。

0x04 修复方案

升级程序:https://roundcube.net/news/2017/03/10/updates-1.2.4-and-1.1.8-released

0x05 相关链接

https://www.seebug.org/vuldb/ssvid-92784

http://www.securityfocus.com/bid/96817/info

http://seclists.org/oss-sec/2017/q1/583

https://twitter.com/0x6D6172696F/status/841204796887564288