Skip to content
Back to Articles

The Malware You Can’t Kill: A 15-Layer Self-Healing Framework Is Sweeping Through WordPress Servers

Share:

You find the malware. You delete every malicious file on the server. You verify: all clean. You reload the page. Every file is back. Not in minutes — in seconds. You delete them again. They return again. The server’s security scanner joins the fight, detecting and removing the files automatically. The malware keeps regenerating. The scanner keeps cleaning. The CPU climbs to 95%. The detect–clean–regenerate loop runs hundreds of times per day, per site, across hundreds of sites on the server. Nothing is actually being cleaned. Welcome to the most advanced WordPress malware framework I’ve encountered in years of incident response work.

Over the span of 10 days, I handled 10 separate cases involving the same malware family. By the time I finished, I had documented 15 independent persistence layers, a blockchain-based command-and-control channel that cannot be seized, evidence of autonomous self-upgrading, and confirmed infections across 1,200+ WordPress sites. This isn’t a backdoor. It’s a malware platform — and it’s designed to survive exactly the kind of file-based cleanup that every WordPress security tool performs.

The Core Innovation: The Database Is the Payload

Every file-based security scanner in the WordPress ecosystem — whether it’s a server-level tool, a WordPress plugin, or a manual investigation — operates on the same fundamental assumption: malware lives in files. Find the file, delete the file, the malware is gone.

This framework inverts that assumption. The real payload lives in the WordPress database, specifically in wp_options. The malware stores its entire encrypted codebase across 12–20 database rows using randomized option names — approximately 900 KB of payload per WordPress installation. The files on disk? Those are just the execution surface. They’re ephemeral. Disposable. They exist only to bootstrap the database payload into running code.

Here’s the regeneration loop in its simplest form:

1. Payload exists in wp_options (12-20 rows, ~900 KB, encrypted)
2. Any HTTP request → WordPress loads PHP
3. ANY surviving entry point reads the DB payload
4. Entry point checks: are all my filesystem components present?
5. Missing components are regenerated instantly from the DB payload
6. Security scanner detects the new files → removes them
7. Next HTTP request → goto step 3

This loop runs every 2-3 seconds per site.
Multiply by hundreds of sites on a shared server.
CPU: 95%+. Cleanup: 0%.

The security scanner is doing its job perfectly. It finds malware, it removes malware. But it cannot touch the database. And the database rebuilds everything the scanner just deleted, on the very next page request. The scanner and the malware enter an infinite feedback loop that burns server resources while achieving nothing.

15 Persistence Layers: Clean 14, the 15th Rebuilds Everything

The framework’s design philosophy is what I call redundant independence: every persistence layer can reconstruct every other layer. They don’t depend on each other in a chain — they’re a mesh. Remove any combination of them, and the survivors regenerate the missing ones from the database payload. The only way to kill the framework is to eliminate all layers simultaneously.

Here are all 15, documented across 10 real-world cases:

Layer 1 — Database Payload (The Anchor)

The encrypted payload stored in wp_options with randomized option names. Three naming patterns observed: underscore-separated pseudowords (tnfzdi_rontcia), hex strings (f58e3f70adba), and mixed-case alphanumeric (7ys3B7yBxoqe). All stored with autoload = 'off' to avoid detection by tools that scan autoloaded options. This is the anchor — everything regenerates from here.

Layer 2 — db.php Drop-In (>50 KB)

WordPress loads wp-content/db.php before its own database class. The framework drops a 50+ KB version that intercepts every database query, providing both persistence and data interception capabilities.

Layer 3 — advanced-cache.php (>5 KB)

Loaded when WP_CACHE is true in wp-config.php. The framework sets this constant (Layer 9) to ensure this drop-in is always active. In several cases, it injected itself into legitimate caching plugin files, making detection harder.

Layer 4 — Must-Use Plugins (mu-plugins)

Files dropped into wp-content/mu-plugins/ that WordPress loads automatically on every request. Cannot be disabled from the admin panel. The framework uses three-word-dash naming to blend in: forge-sentinel-box.php, haven-keeper-ultra.php, glyph-renderer-cue.php. Each includes fabricated WordPress plugin headers with fake version numbers. File size: 100+ KB. Permissions set to 0444 (read-only) to prevent casual deletion.

