HexMage web skimmer
HexMage is a web-skimmer kit whose loader reads a per-site script host from an Ethereum Sepolia smart contract. We have matched it on 500 sites since 11 August.
HexMage in numbers
Since 11 August, the first HexMage match in our records, counted on 12 November 2026. How we count.
| Sites with HexMage code | 500 |
|---|---|
| Sites with a match in the last 30 days | 331 |
| New sites in October | 81 |
| Still serving it when recaptured at least 25 days later | 214 of 252 sites (85%) |
| Sites running WordPress (captured since 25 September) | 77% |
A Tag Manager block that loads a skimmer
Confiant named HexMage in August 2026 after tracing the campaign's loader, contracts and checkout code (Confiant).
The loader sits in the infected store's page as a Google Tag Manager block. It sets a GoogleTagManagerLoaderScript global and pushes a gtm.start event, but never requests gtm.js. Its Base64 configuration contains Ethereum Sepolia RPC endpoints, a contract address and a path for that store's script.
From Sepolia to a per-site script
The loader imports ethers.js from a public CDN, connects to Sepolia and calls getText() on its contract. Early contracts return a Base64-encoded host. Later builds receive an encrypted string in salt:iv:ciphertext hex, derive an AES-256-GCM key with PBKDF2-SHA256 over 100,000 iterations from a per-site password in the page, _gwPwd, and decode the host in the browser. The loader appends its own resourcePath or scriptPath and adds the result as a script.
One current loader imports ethers 5.7.2 from jsDelivr or unpkg and calls Sepolia contract 0x3ad4d35f9b6f306792add1127a397b8824400bbc through five RPC services. On 1 November 2026 its reply decoded to stormwraith[.]com, and the loader added /2/<store>/vendor.chunk.h91s.js from that host, with the store's name in the path. One store carries a second inline loader beside it, reading Sepolia contract 0x1bfb4a0d7e1b8c552f0173c94b976fad2b4f0bc6 with a password of its own. On 2 November 2026 that contract decoded to morrowwillow[.]com.
Four older Sepolia contracts serve the same encrypted build: 0x3b14187b76dc39005e8d568b26455d53bd198749, 0xaed4fb9a79fbf3106f25a7986ad984e8a21a8795, 0x61b3e0681f6485e4e85e5ee981757d8b12ec414f and 0x5a155a3f01fa60c3fa0177022ba05fcc6a313a4d. Their replies decode to ironphantomcore[.]top, zaevoderix[.]com, stormvenomforge[.]top and shadowemberstrike[.]top, and the loaders add paths such as 6/topl.js and 5/proftop.js. In these loaders the password is the only 64-character hex string in the obfuscator.io string array. A loader captured on 10 November 2026 reads Sepolia contract 0x304dda3b4738d7a4f5a2a423d8cc8c883ff8dd5d with the selector 0xe00fe2eb and decrypts the reply the same way, to ashenshadowfort[.]top. One captured on 12 November 2026 has the same Tag Manager wrapper, ethers 5.7.2 import and selector, but its contract answers in plain Base64, the early format, and the host reads without the password. The EtherHiding contract tracker follows every HexMage contract here and each destination we have decoded from them.
The per-site chunk decodes in the store's browser
The script at the resolved URL derives an XOR key from location.hostname, after removing www. and converting the hostname to lower case. It decodes a Base64 string, applies the hostname-derived key and unwraps further Base64/XOR layers. The decoded JavaScript is placed in a Blob and loaded through a script element with a blob: URL.
The chunk served from the contract-resolved URL saves its original script directory in window.__gwScriptBase for the decoded code to use after Blob execution. Decoded checkout scripts target WooCommerce, Stripe and FullCulqi card forms. Report URI documented the same contract-resolved skimmer campaign and its staging hosts.
A stage built for each store
Newer stages are obfuscator.io builds served from a path named for the victim, such as /2/kracik/chunk-o18u22nfonc.js or /migratedfiles/5/hediye724/shield-performance.js. Each store gets its own build. The string array in these builds is plain Base64 with no RC4 key, so all of its strings read without running the code. The stage keeps its state in _gw globals, _gwShowingPopup, _gwPopupSubmitBlocked and __gwScriptBase, the same prefix as the loader's _gwPwd.
stormwraith[.]com serves an ordinary page at its root, and the skimmer sits only at the per-store paths. The stage hosts we have found:
stormwraith[.]com
willowtransit[.]com
morrowwillow[.]com
morrowtheory[.]com
mildrocket[.]com
voidhunterkeep[.]top
nightstalkerwatch[.]top
The WooCommerce checkout overlay
The skimmer watches WooCommerce checkout events and uses a MutationObserver to keep its payment form in place when the checkout refreshes. It leaves logged-in WordPress administrators alone.
The form collects the card number, expiry, CVV, cardholder name and billing email. It checks the card length and Luhn checksum before accepting it. The newer stages hook input[name="payment_method"] and the updated_checkout event, handle cash on delivery, and draw a pf50_card-form with Stripe card-brand icons and labels in the store's language, Arabic on one victim.
The captured values are Base64-encoded and sent by whichever method the build enables: an image request, a fetch GET with a ?data= parameter, navigator.sendBeacon or a WebSocket. When none works, the stage logs All enabled send methods failed. After the send, the overlay disappears and the store's original checkout returns, sometimes with a page reload.
What we see
| Month | New sites |
|---|---|
| September | 346 |
| October | 81 |
| November so far | 27 |
Of the 252 sites recaptured at least 25 days after their first match, 214 were still serving HexMage (85%). 77% of HexMage sites captured since 25 September run WordPress.
Checking a store
Inspect the checkout page source and network log for:
- a
GoogleTagManagerLoaderScriptblock that never loadsgtm.jsand imports ethers.js from a public CDN on a store with no blockchain feature - calls to Ethereum Sepolia followed by
getText()and a new script element, withresourcePath,scriptPath,PBKDF2orAES-GCMin the loader - a per-site chunk that derives an XOR key from
location.hostnameand unwraps Base64/XOR layers window.__gwScriptBasefollowed by a JavaScript Blob and ablob:script URL- a second payment form placed over the WooCommerce checkout
- local-storage keys beginning with
mn_, or_gwglobals such as_gwPwdand_gwShowingPopup - a script loaded from a path containing the store's own name on one of the stage hosts above
- card or billing data sent to an unfamiliar host by fetch, WebSocket, beacon or image request
Remove the loader, its linked script and cached copies. Compare the checkout, theme and plugin files with trusted releases, review WordPress and hosting access, rotate credentials, clear caches and test the checkout while logged out.
Contracts we read
We read 35 HexMage contracts every day.
| Chain | Contract | Current destinations | Last changed |
|---|---|---|---|
| Ethereum Sepolia | 0x04141d92BaA052a778dD86cCcC1287D870a2A06C | stylerunningg[.]com | |
| Ethereum Sepolia | 0x0f57ecf9c92ff364bd30aa64f4c916c458b0e70e | marblehabit[.]com | |
| Ethereum Sepolia | 0x10ef316107c7a499b8b1f28a05cd858f49c0bae9 | wildreceipt[.]com | |
| Ethereum Sepolia | 0x1bfb4a0d7e1b8c552f0173c94b976fad2b4f0bc6 | morrowwillow[.]com | |
| Ethereum Sepolia | 0x1cf9f972ffb05f8f8c32705dccb6548a7cbae496 | stylehailens[.]com | |
| Ethereum Sepolia | 0x28b7605be52b7107cd7710f6f855a1a026e72f4a | nightflamevortex[.]top | |
| Ethereum Sepolia | 0x2979ed1f3eacba7391c667d2a6cb866a25b45719 | everthimble[.]com | |
| Ethereum Sepolia | 0x2ea6aa15cab85d071c42dac4231f0fa1cee3176b | electricvoidsig[.]top | |
| Ethereum Sepolia | 0x304dda3b4738d7a4f5a2a423d8cc8c883ff8dd5d | ashenshadowfort[.]top | |
| Ethereum Sepolia | 0x3ad4d35f9b6f306792add1127a397b8824400bbc | stormwraith[.]com | |
| Ethereum Sepolia | 0x3b14187b76dc39005e8d568b26455d53bd198749 | ironphantomcore[.]top | |
| Ethereum Sepolia | 0x4b6170a49090fae965527e222515c2224fe73dd5 | voidwalkerforge[.]top | |
| Ethereum Sepolia | 0x4dbdca7277427edee4d5b4e3d012bca73aefadf1 | voderlence[.]com | |
| Ethereum Sepolia | 0x50b9acc5a99b28117428e143b6eed6d36a703395 | shadowmatrixpls[.]top | |
| Ethereum Sepolia | 0x515cfebd53a860c311d583b74a8b02a78a7884e0 | shadowrunevail[.]top | |
| Ethereum Sepolia | 0x51e3f029209c210fa8946c219644394aae71224d | frozennebularhy[.]top | |
| Ethereum Sepolia | 0x52022366d66464a4d34c3a6ebfa3b47817d6d0b1 | orchardvector[.]com | |
| Ethereum Sepolia | 0x5a155a3f01fa60c3fa0177022ba05fcc6a313a4d | shadowemberstrike[.]top | |
| Ethereum Sepolia | 0x5a3589462b41fa8cf91ac2c7773a285d9c790548 | velnoderic[.]com | |
| Ethereum Sepolia | 0x5ea589954314fe8d5ecb509b35d77a389912b42f | voidravenstorm[.]top | |
| Ethereum Sepolia | 0x61b3e0681f6485e4e85e5ee981757d8b12ec414f | stormvenomforge[.]top | |
| Ethereum Sepolia | 0x7198012e79fdd490b7c95fdffc6cc65929332c0f | paleanchor[.]com | |
| Ethereum Sepolia | 0x8ac95e2af13326ddbfd53b155923745751cd7d0a | mossparlor[.]com | |
| Ethereum Sepolia | 0xa4d63e7825d5010a1aa77f55bc2a45cecf2b028e | hiddenmorrow[.]com | |
| Ethereum Sepolia | 0xa923654195be9e83144032d8dfc83cd5dfc1f981 | morrowtheory[.]com | |
| Ethereum Sepolia | 0xaafb11bf4199f7499b7f4dbb3d566f2bc13a6535 | ghostharvest[.]com | |
| Ethereum Sepolia | 0xaed4fb9a79fbf3106f25a7986ad984e8a21a8795 | zaevoderix[.]com | |
| Ethereum Sepolia | 0xaf59a46ab7270adf919ea08e9250a5783a78721e | blackthornhold[.]com | |
| Ethereum Sepolia | 0xbcccb3909b0eec36211ddafb3ccdc6c2a537bea1 | ivoryfable[.]com | |
| Ethereum Sepolia | 0xd6e035c294b3b87c5f86ccec3f62dc3cad8a88e9 | hollowsentinel[.]com | |
| Ethereum Sepolia | 0xdfcef1c30f453d206fa32c704a0fd4d1fee1e6f7 | mildrocket[.]com | |
| Ethereum Sepolia | 0xeaa29047855f6761ad7cb95eda01145acd658b2f | silvermorrow[.]com | |
| Ethereum Sepolia | 0xec911c2e254bb62fbdc524841b154adbfa8869ef | voidhunterkeep[.]top | |
| Ethereum Sepolia | 0xedda0630ea1db3662222a413eb034c9cffa756d1 | quietthistle[.]com | |
| Ethereum Sepolia | 0xee572bcc785594e06eacea2e6ef638e087d8fac5 | cedarfable[.]com |
The EtherHiding contract tracker keeps the full destination history.