Inside a Fake WordPress Plugin: How "WP Content Optimizer" Takes Over a Site
During a routine review of Proactive Defense events, our security team noticed widespread activity from what appeared to be a WordPress optimization plugin called "WP Content Optimizer." The plugin header claimed version 3.0.2, authored by "Developer Tools Team," providing "advanced content delivery optimization and site health monitoring."
None of that was true. The plugin is a sophisticated backdoor packed into roughly 1,100 lines of PHP. It creates a hidden administrator account, makes itself invisible, removes security plugins, fights off competing malware, persists through deletion attempts, and delivers encrypted JavaScript payloads fetched from a Binance Smart Chain smart contract.
This post walks through the malware step by step: what it does, how it works, and why it makes the choices it does. We're publishing the full Indicators of Compromise so defenders can check their own environments.

Phase 1: Establishing Control
The first thing the malware does on activation is create a hidden administrator account. The username follows the pattern usr_<8-char-hash>, and the password is derived deterministically from a master key combined with the site's domain:
hash('sha256', $master_key . '|' . $domain . '|' . $purpose)
The master key is hardcoded: 7f3ac891d4e60b52a9f7186db345ce90. This means the attacker can calculate the credentials for any infected site just by knowing its domain. They never need to store passwords or maintain a database of compromised sites.
The email is set to wp-<6hex>@<site-domain>, using the target site's own domain so it doesn't stand out in a user list.
Before creating the account, the malware temporarily detaches five WordPress hooks: user_register, set_user_role, add_user_role, profile_update, and wp_login. This prevents any notification emails from being sent, and, more importantly, prevents activity logging plugins from recording the new user creation. The hooks are reattached immediately after.
The backdoor user is marked with a custom meta key, _wp_ui_render_cfg, which the malware uses to identify its own user in later operations.
Why this approach: Deterministic credentials eliminate a common weakness of backdoors: the need to communicate credentials back to the attacker. There's no C2 call, no DNS lookup, no exfiltrated data at this stage. The attacker already knows the password before the plugin is even installed.
Phase 2: Becoming Invisible
With the admin account created, the malware hides both itself and its user from every surface in WordPress.
User hiding works by injecting conditions into database queries. The pre_user_query hook adds AND user_login != '<backdoor_user>' to every WP_User_Query WHERE clause. The REST API, author dropdowns, and user list table queries all get similar filters. The user count display in the admin panel is decremented by one for both the "All" and "Administrator" rows, so the numbers still look correct.
If another admin tries to directly access the backdoor user's profile page, the malware intercepts the request and returns "Invalid user ID."
Plugin hiding takes a less common approach. Most malicious plugins remove themselves from the all_plugins filter, but some security scanners specifically monitor that filter for missing entries. Instead, this malware injects a <style> tag into the admin page that sets display:none!important on its own row in the plugin list. The effect is the same (the plugin doesn't appear), but the mechanism is harder to detect through filter inspection.
Plugin counts are also decremented via the wp_get_update_data filter, and the mu-plugins list is filtered to hide the persistence dropper (more on that in Phase 5).
Why CSS instead of a filter: This is a deliberate evasion choice. The all_plugins filter is a known hiding spot, and some security tools check for plugins that appear in the filesystem but not in the filter output. CSS injection achieves the same visual result through a path that fewer scanners monitor.
Phase 3: Eliminating Defenses
This is the most aggressive part of the malware and the most consequential for site security.
The plugin scans the filesystem for security tools, identifying them by functional fingerprint rather than by name. It reads the first few kilobytes of each plugin's main file and checks for characteristic strings:
| Security Tool | Detection Strings |
|---|---|
| Wordfence | wfConfig, wfLog, wfScan, wfFirewall, wfUtils, wfWAF |
| iThemes / Solid Security | ITSEC_ |
| Cerber | cerber_admin, cerber_security, cerber_load |
| All-In-One Security | AIOWPSecurity |
| BulletProof Security | BulletProof \+ Security |
| Defender | WP_Defender |
| NinjaFirewall | NinjaFirewall |
| Shield Security | ICWP_ |
| Activity Logs | WP_Security_Audit_Log, Simple_History, Activity_Log |
| Loginizer | Loginizer |
When a match is found, the removal is thorough:
- The plugin is deactivated by directly writing to the active_plugins option in the database.
- The plugin directory is deleted recursively using raw PHP unlink() and rmdir() calls.
- Any matching mu-plugins are deleted (checking for wordfence, wfConfig, SucuriScan, cerber, ITSEC_, NinjaFirewall, wp-defender).
- WAF bootstrap files are removed: wordfence*.php in the document root, .htaccess.wf-backup.
- The .user.ini file is cleaned: auto_prepend_file and auto_append_file directives are stripped. These directives are how several WAF products bootstrap themselves before WordPress loads.
- WAF data directories are deleted: wflogs/, wfcache/, nfwlog/, cerber/.
- Security rules are stripped from .htaccess: Wordfence WAF blocks, iThemes rules, All In One WP Security directives, BulletProof Security sections.
- Security-related cron jobs are purged: anything matching wordfence, sucuri, itsec, cerber, malcare, defender, wpdef, or ninja.
This runs in two modes. A full scan executes every 10 minutes on admin pages (admin_init). A lighter version runs every five minutes on the frontend (init), covering just deactivation and WAF cleanup. The throttling is managed through WordPress transients.
Why direct filesystem calls: This is the critical design choice. WordPress provides functions like delete_plugins() that fire hooks like pre_delete_plugin, giving security tools a chance to intervene. The malware skips these entirely. Raw PHP calls to unlink() and rmdir() cannot be intercepted by any WordPress hook. It's a deliberate step below the application layer, where WordPress-based defenses have no visibility.
Phase 4: Securing Territory
The malware doesn't just defend against security tools. It actively fights for exclusive control of the site by removing competing attackers.
Foreign backdoor cleanup identifies and demotes other attackers' admin accounts using three detection methods:
First, it checks user meta keys associated with known backdoor families: _wp_cache_hash, _wps_sig, _sys_token, _bk_hash, _adm_key, _wp_sys_hash, _stk_sig. It recognizes its own marker (_wp_ui_render_cfg) and leaves those accounts alone.
Second, it maintains a list of suspicious usernames, stored as MD5 hashes to avoid detection by string-based scanners: wp_update, wpsystem, backupadmin, db_admin, sysadmin, wp_maintenance, security_check, admin_backup, and roughly a dozen more.
Third, it flags admin accounts using disposable email domains (also MD5-hashed): protonmail.com, tutanota.com, guerrillamail.com, tempmail.com, mailinator.com, and others.
A fourth heuristic catches accounts that don't match any specific pattern: admins with a user ID more than five positions after the site owner's, zero published posts, and a generic email domain like wordpress.org or example.com.
Detected foreign backdoors are demoted to subscriber role rather than deleted, reducing the chance of triggering error logs or missing user references.
Competing malware removal goes after other malicious plugins. The detection logic looks for plugins that hook wp_footer or wp_head, fetch remote content, hide themselves from the plugin list, and contain three or fewer PHP files. Plugins using blockchain-based injection (checking for eth_call and jsonrpc strings) that also hide themselves are flagged as well. The malware also cleans eval(base64_decode(...)) injections appended to theme functions.php files.
It even removes older versions of itself: directories named xkrfp/ and toolkit-service/ under the plugins folder.
A built-in webshell scanner sweeps uploads/, plugins/, and the WordPress root for files matching common shell patterns: eval(base64_decode(, eval(gzinflate(, system($_GET[, and similar. Matching files are deleted, with one exception: the scanner whitelists files containing its own class name (WP_Site_Performance) or its option prefix (_wco_v). It also adds an .htaccess rule to the uploads directory blocking PHP execution, a legitimate hardening measure that happens to protect only its own territory.
Why fight other malware: Compromised WordPress sites are valuable real estate. A site can only serve one set of redirect targets, one set of injected links, one set of JavaScript payloads. Multiple infections competing for the same hooks create instability that increases the chance of detection. By cleaning out competitors, this malware reduces its own exposure while monopolizing the site's monetization potential.
Phase 5: Persistence
The malware uses a three-layer persistence strategy designed to survive deletion attempts.
Layer 1: Self-reactivation. On every init and admin_init hook, the plugin checks if it's still in the active_plugins option. If it's been removed, it re-inserts itself. This defeats deactivation through the WordPress admin panel.
Layer 2: mu-plugins dropper. The malware installs a small loader file at wp-content/mu-plugins/wp-<6hex>-loader.php. WordPress automatically loads all PHP files in the mu-plugins directory on every request, without requiring activation. This loader does two things: if the main plugin file has been deleted, it restores it from backup; if the plugin has been deactivated, it re-adds it to active_plugins. The dropper is refreshed daily based on file modification time.
Layer 3: Backup copy. The malware stores a copy of itself at wp-content/upgrade/wp-maintenance.tmp. This is a location that many cleanup operations overlook: the upgrade/ directory is used by WordPress core for temporary update files, and a file named wp-maintenance.tmp looks like a routine artifact.
The result: deleting the plugin from wp-content/plugins/ triggers a restore from backup on the next page load. Deactivating it triggers re-activation. Removing the backup but leaving the mu-plugin dropper means it gets restored the next time the dropper runs. All three layers need to be addressed simultaneously.
Email suppression adds a supporting layer. The wp_mail function is filtered at priority 999\. If any outgoing email mentions the backdoor username or email address in the subject, body, or recipient fields, the to field is blanked. This prevents password reset notifications, security alerts, and any other email that might reveal the backdoor user's existence.
The malware also blocks user enumeration: the REST API /wp/v2/users endpoint returns 403 for unauthenticated requests, ?author=N queries redirect to the homepage, and wp.getUsers is removed from XML-RPC methods.
Phase 6: Delivering the Payload
The final component is the actual payload delivery system, implemented as a separate class called WP_Content_Delivery_Engine.

The delivery chain works in four steps:
Step 1: Query the blockchain. The malware calls method 0x3bc5de30 on BSC smart contract 0x8170119B70b4E8c65Ef821425c00c35CDA15d9b0 via JSON-RPC. It cycles through nine BSC RPC endpoints if any are unreachable, starting with bsc-dataseed.binance.org and falling back through endpoints on defibit.io, ninicoin.io, publicnode.com, nodereal.io, and extrnode.com. All endpoint URLs are base64-encoded in the source.
Step 2: Decrypt the configuration. The smart contract's return value is decrypted using AES-256-CBC with key 971d7e9e9f4a07eaeafeb890cff6a4ad (SHA256-hashed). The initialization vector is the first 16 bytes of the ciphertext. The decrypted result is a Cloudflare Worker URL.
Step 3: Fetch the JavaScript. The malware requests the Worker URL's /c endpoint with a spoofed User-Agent. The response is another AES-encrypted blob containing the actual JavaScript payload.
Step 4: Inject. The decrypted JavaScript is injected into every frontend page via the wp_footer hook, wrapped in a <script> tag with an ID matching wp-<6hex>-perf.
This technique is a variant of EtherHiding, documented in the UNC5142 campaign that compromised approximately 14,000 WordPress sites. The key difference: the known EtherHiding campaigns use the blockchain to deliver fake browser update pages via ClickFix social engineering. This sample uses the same infrastructure for a different purpose: injecting encrypted JavaScript directly into the page footer for regular visitors.
Why the blockchain: Smart contracts can't be taken down. If the Cloudflare Worker URL is blocked, the attacker updates the contract to point to a new URL for a fraction of a cent. The malware automatically picks up the change on its next query. Traditional C2 takedowns target domains and IP addresses, but a public blockchain endpoint like bsc-dataseed.binance.org can't be blocked without breaking legitimate cryptocurrency operations.
Evasion in the injection: The JavaScript is only served to visitors who meet specific criteria. It skips admin pages, AJAX requests, cron executions, and REST API calls. It skips bots and crawlers (Googlebot, Bingbot, Yandex, Ahrefs, SEMrush). It skips logged-in administrators and editors. It skips non-HTML requests and static file URLs. It disables page caching (DONOTCACHEPAGE) and sends no-cache headers to prevent CDNs or caching plugins from storing a copy of the page with the injected script.
The JavaScript payload itself is cached in a WordPress transient with a five-minute TTL, reducing the number of outbound blockchain queries.
Indicators of Compromise
Sample SHA256: 1eea58480fa71dbb68901caf26aabd0e97e54f142229fa0cd015e3fad48f569e
Database
| Location | What to look for |
|---|---|
| wp_users | User matching usr_[a-f0-9]{8} with administrator role |
| wp_usermeta | Key: _wp_ui_render_cfg (backdoor user marker) |
| wp_usermeta | Key: _wp_cache_hash (older variant marker) |
| wp_usermeta | Key: _wp_role_backup with value subscriber (demoted foreign backdoors) |
| wp_options | Key: _wco_v with value 3 |
| wp_options | Key: _wco_hide_slugs (array of hidden plugin slugs) |
| wp_options | active_plugins containing the malware's slug |
| wp_options (transients) | _wco_prov, _wco_ka, _wco_sec_n, _wco_light_sec |
| wp_options (transients) | _wco_task_1, _wco_task_3, _wco_task_4 |
| wp_options (transients) | _wco3_js_code, _wco3_config |
Filesystem
| Path | Description |
|---|---|
| wp-content/plugins/wp-content-optimizer/ | Main malware directory |
| wp-content/mu-plugins/wp-[a-f0-9]{6}-loader.php | Persistence dropper |
| wp-content/mu-plugins/00-site-cache.php | Older variant dropper |
| wp-content/upgrade/wp-maintenance.tmp | Backup copy of the malware |
| Missing: security plugin directories | Wordfence, iThemes, Cerber, etc. recursively deleted |
| Missing: wflogs/, wfcache/, nfwlog/, cerber/ | WAF data directories deleted |
| Missing: wordfence*.php in document root | WAF bootstrap files deleted |
| .htaccess | Security rules stripped |
| .user.ini | auto_prepend_file directive removed |
Network
| Target | Purpose |
|---|---|
| bsc-dataseed.binance.org | BSC RPC, smart contract query |
| bsc-dataseed[1-4].binance.org | BSC RPC fallbacks |
| bsc-dataseed1.defibit.io | BSC RPC fallback |
| bsc-dataseed1.ninicoin.io | BSC RPC fallback |
| bsc.publicnode.com | BSC RPC fallback |
| bsc-mainnet.nodereal.io/v1 | BSC RPC fallback |
| bsc-mainnet.rpc.extrnode.com | BSC RPC fallback |
| Cloudflare Worker URL (dynamic) | JS payload delivery (/c endpoint) |
Frontend
| Indicator | Description |
|---|---|
| <script id="wp-[a-f0-9]{6}-perf"> | Injected malicious JS in page footer |
| nocache headers on all HTML pages | Prevents caching of injected content |
Cryptographic Material
| Item | Value |
|---|---|
| Master key (credential derivation) | 7f3ac891d4e60b52a9f7186db345ce90 |
| BSC contract address | 0x8170119B70b4E8c65Ef821425c00c35CDA15d9b0 |
| BSC contract method | 0x3bc5de30 |
| AES key (config decryption) | 971d7e9e9f4a07eaeafeb890cff6a4ad |
If You're Compromised: Cleanup Checklist
All steps should be performed together. The malware's persistence layers are designed so that missing any single step results in full re-infection on the next page load.
- Delete the plugin directory: wp-content/plugins/wp-content-optimizer/
- Delete the backup: wp-content/upgrade/wp-maintenance.tmp
- Delete the mu-plugins dropper: any file in wp-content/mu-plugins/ matching wp-[a-f0-9]{6}-loader.php or 00-site-cache.php
- Remove the backdoor user: delete any user from wp_users matching usr_[a-f0-9]{8} with the _wp_ui_render_cfg meta key
- Clean the database: delete all options and transients with the _wco_ prefix from wp_options
- Restore security plugins: if Wordfence, iThemes, or other security tools were installed, reinstall them. Check that WAF files and cron jobs are restored.
- Restore .htaccess and .user.ini to a known-good state. Verify auto_prepend_file directives if your WAF requires them.
- Audit all admin users: check for unknown accounts, suspicious meta keys (_wp_cache_hash, _wps_sig, _sys_token, _bk_hash, _adm_key), and accounts using disposable email domains
- Rotate all admin passwords
- Check your theme's functions.php for appended code. The malware cleans injections by other attackers, but the original infection vector may have left traces.
- Check the BSC contract for the current Worker URL and block it at the firewall level. The contract address and method are listed in the IoC table above.
How Imunify360 Detects and Blocks This Threat
The analysis above showed 14+ WordPress security plugins targeted for deletion. What they have in common: they all run inside WordPress, as PHP code in the wp-content/plugins/ directory. That's what makes them reachable via the raw unlink() and rmdir() calls the malware uses.
Notice what the malware doesn't target: server-level security. It strips auto_prepend_file from .user.ini (disabling WAF bootstraps that load as PHP files), but it makes no attempt to interfere with security that operates as a PHP module at the engine level or as a system daemon because the web server user running PHP code cannot stop a system process owned by root.
Imunify360 sits on the other side of that boundary. Proactive Defense, Imunify360's real-time PHP security module, monitors PHP script execution at the engine level, outside of WordPress. When it identifies a malicious script, it kills the execution entirely. For the malware documented in this article, that means the full attack chain is stopped at once: security plugin deletion, hidden admin creation, WAF stripping, and payload injection never execute. The malware scanner runs as a separate system daemon, with its own database and process space. Neither component is reachable through the filesystem and database operations this malware uses.
Imunify360 also detects this sample and its variants through behavioral signatures, not just file hashes, which means new variants using the same techniques are caught even before their specific hash is catalogued.
The broader point: when the first thing malware does is delete your security plugins, defenses that run as WordPress plugins face a structural disadvantage. Multilayer protection, with defenses at the server, PHP runtime, and application levels, is the architecture that can handle this class of threat.

6 Layers of Protection




.png?width=115&height=115&name=pci-dss%20(1).png)
