We Got Hacked. Here Is What We Did.

by

this post has been formatted by AI

On July 28, 2026, we investigated a major WordPress exploit that was actively being used in the wild. One of our WordPress properties, let.live, was down, and that changed the question from “are we vulnerable?” to “were we compromised while vulnerable?”

The short version: yes, let.live had been compromised.

We found evidence of a successful exploit, contained the compromised host, rebuilt the site from a pre-exploit backup, restored only vetted newer content, and moved production traffic to a clean replacement server.

This is the write-up of what happened and what we did.

The Exploit

The active exploit chain was being referred to as wp2shell. The vulnerable WordPress versions included WordPress 7.0.0 and 7.0.1, fixed in 7.0.2.

The critical endpoint involved was:

/wp-json/batch/v1

That mattered because let.live had been running WordPress 7.0 before we patched it.

Initial Triage

We first checked our WordPress properties for exposure.

people4liberty.org was already patched as was this site fishdan.com. Both looked healthy:

  • WordPress core was patched.
  • HTTPS returned 200.
  • /wp-json/batch/v1 returned 404.

let.live was more concerning:

  • The site was down.
  • The original Lightsail IP was unreachable.
  • WordPress had previously advertised a vulnerable version.

We recovered access by stopping and starting the Lightsail instance, then attaching a static IP so the host would have a stable target while we investigated.

Evidence of Compromise

Once the server was reachable, we reviewed Apache logs, WordPress users, plugins, files, processes, and database state.

The first confirmed exploit hit was:

2026-07-20 22:44:38 UTC
POST /?rest_route=/batch/v1

Less than 20 seconds later, a rogue WordPress administrator had been created:

user / user@wordpress.org
created 2026-07-20 22:44:56 UTC

We also found a malicious plugin:

wp-content/plugins/yoast-seo-plus-b9c0b4de/

The plugin contained command-execution code, including functions such as:

shell_exec
exec
system
passthru
popen
proc_open

It also contained an explicit wp2shell command runner and a secret token.

The logs showed that the attackers read wp-config.php, queried users and plugins, and later launched an xmrig cryptocurrency miner on July 27.

At that point, the host was not merely vulnerable.

It was compromised.

Containment

We treated the server as hostile but useful for forensics.

Immediate containment steps included:

  • Patched WordPress core to 7.0.2.
  • Deactivated the malicious plugin.
  • Archived the malicious plugin for evidence.
  • Removed the malicious plugin from the webroot.
  • Downgraded the rogue administrator account and destroyed its sessions.
  • Killed miner-related processes.
  • Removed miner and webshell artifacts from /tmp.
  • Rotated WordPress salts.
  • Rotated the local WordPress database password.
  • Verified WordPress core checksums.

The malicious plugin archive was preserved at:

/opt/bitnami/forensics/wp2shell-20260728/

We did not delete the compromised server.

We retained it for forensics.

Finding a Clean Restore Point

The most important recovery question was:

Do we have a backup from before the exploit?

We found two relevant Lightsail snapshots:

  • np-migration-letlive-20260625
  • let-live-pre-reboot-20260728-225513

The July 28 snapshot was post-compromise and therefore useful only for forensics.

The June 25 snapshot was from before the first confirmed exploit on July 20, so it became our clean baseline.

We also had the actual migration backup files from June 25:

/home/dfish/backups/np-migration-20260625/letlive/

Those included:

np-migration-letlive-rds.sql.gz
np-migration-letlive-wp-content.tar.gz
np-migration-letlive-wp-config.php

That distinction mattered.

The Lightsail snapshot itself was effectively stock WordPress content. The real historical let.live content was in the migration database and wp-content backups.

Preserving New Content

The exploit happened after the June 25 migration, but legitimate posts had been published after that date.

We did not want to lose them.

From the contained host, we exported only vetted post-migration content:

  • Liberty in Pieces
  • Daylight Saving or Standard? The Sun Doesn't Care!
  • One referenced image attachment

We explicitly excluded attacker-created trash and auto-draft content.

The clean export was:

/tmp/letlive-wp2shell-recovery/export-20260728-2/letlive-published-posts-after-migration.xml

We checked the export for known bad indicators:

yoast-seo-plus
wp2shell
xmrig
the malicious token
user@wordpress.org
trash records
auto-draft records

The clean WXR passed those checks.

The Rebuild

Our recovery strategy was:

  1. Build a replacement server from a pre-exploit baseline.
  2. Patch WordPress before serving public traffic.
  3. Restore the full pre-exploit database and wp-content.
  4. Reapply only vetted newer posts and media.
  5. Rotate credentials.
  6. Validate content and security checks.
  7. Move the static IP to the clean host.

We first created:

let-live-clean-20260728

from the June 25 snapshot and patched it to WordPress 7.0.2.

That host validated clean, but once we realized that the full historical content was not present in the snapshot itself, we restored the real migration database and wp-content.

The full content set was too heavy for the nano_3_0 Lightsail bundle. The host wedged under load, so we created a larger replacement:

let-live-full-clean-20260728

That host used small_3_0, restored the full June 25 backup, reapplied the two vetted July posts, and became the final production target.

Validation

After the final restore, we validated that:

  • WordPress core was 7.0.2.
  • WordPress core checksums passed.
  • /wp-json/batch/v1 returned 404.
  • The malicious plugin directory was absent.
  • The rogue user@wordpress.org account was absent.
  • No wp2shell, malicious token, or xmrig indicators were found in wp-content.
  • No miner processes or suspicious cron entries were present.
  • HTTPS returned 200.
  • Historical and July content appeared on production.

We also compared the 20 most recent published blog posts between the retained compromised host and the clean production host.

The comparison matched exactly by:

  • Post ID
  • Post date
  • Slug
  • Title

Final restored count:

145 published posts

Cutover

We attached the stable Lightsail static IP to the clean full-content host:

44.211.112.166 -> let-live-full-clean-20260728

GoDaddy DNS for let.live and www.let.live was updated to point at that IP.

Production then validated:

https://www.let.live/ -> HTTP 200
/wp-json/batch/v1 -> 404

The site uses /%postname%/ permalinks, so canonical post URLs look like:

https://www.let.live/stop-kink-shaming-ai-users/
Liberty in Pieces
Daylight Saving or Standard? The Sun Doesn’t Care!

What We Learned

The main lesson is that patching is not recovery.

Once an attacker has administrative access and can upload a plugin with command execution, the host cannot be trusted.

Even if WordPress core verifies clean afterward, the safer path is to rebuild from a pre-compromise backup and migrate only vetted content forward.

Other lessons:

  • Keep pre-incident backups clearly separated from forensic snapshots.
  • Do not assume a cloud instance snapshot contains the full application payload if migration files exist separately.
  • Export new content before rollback, but inspect the export for attacker-created records.
  • Preserve compromised systems for forensics instead of immediately deleting them.
  • Validate both security indicators and business content before declaring recovery complete.
  • Use stable IPs or DNS infrastructure that make emergency cutovers explicit and repeatable.
  • Size the replacement host for the real application, not the stock WordPress baseline.

Current State

Production let.live is now running on the clean rebuilt host:

let-live-full-clean-20260728
44.211.112.166
WordPress 7.0.2

The compromised host remains retained for forensic review.

It should not be decommissioned until we explicitly decide that the evidence is no longer needed.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *