All posts
Infrastructure & Hosting17 April 2026·6 min read

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: admin registered in March 2025, email matched the client’s domain. Legitimate.
  • ID 2: root registered on October 28, 2025, email admin@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:

  1. Back up and scan wp-content/uploads, the only file-based content worth saving
  2. Wipe public_html entirely
  3. Fresh WordPress install
  4. Restore clean uploads
  5. Recreate wp-config.php pointing to the existing database
  6. 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.

Found this useful? Share it.

cPanelhacked WordPressLiteSpeedmalware removalserver administrationweb hostingWordPressWordPress pluginsWordPress recoveryWordPress security