The number that started it #
7294.2 MB/month of traffic for one of our internal WordPress sites — which would have made it one of the busiest domains on the server, except Google Analytics and Cloudflare both said the site was nearly dead. A handful of real visits, nothing more.
That gap is usually explainable by caching or bot filtering, but 7 GB against near-zero analytics sessions is too wide to wave off. The site had been sitting on low-priority maintenance for a while, which already made me uneasy; a neglected WordPress install is exactly the kind of thing that quietly becomes someone else’s asset. So I started digging.
Why Plesk and Analytics disagree in the first place #
Google Analytics only records a session when JavaScript executes in a real browser. Bots, scanners, and brute-force tools don’t run JavaScript — they just hammer the server directly. Cloudflare can catch some of this at the edge, but if a request reaches the origin server, Plesk counts it regardless of what it is.
So Plesk bandwidth measures what the server served; Analytics measures what humans visited. A large gap between those 2 numbers, on a low-traffic site, is a flag worth investigating.
What the logs actually showed #
/xmlrpc.php — 110,541 hits ~86.7 MB
/wp-login.php — 28,797 hits ~48.5 MB
/wp-json — 1,732 hits ~104.9 MB
/ — 39,015 hits ~343.4 MB
xmlrpc.php alone was hit over 110,000 times. This site has no XML-RPC requirement — no mobile app, no Jetpack, nothing — so that volume is purely brute-force and scanner traffic.
There were also dozens of requests to randomly named XML URLs:
/patriciansqtm.xml
/malkahmwr.xml
/erlenelzx.xml
/batrachophobiamlv.xml
/honeystoringzde.xml
Most were returning 404, meaning the files themselves were probably already gone — but bots were still hitting old URLs that had presumably appeared in sitemaps or been indexed somewhere. This is a classic pattern from SEO spam campaigns: fake XML sitemaps get created, links get built, and then even after cleanup the traffic lingers because crawlers keep requesting the old paths.
At this point I thought I was probably just looking at an unprotected WordPress install getting hammered by generic scanning — annoying, but not necessarily a compromise. That assumption didn’t survive the next step.
The compromise itself #
index.php didn’t match the official WordPress.org reference, and there were unexpected PHP files scattered through core directories that have no business being there:
wp-includes/Text/5Vpt/Diff/Engine/wp-env-setup.php
wp-includes/blocks/cover/cache-bridge.php
wp-includes/blocks/cover/plugins.php
wp-includes/blocks/pXGSZ/block/index.php
wp-includes/sodium_compat/namespaced/index.php
wp-includes/style-engine/cvF1/style/css/plugin-install.php
wp-admin/images/Dg/plugin-bridge.php
wp-admin/fcefabhjea.gz
wp-admin/cafccefhgc.gz
wp-site.php
Then the .htaccess file. At first glance it looked like it was restricting PHP execution, which is exactly what you’d want to see on a hardened site — my initial read was that a previous admin had put some basic protections in place. But reading it more carefully, the rules included an allowlist of specific filenames that were exempt from the restriction:
wp-env-setup.php
cache-bridge.php
plugin-bridge.php
wp-core-patch.php
security-bridge.php
wp-runtime.php
config-bridge.php
ace.php
goods.php
shop.php
Those names match the unexpected files found in the core directories. So the .htaccess wasn’t a defensive measure — it was the attacker’s persistence mechanism: block arbitrary PHP execution, but quietly keep the backdoors alive. I would have missed this entirely if I hadn’t checked the allowlist against the filesystem. The fact that the file looked protective is what made it effective; on a neglected site with no one reviewing it regularly, that rule could sit there for months.
The plugin situation was similarly messy. Running wp plugin list via WP-CLI and comparing it against what actually existed under wp-content/plugins turned up directories WordPress didn’t recognise:
gallery-1755048406
security-1755051091
seocore
wp-credit-urogenital
wp-functions-how
wp-policy-product
wp-regimes-modules
widget_1753569556
There was also a suspicious must-use plugin called site-compat-layer that we hadn’t installed. And 2 file manager plugins — file-manager-advanced and wp-file-manager — were active. Those are high-risk on any production site: they let anyone with WordPress admin access browse, edit, upload, and extract files directly from the browser, which is exactly the capability an attacker wants to preserve.
What I cleaned up #
This was a low-priority internal site, so I went for thorough-but-practical rather than a full rebuild.
I replaced .htaccess first, before touching anything else, because I didn’t want that backdoor allowlist staying active while I worked through the rest of the cleanup — the whole point of it was to keep those PHP files executable, and some of them were still sitting in core directories. Once that was gone, I added 410 Gone rules for the fake XML URLs so the server would stop wasting resources responding to bot requests for paths that no longer existed, while leaving legitimate sitemap URLs intact. Then I blocked xmlrpc.php at the server level since the site has no use for it.
With the surface hardened, I rebuilt WordPress core files through WP Toolkit to replace the tampered index.php and clear the injected files from core directories. The suspicious plugin directories that WP-CLI didn’t recognise came out next, along with the site-compat-layer must-use plugin. The 2 file manager plugins went too, along with a couple of others that had no clear purpose — one of them was explicitly blocking auto-updates, which on a neglected site is its own kind of red flag. I finished with a pass through wp-content/uploads checking for executable PHP files (a common hiding spot) and a final state verification through WP Toolkit.
I didn’t come away with a clean bill of health from a forensic standpoint. For a site at this priority level, the practical call was to clean the obvious indicators, harden the surface, and monitor traffic over the next 2 weeks. A full forensic audit would be appropriate for a higher-value asset.
What I still don’t know #
The exact entry point wasn’t conclusively identified. The site had been neglected for a while, and neglect creates a wide attack surface: outdated plugins, no 2FA on the admin account, file manager plugins sitting active for who knows how long. The file managers are probably where I’d start if I had to guess, but I can’t confirm it.
I also can’t rule out that the attacker had access longer than the current evidence suggests. The .htaccess approach — disguising a restriction as defensive while allowlisting your own backdoors — is not something someone sets up in a hurry. It implies time and intent. How long those files were sitting there functional before cleanup is unknown, and on a site nobody was watching closely, the honest answer is: it could have been a long time.
The practical lessons #
The Plesk traffic column is a legitimate security signal. I don’t check it often enough, and I suspect most people don’t. Comparing origin bandwidth against analytics on a regular cadence — even roughly, even quarterly — would have surfaced this sooner. The gap between 7 GB served and near-zero analytics sessions is hard to miss once you’re looking at both numbers together.
Neglected sites are still part of your attack surface. This site had low legitimate traffic, so it had low perceived value, so it got low maintenance attention. But from the attacker’s perspective, a neglected WordPress install on a shared host is exactly what you want: low scrutiny, existing infrastructure, and a potential pivot point to other domains on the same server. Low traffic does not mean low risk.
File manager plugins should not be left active on production WordPress installs. I know this, and we still had 2 active ones. They’re convenient for developers doing ad-hoc work who don’t want to open SFTP, but that convenience has a real cost. Disable them when not in active use, at minimum.
The fake-defensive .htaccess pattern is worth knowing. If I’d just seen PHP restrictions and moved on, I would have missed the allowlist entirely. Whenever you see security-looking rules in a compromised install, read them fully before assuming they’re yours.
The follow-up list for this site is straightforward: 2FA on the admin account, DISALLOW_FILE_EDIT in wp-config.php, Cloudflare WAF rules covering wp-login.php and xmlrpc.php, and a check of other low-traffic WordPress installs on the same server for similar symptoms. That last item is probably where I’ll find the next problem.