Layer 5 — Hidden Hex-Named PHP Files

Eight-character hexadecimal filenames, dot-prefixed: .03dec8d9.php, .9dfa4b1e.php. Invisible to default ls (requires ls -la). Each installation gets unique names.

Layer 6 — .user.ini with auto_prepend_file

PHP’s per-directory configuration file, set to load a framework file before any script executes. Dual placement: document root and wp-content/. In one case, the attacker used chattr +i to set the immutable flag — preventing deletion even by root without explicitly removing the flag first.

Layer 7 — Theme functions.php Injection

This is the primary regeneration engine. The framework injects code into the active theme’s functions.php, which fires on every single page load. In the older version, injections were marked with distinctive comment delimiters. The newer version uses no markers at all — randomized obfuscated function names with gzdecode/gzinflate wrappers that are much harder to find with automated scanning.

Layer 8 — .htaccess php_value Directive

A redundant auto_prepend_file at the Apache/LiteSpeed level. Even if you clean the .user.ini, the .htaccess directive achieves the same effect.

Layer 9 — wp-config.php Constants

Injects WP_CACHE (enabling Layer 3) and a custom framework flag. Without cleaning these, the drop-in files remain active even if the security scanner removes and restores them.

Layer 10 — ZIP Payload Archives

Cold-storage backups scattered across wp-content/, uploads/YYYY/MM/, and theme directories. Eight-character hex filenames with .zip extension. These enable offline regeneration without calling back to the C2 — the payload can rebuild itself entirely from local archives.

Layer 11 — Daemon Directories and Processes

Hidden directories named .sc_[8hex]/ containing daemon scripts that run as standalone processes with PPID=1. These are not web server child processes — they survive Apache/LiteSpeed restarts. On one server, I found 224 concurrent daemon processes running from these directories. They use shared memory segments (shmop) for fileless payload storage in kernel RAM — no on-disk footprint at all.

Layer 12 — active_plugins Serialized Array Injection (THE ROOT CAUSE)

This was the last layer we discovered, and it turned out to be the most critical one. The framework injects fake plugin names directly into the serialized active_plugins value in wp_options. WordPress reads this array on every request and attempts to load the listed plugins. The framework’s files are loaded by WordPress itself — before mu-plugins, before themes, before anything else.

This is why cleanups kept failing. You could delete every malicious file, clean every database option, strip every theme injection — and the malware would still come back. WordPress was re-loading the fake plugins from active_plugins on every single request, re-bootstrapping the entire framework.

Cleaning this layer requires PHP unserialize()/serialize() — simple string replacement breaks PHP’s serialization length prefixes and corrupts the entire plugins list.

Layer 13 — object-cache.php Drop-In (17 KB)

A second WordPress drop-in file deployed across 32+ accounts on a single server. Acts as a redundant regeneration engine alongside db.php.

Layer 14 — Standalone PHP Daemon

Long-running PHP processes (not LSPHP, not Apache children) with PPID=1, executing from deep inside wp-content/ subdirectories. These survive web server restarts and must be specifically identified and killed with kill -9.

Layer 15 — System Crontab Bypass

User-level crontabs running wp-cron.php via direct /usr/local/bin/php invocation. This triggers the framework’s regeneration logic even with the web server completely stopped — bypassing the “stop Apache before cleanup” approach that was supposed to prevent regeneration during remediation.

Bonus: MySQL Triggers

In one case, the attacker also deployed MySQL triggers: an AFTER INSERT trigger on the comments table that created a rogue admin account when a specific activation phrase was posted as a comment, and a BEFORE UPDATE trigger on the posts table that re-injected spam content every time a post was edited. Completely invisible to every file-based scanner in existence.

Blockchain C2: Command & Control You Can’t Seize

The framework’s newer versions use Ethereum blockchain for command-and-control communication, via public RPC endpoints. Instructions are stored on-chain as transaction data. This makes traditional C2 takedown impossible — you cannot seize a blockchain, shut down a smart contract through law enforcement, or redirect DNS to a sinkhole.

The older version used a REST API-based C2 with surprisingly sophisticated security: Ed25519 cryptographic signatures for authentication, AES-256-GCM encryption for the execution channel, monotonically increasing nonces for replay protection, and a 5-minute timestamp window. It even had a self-destruct: after 50 failed authentication attempts from non-loopback IPs, it would delete all its database options and its own file. A C2 with better authentication than most web applications.

The full decoded source of the older version revealed 15 remote commands, including:

  • exec — AES-encrypted arbitrary function execution, including file_put_contents, unlink, and raw SQL queries
  • self_update — Remote code update with syntax validation, backup, and OPcache flush
  • install_plugin — Install plugins from URL or inline base64 ZIP, with optional auto-activation
  • create_user — Create admin accounts or reset existing users’ passwords and roles
  • inject_js — Three-layer JavaScript injection: database headers/footers, theme file patching, and stealth .fonts-*.js files
  • site_info — Full reconnaissance including the AUTH_KEY value from wp-config.php

This is not a dumb webshell. It’s a remote administration framework with military-grade cryptography.

It Upgrades Itself

Perhaps the most alarming discovery: the framework can upgrade itself autonomously. On one server, I documented the framework evolving from version 4.0.3 to version 4.3.24 within 7 days. The upgrade added 6 new persistence layers, renamed all mu-plugin files to evade the detection signatures that already existed for the older version, and switched from marker-based theme injections to markerless obfuscated ones.

The timeline was conclusive:

  • Day 0: First case on this server. Version 4.0.3 identified. 9 persistence layers. Cleanup completed. Its file names became known detection indicators.
  • Day 7: Second case on the same server. Version 4.3.24 identified. 15 persistence layers. All mu-plugin files renamed to evade the signatures that existed for the earlier file names. Theme injections now markerless. New active_plugins bootstrap discovered.

The implication is stark: any signature-based detection has a built-in expiration date. A signature written for forge-sentinel-box.php works only until the framework, within days, renames itself to drift-dashboard-plus.php with a freshly generated plugin header. The file hash changes. The filename changes. The behavioral pattern stays identical.

Server-Wide Propagation: One Site Falls, All Sites Fall

This framework is not designed for single-site infections. It is designed for server-wide deployment. Every shared hosting case I handled involved mass infection across accounts. The propagation mechanism varies by hosting platform:

Managed Cloud Hosting

On one server, malicious files were owned by root:root inside application-user directories — conclusive proof that the infection had elevated to the server level. A root-level process was deploying the framework into all application directories in automated waves. I observed 78 applications infected simultaneously in one wave, and 159 in another. 165 out of 167 applications were compromised. The earliest artifacts dated back over two years. Application-level cleanup was impossible because the application user couldn’t delete root-owned files.

cPanel / WHM Shared Hosting

CageFS filesystem isolation doesn’t protect the database layer. The framework harvests wp-config.php credentials from the initially compromised account and connects to every WordPress database on the server via localhost. On one server: 43 databases across 44 accounts. On another: 2,267+ wp_options tables across 164 accounts. The framework stores credentials in a structured format — SITE|DB|USER|PASS|HOST|PREFIX — and iterates through them methodically.

DirectAdmin

In one case, a separate webshell framework with 79 exploit modules provided the cross-account vector, including DirectAdmin credential extraction. It sprayed backdoors across all accounts on a daily schedule. The self-healing framework provided per-site persistence. Two frameworks working in tandem: one for lateral movement, one for survival.

The rule is now absolute: when this framework is found on any site on a shared server, assume every site on that server is infected. Account-level remediation is always insufficient.

Evasion: 12 Layers of Invisibility

