CASO DI STUDIO · E-COMMERCE

The Rogue Webhook: A WooCommerce Store’s Orders Copied for Months to an Unknown Server

  • CLIENTOnline sporting goods store (anonymized data)
  • PLATFORMWooCommerce

Customers hit by phishing that used the real data from their orders, with clean files: a webhook and an API key created by a ghost admin. Closed within 6 hours.

An order receipt with the customer’s details highlighted, caught on a large white hook from which a dashed red line runs to an unknown dark server with a red question mark

Background

An online sporting goods store on WooCommerce, with a few thousand products and an average of twenty-five orders a day, rising sharply during sales. The site runs on a virtual server managed by a small agency, which handles updates and backups; payments go through an external gateway with a hosted payment page, so card data never passes through the site. The store is connected to the warehouse management system through an official integration, which receives orders via a WooCommerce webhook. For confidentiality, the store’s name, its exact sector, numbers and any detail that would allow it to be identified have been omitted or changed.

Symptoms

The first signal did not come from the site but from customer service. In early September, a few customers called to ask whether it was normal to receive a text message asking them to pay a few euros “to release the shipment” of their order. A message like that is not new in itself: fake courier notifications reach everyone. This time, though, one detail did not add up. The message quoted the correct order number, the name of the product purchased and the exact amount. In some cases an email also arrived, nicely designed, with the real delivery address.

Within a week the reports had grown to about twenty. The agency had run two security scanners, verified file integrity and checked the payment page for a skimmer, like those described in the article on JavaScript skimmers in the WooCommerce checkout. Everything came back clean. The most widespread theory was a data leak at the courier. The owner was not convinced: some of the orders mentioned in the texts had not yet been handed to the courier. He contacted us, choosing the service with intervention within 6 hours, because customer data could still be leaving.

What we found at the application level

The agency’s checks were correct: core, plugins and theme matched the original packages, there were no PHP files in the uploads folder, the mu-plugins folder was empty and the database held no options with hidden code. From a file standpoint, the site was clean. But data leaving a WooCommerce store does not need malicious code: WooCommerce itself has two mechanisms designed precisely to deliver orders to external systems, webhooks and REST API keys. They are configuration, database records, and no file scanner looks at them.

So we listed both, along with the accounts with elevated privileges:

# Configured webhooks: destination, event, author and creation date
wp db query "SELECT webhook_id, name, status, topic, delivery_url, user_id,
  date_created, failure_count FROM wp_wc_webhooks ORDER BY date_created;"

# REST API keys: who owns them, permissions and last use
wp db query "SELECT k.key_id, k.user_id, u.user_login, k.description,
  k.permissions, k.truncated_key, k.last_access
  FROM wp_woocommerce_api_keys k LEFT JOIN wp_users u ON u.ID = k.user_id;"

# Accounts with the administrator or shop manager role
wp user list --role__in=administrator,shop_manager --fields=ID,user_login,user_email,user_registered

There were two active webhooks, both on the order created and order updated events. The first, “Warehouse system”, pointed to the warehouse system’s service and had been created by the agency two years earlier. The second was called “Warehouse sync”: an almost identical name, chosen to blend in with the real one. It pointed to a domain registered a few days before the webhook was created, hosted on a virtual server abroad, and had been created in April, five months earlier, by the user with ID 41.

User 41 was an administrator with a name reminiscent of the agency’s, something like “web-support”, and an email address on a free mail service. Nobody knew it. It had been registered in April as an ordinary store customer, and a few minutes later its role had become administrator. It had logged in to the admin panel only once, for less than ten minutes: just long enough to create the webhook and a read-only REST API key, described as “Shipping integration”. The key had last been used two days before our intervention. It is the pattern described in the article on hidden administrators, with one difference: here the account was not used to come back to the site, only to leave behind a configuration that worked on its own.

The two configurations had complementary jobs. The webhook sent every new or modified order in real time, with everything WooCommerce puts in the delivery payload: name, email, phone, billing and shipping addresses, products, amounts and order status. The API key, in contrast, had been used to download the history: orders from previous years, page after page. No card data was involved, because the site does not handle it, but the stolen data was exactly what is needed for a credible phishing campaign.

