If a site of yours were compromised tomorrow, the first place you would look is the plugins list. It is the right instinct and it is why WordPress malware increasingly makes a point of not being there. A sample analysed by Wordfence in September 2026, found during a site clean in the middle of June, is a good demonstration: it installs itself as a must-use plugin, edits the screens that would show it, and rebuilds itself from the database if you delete the file.
WordPress has a second plugin directory, wp-content/mu-plugins, that behaves differently from the familiar one. Any PHP file dropped straight into it is loaded automatically on every request. There is no activation step, there is no switch to turn it off, and it does not appear in the ordinary Plugins list. It has its own screen, reachable only if such files exist, and that screen is easy to go years without visiting.
This exists for good reasons. Must-use plugins are how a host or a developer guarantees that something is always running: a mail configuration fix, a caching adjustment, a piece of site-wide behaviour that must not be switched off by accident. Plenty of well-run sites have several, entirely legitimately.
The same properties make the directory the most attractive place on a WordPress site to leave something unwelcome. Automatic loading, no activation record, no deactivate link, and a screen nobody checks.
The malware presents itself as an automated health check and reporting tool, with a plausible author name and a link to a code repository. Across Wordfence’s detections it turned up under more than four thousand different filenames, the commonest being names that belong on a real site: advanced-cache.php, db.php, a theme’s functions.php. The plugin name, author and repository link differ between samples too, so none of that metadata is worth anything as an indicator.
Inside, the give-away patterns are absent. Rather than the familiar eval(base64_decode(...)), the WordPress function calls are left in plain sight and it is the arguments that are hidden: hook names, option keys, file paths and SQL statements are held in a table and decoded through a custom substitution cipher only at the moment they are used. Searching a site for the usual malware signatures does not find it.
Then it edits what you would see. It hooks the filter that builds the must-use plugins screen and removes its own entry before the page is drawn, and it does the same across the Plugins page, update notifications and Site Health. Looking in the one place that would list it is not sufficient, because it has arranged not to be listed.
This is the part worth reading twice, because it defeats the check most people would run.
The malware gives itself an administrator account. Either it creates one, with a username built from a fixed prefix and six random characters, or, if a suitable account already exists, it simply resets that account’s password and uses it, which leaves your user count unchanged.
It then hides the account from the Users screen by adding a condition to the query WordPress runs before listing users, and hides it from the REST API the same way. Those two alone would leave a tell, because the role counts at the top of the Users screen would still include it and the numbers would not add up. So it adjusts those too, subtracting one from the All and Administrator totals. You see a list with an account missing and a total that agrees with the list.
A reader commenting on Wordfence’s analysis had hit exactly this: a notification that a new user had registered, no new user visible in the admin area, and a security scan reporting one more user than the screen showed. Looking directly at the database was what found it.
It also records your passwords. It hooks the filter that runs after every login, and when the account logging in is an administrator it writes the password, in plain text, into a database option. Changing your password after an infection and leaving the malware in place simply hands over the new one.
Three separate mechanisms undo the obvious cleanup steps.
The file restores itself. A routine runs early on every request, throttled by a timer to once an hour, and if its own file is missing or suspiciously small it writes a stored copy back out from the database, backdates the modification time so the file looks old, and sets it read-only. Deleting the file buys you an hour.
Copies placed in the ordinary plugins directory reactivate themselves. Rather than going through WordPress, the code reads the database name and table prefix out of wp-config.php and writes itself back into the list of active plugins directly, inside a locked transaction.
And it spreads. Every three days it looks through the usual web server directories, including /home, /var/www and several others, for other WordPress installations, and writes a copy of itself into each one it can reach, preferring the must-use directory and falling back to the ordinary plugins directory. On a badly isolated shared server, one compromised site becomes all of them. This is a large part of why accounts on a properly configured host cannot read each other’s files, and why “it is only a small site, it does not matter” is not a safe position to hold.
Payment credentials, mostly. The report it uploads includes the site’s address and login URL, its plugin list, the administrator credentials it created and the ones it captured, session cookies that let the attacker log in without either, WooCommerce order data, and a deliberate sweep for payment and cloud keys. That sweep reads wp-config.php, any .env file and .git/config looking for Stripe, Braintree, Authorize.Net and AWS constants, and reads the settings WooCommerce stores for each payment gateway, taking any field whose name looks like a key, a secret or a token.
Where it sends that is the other notable trick. Instead of a hard-coded address that could be blocked or taken down, it reads the current list of command servers from a smart contract on the Ethereum blockchain, through any one of twenty-one public gateways chosen at random. There is no single destination to block, and when the attacker’s servers are taken down they update the contract and every infected site picks up the new addresses. The technique is known as EtherHiding.
Once connected, the instructions it accepts are open-ended: replace itself with a new version, delete other plugins matching patterns the attacker supplies (security plugins being the obvious target), strip text out of the files of plugins that stay, and inject JavaScript into the footer of every page your visitors load. That last one is how a site like this ends up skimming card details at the checkout.
wp-content/mu-plugins over SFTP or SSH, not through the dashboard. You are looking at the directory itself, because the screen that should show you its contents is the thing being edited. Anything you cannot account for deserves attention, particularly a file whose contents are unreadable dense PHP.admin_, adm_, administrator_ or backup_ followed by six random characters are this sample’s signature, though the point of the exercise is to look at the real list rather than to match one pattern.src, bu, bp and ic. They hold, in order, the malware’s own source code, its username, its password, and every administrator password it has captured. Their presence is unambiguous.Do not start by deleting files. Between the hourly restore, the self-reactivation and the three-day spread to other sites on the server, a partial cleanup teaches you nothing and leaves you believing the problem is solved. The dependable route is the one we set out for an earlier backdoor campaign: restore the whole site from a backup predating the compromise, update everything before it goes back online, and then change every credential the site held, including the hosting account, the database user, and any payment or cloud API keys, because those were the target. Rotate the payment gateway keys even if you find no evidence they were used.
If other sites share the server account or the server itself, they all have to be checked before any of them is declared clean.
Nothing above is the entry point. This is what happens after something else lets an attacker write a file, which in practice is an out-of-date plugin, a stolen administrator password, or a flaw in a component nobody was watching. The defences are the unglamorous ones: update quickly rather than on a schedule, as the WordPress core flaw in September showed, keep the number of installed plugins small, remove what you do not use, and treat administrator accounts as the thing being protected. Our piece on the security measures that actually matter covers the ground properly.
Worth adding one habit specifically: look inside wp-content/mu-plugins once, today, on every site you are responsible for, and write down what is legitimately there. It takes a few minutes per site and it turns a directory nobody checks into one where anything new is obvious. If we host your site and you would rather we did that for you, open a ticket and we will list what is in the directory and tell you what each file is.