The framework doesn’t just survive cleanup — it actively hides from every WordPress admin interface:

  • Plugin list filtering: Removes itself from the Plugins page, WP-CLI, REST API, and multisite admin
  • Deactivation blocking: Auto-reactivates via deactivated_plugin hook; uninstall triggers wp_die()
  • Admin user hiding: pre_user_query SQL exclusion, REST 404 for the hidden user, admin count decremented, notification emails suppressed
  • Ghost admin: Orphaned wp_usermeta entries granting administrator role to a user_id that has no corresponding wp_users row — completely invisible to all user listing methods
  • Dot-prefixed files: Hidden from default ls
  • Hex-padded bloat: File sizes up to 6.9 MB (vs. 187 KB standard) filled with comment blocks to evade size-based heuristics
  • Timestomping: File dates set to 2012, 2017, or 2019 to evade recency-based scanning
  • Read-only permissions: 0444 on mu-plugin files
  • Immutable flags: chattr +i on .user.ini
  • Cache directory placement: Loaders in wp-content/cache/ — often excluded from scans
  • DB evasion: All options stored with autoload = 'off'; WP-Cron hook named to survive NOT LIKE '%cron%' exclusions
  • Signature evasion: Once its file names became known indicators, the framework renamed its files and regenerated plugin headers within days

The Root Cause We Almost Missed

After 10 cases and multiple failed cleanups on the same server, I was running out of ideas. Every cleanup followed the same pattern: stop the web server, kill all processes, clean all files, clean the database, restart — and the malware came back.

The breakthrough came during a re-work of a particularly stubborn case. After cleaning everything — files, database options, theme injections, drop-ins, daemon processes — the framework still regenerated. I started examining what WordPress itself was doing on boot, before any of the known entry points could fire.

That’s when I found it:

SELECT option_value FROM wp_options WHERE option_name = 'active_plugins';

-- Result: a serialized PHP array containing:
a:12:{
  i:0;s:31:"nexus-extension-ops/nexus-extension-ops.php";
  i:1;s:27:"unity-builder-rig/unity-builder-rig.php";
  i:2;s:25:"linkflow-control/linkflow-control.php";
  ...legitimate plugins...
}

The active_plugins option — the serialized array that tells WordPress which plugins to load — contained fake plugin names injected by the framework. WordPress was loading these fake plugins on every single request, before mu-plugins, before themes, before anything. The framework was being bootstrapped by WordPress’s own plugin loader.

Every previous cleanup had cleaned the database options (the payload rows), but never touched active_plugins because it’s a core WordPress setting, not a malware artifact. The framework had weaponized WordPress’s own plugin loading mechanism as a persistence layer.

Fixing it required PHP unserialize()/serialize():

// You CANNOT do this with string replacement:
// UPDATE wp_options SET option_value = REPLACE(...) ← BREAKS SERIALIZATION

// You MUST do this:
$plugins = unserialize($current_value);
$bad = ['nexus-extension-ops/nexus-extension-ops.php',
        'unity-builder-rig/unity-builder-rig.php',
        'linkflow-control/linkflow-control.php'];
$clean = array_values(array_diff($plugins, $bad));
$new_value = serialize($clean);
// UPDATE wp_options SET option_value = '$new_value' WHERE option_name = 'active_plugins';

PHP serialization stores string length prefixes (s:31: means a 31-character string follows). Using SQL REPLACE() changes the string content without updating the length prefix, corrupting the entire serialized array and potentially bricking the WordPress installation. The fix must go through proper PHP serialization.

Detection: What to Look For

If you’re investigating a WordPress site and suspect this framework, here are the detection methods I’ve validated across 10 cases:

Database Indicators

-- Check for framework payload options
SELECT option_name, LENGTH(option_value) AS size
FROM wp_options
WHERE option_name REGEXP '^[0-9a-f]{8,14}$'
   AND LENGTH(option_value) > 1000;

-- Check active_plugins for fake plugins (ROOT CAUSE)
SELECT option_value FROM wp_options
WHERE option_name = 'active_plugins';
-- Look for 3-word-dash names you don't recognize

-- Check for MySQL triggers (ALWAYS do this)
SHOW TRIGGERS;

Filesystem Indicators

# Must-use plugins: 3-word-dash names, >100 KB
find /path/to/wp-content/mu-plugins/ -name "*.php" -size +100k -ls

