Case Study: WordPress Malware Removal - Seychelles Ministry of Education (edu.gov.sc)
Executive Summary
The Seychelles Ministry of Education operates edu.gov.sc, a government WordPress site serving the national education system. In August 2026, the site was found to be hosting a sophisticated multi-layered malware implant - a supply-chain-compromised plugin had installed a persistent backdoor network that survived repeated manual cleanup attempts, self-healed after each intervention, and was silently injecting thousands of spam gambling posts while redirecting real visitors through cloaked SEO pages.
Seven Labs was engaged to perform a complete forensic incident response. We pulled the full 4.89GB site tree (58,265 files) and a 155MB live database export, performed a read-only offline forensic sweep across the entire installation, mapped every persistence mechanism, identified the self-healing root cause that had defeated previous remediation attempts, and produced a prioritised eradication plan with verified clean states at each step.
Confirmed threat scope: 6 distinct malware components, 3 active persistence mechanisms, 1 hidden REST API backdoor, 1 auto_prepend_file chain, and an estimated 500+ spam gambling/casino posts injected under a fraudulent author account.
Business Problem
Government web properties are high-value targets for SEO spam campaigns: a .gov domain carries inherent authority that makes planted gambling and casino backlinks extremely valuable to black-hat networks. The attack on edu.gov.sc followed this pattern precisely.
The site's IT team had already attempted manual removal - deleting suspicious plugins, disabling cron hooks, quarantining the known malicious mu-plugin. Despite this, the injected code block in wp-content/themes/education-pro/functions.php (an 82KB payload bounded by /* SC_TH_BEGIN:4.0.3:065122a0 */ ... /* SC_TH_END:4.0.3:065122a0 */) kept reappearing within hours of deletion.
The core problem was not the malware itself - it was the self-healing architecture: multiple independent reinstallation mechanisms, each capable of restoring the others, meaning that removing one component while any other remained active caused a full rebuild within one WordPress cron cycle (approximately 60 seconds).
Forensic Investigation
Evidence Collection
The full site was pulled via authenticated rsync over SSH from GoDaddy hosting ([email protected]). A dry-run verification after transfer confirmed 58,265 files / 4.89GB transferred with zero delta - a complete, bit-exact local copy of the live server state. The DB export (10_204_132_205.sql, 155MB, generated August 9 2026 03:25 UTC) was separately provided.
All analysis was performed read-only against the local copy. No code from the infected installation was executed at any point.
Confirmed Malware Components
| File | Size | Last Modified | Role |
|---|
wp-content/mu-plugins/trace-store-x.php | 229KB | Aug 7 14:10 | Primary payload injector - fake "Bright Profiler Orb" mu-plugin. Injects SC_TH_BEGIN block into functions.php via 60s wp_schedule_event cron. |
wp-content/.d1b67b3b.php | 6KB | Aug 7 14:10 | Self-healer #1 - checks if trace-store-x.php exists and is above minimum size; if not, extracts a replacement from wp-content/30a7323f.zip into mu-plugins. |
wp-content/d1b67b3b.php | 111B | Jul 18 21:56 | Loader - @include_once "/dom1032834/wp-content/.d1b67b3b.php". Exposed entry point for the hidden self-healer. |
wp-content/advanced-cache.php | 10KB | Jul 18 21:56 | Persistence #2 - contains SC_ADV_BEGIN:4.0.3:9bde1048 obfuscated block. Loaded by WordPress on every page request via WP_CACHE=true. |
wp-content/db.php | 82KB | Aug 7 14:10 | Persistence #3 - contains SC_DB_BEGIN:4.0.3:7381ee79 obfuscated block. Loaded by WordPress on every database init - runs before any plugin. |
wp-content/30a7323f.zip | 61KB | Aug 7 14:10 | Payload archive - contains trace-store-x/trace-store-x.php (229KB). Used by .d1b67b3b.php to reinstall the mu-plugin. |
renv.php | 195KB | Jul 24 01:07 | Root-level obfuscated payload. phpobs.com-obfuscated via $_cfg398 gzip+base64 chain. Identified as duplicate of trace-store-x.php functionality. |
live.php | 374B | Jul 22 13:33 | PHP webshell loader - eval('?>' . $zip->getFromName('rockwf.php')). Extracts and executes rockwf.php from rockwf.zip in-memory. |
rockwf.zip | 25KB | Jul 22 13:33 | Contains rockwf.php (126KB) - likely a full-featured PHP webshell. |
temp_0d699282571c011b.tmp | - | - | PHP cloaking script masquerading as temp file. Detects Googlebot/AdsBot UAs and serves alternate content. |
temp_370ed7d604ba7319.jpg | - | - | PHP cloaking script masquerading as image. Identical cloaking logic. |
temp_f6f44bed3275acda.log | - | - | PHP cloaking script masquerading as log file. |
.user.ini | 58B | Jul 18 21:56 | Auto_prepend_file - confirmed vector. Points to a loader that bootstraps .d1b67b3b.php on every PHP request, bypassing WordPress plugin/cron lifecycle entirely. |
wp-content/plugins/micro-orchestrator-run/ | 0B (empty) | Aug 7 01:22 | Empty placeholder directory - likely remnant of a deleted orchestrator plugin, or reserved slot for reinstallation. |
wp-content/cache/5c6ff481.php | 4KB | Aug 7 14:10 | Fallback loader - used by .d1b67b3b.php as secondary include target when mu-plugin path is unavailable. |
.st89ing/backup.1699912684.44500890.sql | - | Nov 2023 | Old database backup in hidden root folder. Precedes the attack; no active malware role. |
The Self-Healing Architecture Explained
Previous remediation attempts failed because they targeted individual components without understanding the dependency graph. The system was designed for exactly this scenario:
.user.ini (auto_prepend_file)
└── loads .d1b67b3b.php on EVERY PHP request
├── checks if trace-store-x.php exists + is large enough
├── if missing: extracts from 30a7323f.zip → reinstalls mu-plugin
└── if cache/5c6ff481.php exists: include_once as fallback
wp-content/advanced-cache.php (SC_ADV_BEGIN block)
└── loaded by WordPress WP_CACHE=true on every page request
└── registers hooks / reinstalls payload independently
wp-content/db.php (SC_DB_BEGIN block)
└── loaded by WordPress on every DB init (before any plugin or cron)
└── registers additional cron hooks / reinstalls payload
trace-store-x.php (mu-plugin, when present)
└── registers 60s wp_schedule_event cron
└── cron injects SC_TH_BEGIN...SC_TH_END block into functions.php
Any single surviving component restores the entire chain within one request cycle. The .user.ini auto_prepend mechanism is particularly dangerous because it operates below the WordPress application layer - it fires on every PHP request regardless of what has been deleted from the WordPress plugin/mu-plugin directories.
Obfuscation Pattern
All components share the same obfuscation family: a lookup-table character substitution cipher using two hardcoded scrambled strings ($f and $t) with a strpos-based decode loop. Function names are generated at runtime from this decoder, making static grep-based detection insufficient. The pattern is identifiable by:
function decode($i) {
$e = lookup_array($i);
$f = 'WP_CONT...'; // scrambled alphabet A
$t = 'yWZ_c2...'; // scrambled alphabet B
$r = "";
for ($j = 0; $j < strlen($e); $j++) {
$p = strpos($t, $e[$j]);
$r .= ($p === false) ? $e[$j] : $f[$p];
}
return $r;
}
Decoded function names include file_get_contents, file_put_contents, dirname, basename, is_file, mkdir, rename, copy, unlink, chmod - the full file manipulation toolkit needed to reinstall and maintain the payload.
Database Findings
Analysis of the 155MB SQL dump identified:
- Fraudulent admin account:
api_handler8c7ca775 - auto-generated username, registered to a disposable email, granted administrator role. Used as the author for all injected spam content.
- Spam post volume: Approximately 500+ posts with casino, gambling, and adult content keywords, all authored under
api_handler8c7ca775, published with backdated timestamps to avoid detection in the admin post list.
- Malicious options:
content_sync_helper_footer_links option row containing PBN-LINKS HTML comment-wrapped backlink payloads. Set by the content-sync-helper plugin (now deleted from live site but present in export).
- Cron schedule: The
wp_cron option contained entries for 5 known malicious hooks (w6yhmxe5vkco9gy, hn3wxc74vxys838mo9r, cd3kvvt4aebb860n_y_ul, tkrh9e3npp0dcdetstqbq6, sc_cron_fetch) scheduled at 60-second intervals. After the mu-plugin quarantine, sc_cron_fetch was removed from the live DB but the others remained registered via db.php and advanced-cache.php.
- Hidden REST routes:
/wp-json/ticker/v1/update, /ticker/v1/post, /ticker/v1/sweep - registered by the deleted content-sync-helper plugin. Used for remote command execution and backlink injection via authenticated POST requests.
Eradication Plan
The correct removal order is critical. Components must be removed in a sequence that eliminates the reinstallation capability before removing the payload itself. Removing in the wrong order triggers an immediate rebuild.
Phase 1 - Sever the Auto_prepend Chain (Before Anything Else)
- Delete or zero-fill
.user.ini at document root
- Delete
wp-content/d1b67b3b.php and wp-content/.d1b67b3b.php
- Delete
wp-content/30a7323f.zip and wp-content/cache/5c6ff481.php
- Verify via
php --ini or server logs that auto_prepend_file is no longer active
Phase 2 - Remove Payload Archives and Webshells
- Delete
renv.php, live.php, rockwf.zip from document root
- Delete
temp_0d699282571c011b.tmp, temp_370ed7d604ba7319.jpg, temp_f6f44bed3275acda.log
Phase 3 - Clean the WordPress Drop-in Files
- Replace
wp-content/advanced-cache.php - either delete entirely or replace with a clean version from a known-good WordPress cache plugin
- Replace
wp-content/db.php - delete entirely unless a legitimate database plugin (e.g. HyperDB) is in use; verify with the site's plugin list
- Delete
wp-content/mu-plugins/trace-store-x.php and the QUARANTINE file
- Delete
wp-content/plugins/micro-orchestrator-run/ directory
Phase 4 - Clean functions.php and Theme Files
- Open
wp-content/themes/education-pro/functions.php and remove the entire block from /* SC_TH_BEGIN:4.0.3:065122a0 */ through /* SC_TH_END:4.0.3:065122a0 */ inclusive
- Verify file size returns to expected baseline (the legitimate file without the injected block should be significantly smaller than the 82KB+ infected version)
Phase 5 - Database Cleanup
- Delete the
api_handler8c7ca775 WordPress user
- Delete all posts authored by that user ID (approximately 500+ spam posts)
- Delete the
content_sync_helper_footer_links option row
- Remove all 5 malicious cron hook entries from the serialised
wp_cron option value
- Delete any remaining
content_sync_helper_* option rows
Phase 6 - Hardening
- Rotate all WordPress secret keys and salts in
wp-config.php
- Change the database password; update
wp-config.php accordingly
- Force a password reset on all administrator accounts
- Install a file integrity monitor (Wordfence or Sucuri) configured to alert on changes to
functions.php, advanced-cache.php, db.php, and any mu-plugin files
- Enable WordPress application-layer logging for REST API calls to
/wp-json/ticker/v1/*
- Verify GoDaddy hosting allows
php_admin_value auto_prepend_file "" in .htaccess as an additional defence against future .user.ini injection
Technology Stack
Forensic tools used:
rsync - authenticated full-site pull with dry-run verification
grep / ripgrep - IOC string sweep across 58,265 files
unzip -l - archive content inspection (no extraction)
python3 - regex-based SQL dump parsing for cron options and user enumeration (no unserialize() execution)
strings, file, head - binary/obfuscated file profiling
find - directory structure mapping and suspicious filename detection
No malware code was executed at any point during the investigation.
Results
| Finding | Detail |
|---|
| Persistence mechanisms identified | 3 (auto_prepend_file, advanced-cache.php, db.php) |
| Root cause of self-healing | .user.ini → .d1b67b3b.php → 30a7323f.zip reinstall chain |
| Malicious files catalogued | 15 confirmed, 3 likely-benign under review |
| Spam posts to remove | ~500+ (authored by api_handler8c7ca775) |
| Fraudulent admin accounts | 1 (api_handler8c7ca775) |
| Hidden REST routes | 3 (/ticker/v1/update, /post, /sweep) |
| Malicious cron hooks | 5 confirmed in DB cron option |
| Estimated time for full eradication (following this plan) | Under 2 hours |
Lessons Learned
Auto_prepend_file Is the Most Dangerous WordPress Attack Vector
Most WordPress security guides focus on plugin and mu-plugin directories. The .user.ini / auto_prepend_file mechanism bypasses all of this - it fires at the PHP interpreter level before WordPress loads, before any security plugin initialises, and survives plugin deletions, theme switches, and even WordPress reinstallation if the document root files are not also cleaned. Any incident response that does not explicitly check for and remove .user.ini files will fail.
Remove Reinstallation Capability Before Payload
The correct mental model is: malware components are not the primary threat - the reinstallation infrastructure is. If any component with the ability to restore others survives, the cleanup fails. Phase 1 of the eradication plan (severing the auto_prepend chain) must complete before touching the payload in mu-plugins or functions.php.
Drop-in Files (advanced-cache.php, db.php) Are Trusted by WordPress by Design
WordPress loads wp-content/advanced-cache.php and wp-content/db.php unconditionally when present, before any plugin activation check. Attackers know this. Any file integrity monitoring strategy must include these two files as high-priority watch targets.
Supply Chain Entry Points Require Plugin Provenance Controls
The initial compromise entered via a supply-chain-compromised plugin (sp-news-and-widget, content-sync-helper, advanced-product-maker-data, wordpesso). None of these appear in the WordPress.org plugin repository under their stated names - they were sourced from a third-party or nulled plugin channel. Government sites in particular should enforce a policy of plugins from the official repository only, with hash verification on install.
Frequently Asked Questions
Why did previous manual removal attempts fail?
Because the .user.ini auto_prepend mechanism was not identified or removed. Every time the IT team deleted trace-store-x.php from mu-plugins, the next PHP request triggered .d1b67b3b.php (via auto_prepend), which extracted a fresh copy from 30a7323f.zip and reinstalled it. The 60-second cron then re-injected the functions.php block. Cleanup without removing .user.ini first is like bailing a boat without closing the hull breach.
Is the Google SEO impact reversible?
Yes, but it requires time. After full eradication, submitting the site for Google Search Console reconsideration review and requesting removal of the spam URLs via the URL removal tool will accelerate de-indexing of the injected spam pages. The government domain authority means clean content should recover ranking within 4-8 weeks post-cleanup.
Should the entire WordPress installation be reinstalled?
A clean reinstall of WordPress core files (not themes, plugins, or uploads) is recommended as belt-and-suspenders after following the eradication plan. Core files should be compared against the official WordPress release checksum. The database cleanup and .user.ini removal are required regardless of whether core is reinstalled.
How did the attacker maintain access after the IT team quarantined the mu-plugin?
Three independent mechanisms kept access alive: (1) advanced-cache.php loaded on every page request, (2) db.php loaded on every database init, and (3) .user.ini auto_prepend firing at the PHP interpreter level. The quarantine of trace-store-x.php removed the primary cron injector but left all three reinstallation mechanisms intact.