Two sites, one leaked key: how UpdraftCentral was used to plant backdoors
Two WordPress sites went down after an attacker used valid UpdraftCentral keys to plant backdoors. Here’s how we traced the installs, ruled out a suspected vulnerability, and revoked remote access across our hosting fleet.
On the night of 18 to 19 October 2026, someone used valid UpdraftCentral keys to install backdoor plugins on two WordPress sites we host. No password was stolen, no plugin was vulnerable and no server was breached. The attacker simply held the keys that let UpdraftCentral manage those sites remotely, and the sites did exactly what they were told.
We got lucky: the backdoor had a bug that took both sites down instead of opening them up, so they were noticed within hours. This is how we traced it, what we got wrong on the way, and what we changed across our fleet the same day.
All times below are UTC.
A plugin nobody installed
On Monday morning one of our agency partners opened a ticket: their client's WooCommerce shop was down, and a plugin called "Type Board 72D" had appeared on it. Deactivating the plugin brought the site back.
The plugin presented itself as a "small maintenance toolkit" that "keeps option storage tidy". It did nothing of the sort. It hooked WordPress init at priority 1 and waited for a request carrying a secret GET parameter (?_eb700e=) with a 32-character token. Given the right token, it would:
- Create a user with a hardcoded password, an
@example.orgemail and the display name "Site Tools". - Give it the Editor role, then add
manage_optionsandinstall_pluginson top. - Log the visitor in with
wp_set_auth_cookieand redirect to wp-admin.
The Editor disguise is clever. A quick look at your administrators list won't show it, yet install_plugins is code execution and manage_options is a short step to full admin. Logging in by cookie also skips the login form, so 2FA and login rate limits never run.
The author made one mistake. The constants were defined with define() as globals, but the class read them as self::FIELD and self::TOKEN. On PHP 8 that throws a fatal error on the very first line of the handler, before the token is even checked. Because the hook ran on every request, every uncached page returned HTTP 500. The backdoor never worked; it just switched the site off.
If you ever find one of these, do not fix the bug. Fixing it turns the backdoor on.
Following the logs
The plugin files were all stamped 04:07. A few weeks ago we started shipping every cPanel server's access logs into ClickHouse through Vector, mostly to catch 503 spikes. This was its first real forensic job.
The obvious suspects came back empty. There was no wp-login.php POST, no upload through update.php, no REST call to /wp/v2/plugins, and no application passwords on the admin account. The only admin session belonged to the person who fixed the site at 06:57.
What was there, one second before the files appeared, was this:
2026-10-19 04:07:38 2a09:bac5:4e21:2e3c::49b:9 POST / 200 "WordPress/6.4; class-udrpc.php"
class-udrpc.php is the remote procedure call library UpdraftPlus uses to receive commands from UpdraftCentral, its remote management dashboard. The same IP had sent an earlier request at 00:36, which in hindsight was a probe. A per-five-minute status breakdown showed the rest of the story: 200s until 04:05, then nothing but 500s until the plugin was removed just before 07:00.
The site held a single UpdraftCentral key, created in October 2019 and forgotten since.
A second site, and a wrong turn
Searching the whole fleet for that User-Agent turned up exactly one more site: an arts foundation's site on a different server, belonging to a different agency. Same IP, a probe at 21:28 the previous evening, an install at 07:18. That site was still down when we got to it. Its plugin was called "Site Kit", by "Harbor Stack Labs". The name, author, version, token, trigger parameter and username were all different, but the code was byte-for-byte the same template, bug included.
My first theory was CVE-2026-10795, the UpdraftCentral authentication bypass disclosed in June. A failed RSA decryption fell back to a predictable key, so anyone could forge commands to a site that had a key, without holding the key. Old forgotten keys, a probe followed by an install, two unrelated victims: it fit nicely.
It didn't survive the first check. The first site ran UpdraftPlus 1.26.11, and that bug was fixed in 1.26.5. Whatever signed these commands had the real keys.
The real cause: the keys themselves
When a site is connected to UpdraftCentral, it generates its own key pair and stores the dashboard's public key. From then on it executes any command signed with the dashboard's private key, with the rights of the WordPress user who created the connection. Nobody confirms individual commands, and the keys never expire.
Both sites' keys pointed at the same place: "mothership":"__updraftpluscom", which is UpdraftCentral Cloud, the dashboard TeamUpdraft runs. One key was from 2019, the other two from late 2024. The dashboard-side private keys for those connections have never been on our servers. If commands signed with them are arriving from a Cloudflare WARP address with a User-Agent that has no library version and no dashboard URL, they leaked from somewhere upstream.
The pattern in our logs supports that. Across every server that ships logs to ClickHouse, that IP made exactly four requests, two per victim, and only to sites holding a key. That looks like working from a list, not scanning.
Others saw the same campaign the same day: Nethemba published an analysis of three of their own sites, and Patchstack reported a much wider set from their telemetry. Their findings match ours. Whatever the root cause turns out to be, this is not a plugin vulnerability, and updating UpdraftPlus does not help.
What we did
On the two sites. We archived each plugin outside the web root for analysis, deleted it from disk rather than uninstalling it through wp-admin (its uninstall.php cleans up its own traces), and removed the leftover <slug>_sync_token option and the UpdraftCentral keys. No backdoor user existed on either site, the trigger parameter never appeared in any log, and no non-admin account carried extra capabilities. Nothing needed restoring.
Across the fleet, at the web server. Deleting keys site by site takes time, so we first closed the channel itself with two ModSecurity rules, rolled out with Ansible to all cPanel servers. We canaried them on one LiteSpeed server and one Apache server first:
# Block all UpdraftCentral / UDRPC commands (scheduled backups are unaffected)
SecRule ARGS_POST_NAMES "@streq udrpc_message" \
"id:99101,phase:2,t:none,deny,status:403,log,msg:'UpdraftCentral UDRPC blocked (Oct 2026 key compromise)'"
# Block the backdoor trigger: ?_<6 hex>=<32 chars>
SecRule ARGS_GET_NAMES "@rx ^_[0-9a-f]{6}$" \
"id:99102,phase:2,t:none,deny,status:403,log,msg:'UpdraftCentral backdoor trigger',chain"
SecRule ARGS_GET "@rx ^[A-Za-z0-9_]{32}$" "t:none"
The first rule stops any new install, whoever holds the keys and whatever IP or User-Agent they move to. The second catches a fixed variant that might already be sitting somewhere quietly. Two caveats: rule 99101 needs SecRequestBodyAccess On, and domains with ModSecurity disabled aren't covered.
Across the fleet, per site. We then swept every WordPress install, using /etc/userdatadomains so addon domains and subdirectory installs aren't missed. For each install we saved the UpdraftCentral client log, which holds the last 50 events and is useful evidence on servers without central logging, and then deleted the keys. We also looked for the template's files, its options and any non-admin account holding admin-level capabilities. We run WP-CLI with --skip-plugins so a planted backdoor can't crash the scan.
Indicators of compromise
| Indicator | Value |
|---|---|
| Source IP (seen by us) | 2a09:bac5:4e21:2e3c::49b:9 (Cloudflare WARP) |
| Source IP (reported elsewhere) | 43.250.53.42 |
| User-Agent | WordPress/6.4; class-udrpc.php (the real dashboard sends a library version and URL) |
| Request | POST /, a probe followed by plugin.upload_plugin hours later |
| Plugin description | Small maintenance toolkit: keeps option storage tidy. |
| Plugin names (ours) | Type Board 72D ("PixelBinder"), Site Kit ("Harbor Stack Labs") |
| SHA-256 type-board-72d.php | 427a964c85c158c2e60dddfe98454b1a374c0bbcd093a4f5de52c1349a804fd7 |
| SHA-256 site-kit.php | f7418f717c30b87c9004c76da0f9a8eebdc17f505da98b9d236616946da1ca88 |
| Trigger | ?_<6 hex>=<32 chars> on any URL |
| Backdoor account | display name "Site Tools", @example.org email, Editor + manage_options + install_plugins |
| Database | an option ending in _sync_token |
| Symptom | Uncaught Error: Undefined constant <Class>::FIELD, HTTP 500 on every uncached request |
Plugin names, tokens and usernames are generated per site, so the description line is the best file-level signature:
grep -rl "keeps option storage tidy" /home/*/public_html/wp-content/plugins/ /home/*/public_html/wp-content/mu-plugins/ 2>/dev/null
What to do on your own sites
- Check for a key, even if you never used UpdraftCentral:
wp option get updraft_central_localkeys. Any output means the site accepts remote commands. Connecting a TeamUpdraft.com account can create one without you noticing. - Delete it with
wp option delete updraft_central_localkeys, or in wp-admin under UpdraftPlus, Advanced Tools, UpdraftCentral. Removing the site from the dashboard is not enough, because the key that matters lives on the site. Backups keep working. - If you host other people's sites, block
udrpc_messageat the web server. It protects every site at once while the per-site cleanup catches up. - Look for the backdoor using the indicators above. If you find one, delete the key first, then move the plugin out of the web root. Don't uninstall it from wp-admin, and never fix it.
- Don't reconnect until TeamUpdraft explains how the keys got out.
What I'm taking away
Remote management keys are root passwords that never expire. A key created in 2019 for a dashboard nobody had used in years still had full plugin-install rights in 2026. We now treat any "connect this site to our cloud" feature the same way we treat an SSH key: inventoried, owned, and revoked when the person or tool behind it goes away.
Central logging paid for itself in one morning. Without the domlogs in ClickHouse, the second victim would have been found by its owner, not by us, and the probe-then-install pattern would have taken hours of grepping across servers. It's now going onto the rest of the fleet faster.
Test the theory that fits best. The CVE explanation was neat, plausible and wrong. One version number disproved it. The difference matters: a patched plugin would have meant "update and move on", while the real answer meant revoking keys everywhere.
We've reported our findings and samples to TeamUpdraft and will update this post when they respond.