# Hidden hex-named PHP (use posix-extended — emacs regex silently fails!)
find /path/to/ -regextype posix-extended -regex '.*/\.[0-9a-f]{8}\.php' -ls

# Oversized drop-ins
ls -la /path/to/wp-content/db.php           # Should be <5 KB or absent
ls -la /path/to/wp-content/advanced-cache.php  # Same
ls -la /path/to/wp-content/object-cache.php    # <10 KB normally

# Hidden directories
find /path/to/wp-content/ -type d -name ".sc_*" -ls

# Theme injection (markerless — look for gzdecode in functions.php)
grep -rlE 'gzdecode|gzinflate' /path/to/wp-content/themes/*/functions.php

Process Indicators

# Framework daemon processes
ps aux | grep -E '\-\-sc-cli|\-\-sc-succ|\.gr_|\.g_'

# Shared memory segments from PHP
ipcs -m | grep -v "^---"

The Kill Protocol

After 10 cases of iterating on cleanup approaches — including several failures — this is the validated remediation sequence. Every step is non-negotiable. Skipping any one of them leaves at least one regeneration path active.

  1. Stop the web server. systemctl stop httpd or systemctl stop lsws. No HTTP requests can reach WordPress during cleanup. This is the mandatory first step.
  2. Comment out system crontabs that run wp-cron.php via direct PHP. These bypass the web server shutdown.
  3. Kill all framework processes. Identify by --sc-cli flag or .gr_/.g_ patterns. Kill all lsphp processes. Verify with ps aux.
  4. Full database backups. Dump every infected database before touching anything.
  5. Clean active_plugins in ALL databases. This is the root cause. Use PHP unserialize()/serialize() — never SQL string replacement. Generate a script that processes every database.
  6. Bulk SQL cleanup. DELETE all framework options, hex-named options with large values, transients, and cron hooks across every database.
  7. Filesystem nuke — all 15 categories simultaneously. Delete: hidden directories, daemon files, drop-ins (db.php, advanced-cache.php, object-cache.php), fake plugin directories, mu-plugins, hex-named PHP, ZIP archives, marker files. Surgically strip theme functions.php injections. Clean wp-config.php constants. Clean .user.ini and .htaccess injections.
  8. Drop MySQL triggers. SHOW TRIGGERS on every database. DROP TRIGGER any that are not legitimate.
  9. Restart the web server. Immediately verify: spot-check 3–5 sites for HTTP 200. Run a second filesystem scan to confirm no regeneration.
  10. Mop-up pass. Re-run DB queries for rows that failed. Check for chattr +i remnants. Flush all caches. Restore crontabs.

On shared hosting with hundreds of infected sites, this entire sequence must be scripted and executed in one pass. The window between stopping the web server and restarting it is the only time the framework is vulnerable.

Why This Matters

This framework represents a generational shift in WordPress malware. Previous generations stored their payload in files, which meant file-based scanners could detect and remove them. This generation stores its payload in the database and uses the filesystem purely as a disposable execution layer.

The implications:

  • File-based scanning is architecturally incapable of defeating this class of threat. You can have the best file scanner in the world, with perfect signatures, 100% detection rate — and this framework will survive it indefinitely. The scanner becomes part of the problem, not the solution, because the detect–clean–regenerate loop burns CPU.
  • Blockchain C2 means there is no takedown. You cannot seize a blockchain address. You cannot redirect DNS. You cannot convince a registrar to suspend a domain. The C2 is permanent.
  • Self-upgrading means signatures expire. A detection rule written today misses the renamed files tomorrow. The only durable detection is behavioral, not signature-based.
  • Server-wide propagation means no partial cleanup. If one site is infected, assume all sites on the server are infected. Account-level remediation is always insufficient.

WordPress powers over 40% of the web. The vast majority of WordPress security tools — plugins, server-level scanners, managed security services — are file-based. A malware platform that has decisively moved its persistence layer to the database represents a fundamental mismatch between the threat and the defenses deployed against it.

The race is on to close that gap. Until then, this framework will keep rebuilding itself, 2–3 seconds at a time, across thousands of sites, in perfect silence.