Diagram of an order’s flow. The customer completes checkout and WooCommerce saves the order; the scheduled actions queue delivers the event to two webhooks with almost identical names. The first, Warehouse system, created by the agency two years earlier, goes to the recognized warehouse system. The second, Warehouse sync, created in April by an unknown administrator, goes to a domain registered a few days earlier on a server abroad. Below, a read-only REST API key, described as Shipping integration, downloads the order history from the same external server. A note points out that none of the three elements is a file.
The same mechanism, two destinations: the only things distinguishing the legitimate webhook from the malicious one were the domain, the author and the creation date.

What we found at the system level

At the system level the server was clean: no foreign entries in the crontab, no anomalous processes, no unknown SSH keys. But the system kept evidence of what the configuration had done, and we gathered it from four different sources.

The first was the scheduled actions queue. WooCommerce does not deliver webhooks while the order is being saved, but hands them to the Action Scheduler queue, which keeps completed actions for about a month. There, for every order in the last month, was one delivery to the warehouse system and one to the unknown server, both completed.

# Webhook deliveries left in the Action Scheduler queue
wp db query "SELECT action_id, status, scheduled_date_gmt, args
  FROM wp_actionscheduler_actions
  WHERE hook = 'woocommerce_deliver_webhook_async'
  ORDER BY scheduled_date_gmt DESC LIMIT 20;"

# WooCommerce delivery log (Status, Logs, webhooks-delivery source)
ls -l wp-content/uploads/wc-logs/ | grep webhooks-delivery

The second was WooCommerce’s delivery logs, which record the destination and the response code for each send: the external server always answered with a 200, a sign that it was receiving and accepting the data. The third was outbound connections: we created a test order and watched, a few seconds later, the site’s PHP processes open a connection to the address of the unknown domain.

# Outbound connections of PHP processes right after a test order
ss -tnp state established '( dport = :443 )' | grep php-fpm

The fourth was the web server access logs, which the agency kept compressed for a year. Filtering the requests to the REST API orders endpoint turned up a single external address besides the warehouse system’s: in the hours after the key was created it had downloaded the entire order archive, about thirty thousand orders, a hundred at a time, and since then it had come back once a week to fetch modified orders. To scroll through months of logs and isolate these sequences we got help from an open-weight artificial intelligence model run on our own servers, without sending customer data to external services; every result was then checked by hand against the original lines, as explained in the article on access logs as an investigation tool.

# Who queried the orders endpoint, and how many times
zgrep -h '/wp-json/wc/v3/orders' /var/log/nginx/access.log* \
  | awk '{print $1}' | sort | uniq -c | sort -rn | head

# Requests from the minutes when account 41 went from customer to administrator
zgrep -h 'admin-ajax.php' /var/log/nginx/access.log-202604*.gz | grep 'POST'

That last filter also revealed the way in. A few seconds after the account was registered as a customer, a request had gone out from the same address to a function of a loyalty points plugin, installed years earlier and no longer updated. The version on the site had a known, published vulnerability of the privilege escalation type: a missing check allowed any registered user to change their own role. The fix had been available for months, but the plugin had been excluded from automatic updates after an old update broke a theme customization.

Incident timeline from April to September. In April, within one hour: registration of a customer account, role escalation to administrator through the outdated loyalty points plugin, creation of the API key and the webhook, download of about thirty thousand historical orders. From May to August: every new order delivered in real time to the external server and a weekly pass by the API key. In September: customers receive phishing texts and emails with the real order data, and the store contacts us. Below, the six hours of the engagement: evidence collection, deactivation of webhook and key, removal of the account, search for the entry point, credential rotation, hardening.
One hour of work in April, five months of data flowing out without any need to return to the site, six hours to close everything in September.

How we removed it

Before changing anything we saved the evidence: an export of the webhook, API key, user and action queue tables, the delivery logs and the relevant portions of the access logs, with the date and time of each extraction. Then we proceeded in this order:

  1. disabled the malicious webhook and revoked the API key, to stop the data leaving immediately, and verified with a new test order that no more connections went out to the external server;
  2. removed the role from account 41, closed all its sessions and checked that it had no application passwords; once the analysis was complete, the account was deleted;
  3. checked everything an administrator could have changed in those ten minutes: payment gateway settings and bank transfer accounts, WooCommerce email recipients, other users, options and installed plugins, finding no other changes;
  4. updated the loyalty points plugin to the fixed version, after confirming with the agency that the theme customization no longer depended on it;
  5. changed the credentials: passwords of all administrators and store managers, WordPress keys and salts, the warehouse system’s API key and webhook secret, as a precaution even though they showed no use by anyone else.
