How I Recovered a Hacked WordPress Site With Zero Backups
A client called me thirty minutes ago to say that their website was giving them a 500 error. Pretty normal. What I found when I opened their public_html was not what I expected.
What I Found First
The file listing told the story immediately. Dozens of zero-byte PHP files with names like xor.php, cmd.php, fput.php, bad.php, dop.php sitting in the web root alongside legitimate WordPress files. A binary .so file called dbus-plugin.so with execute permissions. A file named f_f65f5cc8e6d9 with no extension. Hidden dotfiles like .accept, .locked, .sys, .system scattered throughout.
And wp-config.php was 0 bytes. Wiped clean.
This was a compromised site.
Reading the Error Log
The error_log file was 4.85 MB and had been written to that very morning. The first entries made the story clear.
In early January 2026, malware executing from /dev/shm/.marker was attempting to upload files disguised as new_products and new_products.gz into system paths — /opt/alt/alt-nodejs18/, /opt/cpanel/ea-php83/, /opt/cpanel/ea-php72/. The uploads didn’t work because those paths didn’t exist from the web root. But the goal was to put backdoors in places outside the web root where they would be harder to find.
From February onwards, every entry was a cascade of Call to undefined function wp(). WordPress couldn’t bootstrap because wp-config.php had been wiped. The site had been pretty much dead for months.
The most worrying thing I saw was a lot of requests coming in quickly on the morning I was looking into it. Someone was still looking into it.
Establishing the Infection Timeline
With the file system clearly compromised, I moved to the database to understand the scope.
The client’s site used a non-standard table prefix (wpt3_). Querying wpt3_users revealed two accounts:
- ID 1:
adminregistered in March 2025, email matched the client’s domain. Legitimate. - ID 2:
rootregistered on October 28, 2025, emailadmin@wordpress.com. The attacker’s backdoor account.
October 28, 2025. That was the breach date.
The attacker had admin access for roughly two months before wiping wp-config.php in late December, probably to cover their tracks or as part of a more destructive phase of the attack.
Checking the Database for Injected Content
Before touching anything, I needed to know if the database itself was compromised. I ran targeted queries against wpt3_options:
SELECT option_name, LEFT(option_value, 200)
FROM wpt3_options
WHERE option_value LIKE '%eval(%';
SELECT option_name, LEFT(option_value, 200)
FROM wpt3_options
WHERE option_value LIKE '%base64_decode%';
SELECT option_name, LEFT(option_value, 200)
FROM wpt3_options
WHERE option_value LIKE '%gzinflate%';
All returned zero rows. I also checked the cron table for external URLs, clean. Standard WordPress and plugin cron jobs only.
The database remained unaltered. All of the damage occurred at the file system level.
The Recovery Plan
With no server-level backup available and the database confirmed clean, the recovery path was:
- Back up and scan
wp-content/uploads, the only file-based content worth saving - Wipe
public_htmlentirely - Fresh WordPress install
- Restore clean uploads
- Recreate
wp-config.phppointing to the existing database - Reinstall theme and plugins fresh from official sources
Step 1: Save the Uploads
cp -r /home/client/public_html/wp-content/uploads /home/client/uploads_backup
find /home/client/uploads_backup -name "*.php"
One result: /wpforms/cache/index.php — empty file, legitimate WPForms placeholder. Clean.
Step 2: Identify What to Reinstall
Before nuking, I pulled the active plugin list from the database:
SELECT option_value
FROM wpt3_options
WHERE option_name = 'active_plugins';
Seven plugins. Theme: newsxo.
WP Radio was the first plugin that caught my eye. A plugin for streaming niche radio. I made a note of it for later.
Step 3: Wipe and Reinstall
rm -rf /home/client/public_html/*
rm -rf /home/client/public_html/.*
cd /home/client/public_html
wget https://wordpress.org/latest.tar.gz
tar -xzf latest.tar.gz --strip-components=1
rm latest.tar.gz
chown -R client:client /home/client/public_html
Step 4: Restore Uploads and Recreate wp-config.php
mkdir -p /home/client/public_html/wp-content/uploads
cp -r /home/client/uploads_backup/* /home/client/public_html/wp-content/uploads/
chown -R client:client /home/client/public_html/wp-content/uploads
Then wp-config.php from the sample file, updated with:
- Correct database name and user
- Correct password (reset via WHM since original was unknown)
- Table prefix set to
wpt3_, critical, without this WordPress can’t find the existing data - Fresh salts from
https://api.wordpress.org/secret-key/1.1/salt/
Step 5: Delete the Attacker’s Admin Account
DELETE FROM wpt3_users WHERE ID = 2;
DELETE FROM wpt3_usermeta WHERE user_id = 2;
Step 6: Reinstall Theme and Plugins
Everything is installed fresh from official sources. Except WP Radio.
The Attack Vector: WP Radio
WP Radio: the radio streaming plugin the client was running is no longer available on the WordPress.org plugin repository. When a plugin is closed on WordPress.org, it can mean several things: a security vulnerability was found and left unpatched, the developer abandoned it, or it violated repository guidelines. In any of these cases, WordPress.org’s own plugin guidelines state that plugins are closed to prevent further downloads until the situation is resolved.
The client described the current listing as a completely different plugin under a similar name, a common pattern after a compromised or abandoned plugin gets pulled and another developer registers a replacement. Reinstalling an unverified replacement onto a freshly cleaned site would be reckless. I documented the situation for the client and recommended actively maintained alternatives with a clear update history.
The Result
The WordPress dashboard is up. All 6 posts, 11 pages, and site content are still there, taken straight from the database that hasn’t been changed. The client’s content survived the whole attack because attackers usually go after file system persistence, not content destruction.
The total time between the breach and recovery was about 2.5 months, most of which went unnoticed.
What I’d Do Differently (and What You Should Do Now)
For hosting clients:
- JetBackup or equivalent configured per account, not just at the server level
- Automated malware scanning via Imunify360 or similar
- Plugin update monitoring: flag plugins that go unmaintained or get removed from the repository
For WordPress site owners:
- Avoid niche plugins with small maintainer teams and infrequent updates
- Enable auto-updates for core and plugins where possible
- Use a security plugin (Wordfence or Solid Security) with file change monitoring
- Maintain at least one off-server backup (Google Drive, Dropbox via UpdraftPlus)
The most important thing is that a backup from before the breach date would have cut the time it took to recover from an hour to 20 minutes. Backups are a necessary part of infrastructure.
// Tags