# Disable the unknown webhook, then delete it after saving its data
wp wc webhook update 7 --status=disabled --user=1
wp wc webhook delete 7 --force=true --user=1

# Revoke the API key created by the unknown account
wp db query "DELETE FROM wp_woocommerce_api_keys WHERE key_id = 3;"

# Close the sessions and remove the role before deletion
wp user session destroy 41 --all
wp user set-role 41 subscriber

On the legal side, the owner assessed the incident with his own privacy consultant. The names, contact details, addresses and purchase history of thousands of customers had been transmitted to an unknown party for months, with concrete consequences already visible: the assessment led to notifying the Italian Data Protection Authority (Garante) within the required deadline and to a message to customers, explaining what had happened and warning them not to pay any request tied to orders received by text or email. Our contribution was a technical report with the dates, the categories of data involved and the number of orders exposed, which is what the people who have to decide need; the general framework is described in the article on hacked sites and GDPR.

Hardening

The goal was to make visible, next time, what this time had stayed invisible for five months:

  • daily configuration check: a script compares webhooks, API keys, administrators and store managers every night against an approved reference list, and sends an alert to the owner and the agency for every difference;
  • restricted outbound connections: the server firewall lets PHP processes connect only to the payment gateway, the warehouse system, the courier, the email service and the update servers;
  • activity log for administrators, sent off the server, with an immediate alert when a user with an elevated role is created or an existing user’s role changes;
  • two-factor authentication required for administrators and store managers;
  • updates without silent exceptions: a plugin excluded from automatic updates stays on a list with a review date, and security advisories are checked every week;
  • plugin review: three plugins no longer needed, including an old import module, were removed.

The result

The webhook and key were disabled within the first hour, and the entire engagement, from analysis to hardening, closed within the 6 hours provided by the service. In the following days the nightly configuration check reported no differences and the firewall recorded no connection attempts to unauthorized destinations. Phishing reports continued for a few days, always with data from before the intervention, and then died out.

The owner received a technical report with the reconstruction of the attack, the evidence collected, the list of actions taken and the new procedures, and a separate report for the privacy consultant. The agency added the webhook and API key check to the verification it runs on the other stores it manages.

What this case teaches

When people think about an attack’s persistence, they think of files: a backdoor in uploads, a fake plugin, a hidden cron job. Here the persistence was entirely in the configuration. A webhook and an API key are legitimate features, created from the standard interface, that survive a core reinstall, a file restore from a backup and scanners, because from the software’s standpoint there is nothing anomalous. As the article on the limits of security scanners explains, a tool only finds what it was designed to look for.

The second lesson concerns symptoms. The site worked perfectly, and it was the customers who raised the alarm. When phishing cites data that only the store has, such as the order number or the exact amount, the right question is not “which courier did this leak from” but “which systems receive our orders, and who configured them”. Answering takes a few minutes: just list the webhooks, API keys and administrators, and ask for each one who created it and why.

Malwarecleanup.com is the WordPress and WooCommerce malware removal service of Managed Server S.r.l., active since 2005 in server and hosting management. We analyze files, database, configuration and system, with the help of artificial intelligence models run locally, and guarantee cleanup within 6, 12 or 24 hours per installation, counted from receipt of access. If your customers are receiving messages that know too much about their orders, you can contact us.

THE NEXT STEP

Is your site
infected?

Every hour counts: the malware keeps hitting visitors and customers, and Google, your host and the blacklists won’t wait. Choose a plan, pay online and share your access: the cleanup starts right away, at a fixed price and with guaranteed times.

Request a cleanup

Takes you to the plans: cleanup within 6, 12 or 24 hours of receiving access.

MANAGED SERVER S.R.L.info@malwarecleanup.com +39 02 5656 9681

Monday – Friday · 9:30am – 7:30pm Italian time (CET)

Prefer an online form?
Contact us at managedserver.it ↗