SC WordPress Malware: A Self-Healing Mesh of Loaders, Drop-Ins, and a Blockchain-Controlled Backdoor
OverviewDuring recent website cleanup work, we analyzed a WordPress compromise where the same backdo 2026-10-1 03:3:23 Author: blog.sucuri.net(查看原文) 阅读量:7 收藏

Overview

During recent website cleanup work, we analyzed a WordPress compromise where the same backdoor kept returning within seconds of every removal, no matter how carefully the visible files were deleted. Throughout this article, we’ll refer to this family of malware as SC, named after the “SC_” markers found in the injected content.

What makes SC worth documenting is how it survives. The payload lives in at least eight places at once, spread across files, the database, and shared memory, and every one of those places can rebuild all the others. Delete the plugin and a drop-in rewrites it. Delete the drop-in and the theme rewrites it. Clean every file on disk, and the next page load restores the whole set from the database or from a shared-memory segment. The result is a circular system with no single point you can remove to stop it.

We’ll walk through each component of the infection, how they regenerate each other, what the core backdoor actually does, and the order of operations that a real cleanup has to follow.

The Components We Recovered

Every SC file we examined shares the same obfuscation scheme. There is no eval, no marker on the newest pieces, and no readable function names. Each file carries a table of scrambled strings and a small decoder that resolves a numeric index into a real function name through a positional substitution cipher. See the example below:

Here is what each component does:

  1. The .user.ini trigger. A single line, auto_prepend_file, points PHP at a loader that runs before every request in that directory tree. This fires even on requests that never reach WordPress. The value is cached by PHP. That cache is why deleting the prepend target carelessly is dangerous. If the file it points at is removed while the cache is still warm, every PHP request on the account fails until the cache expires.
  2. The visible shim. The .user.ini points at a plainly named PHP file, and that file does one thing: if a hidden dot-prefixed file exists next to it, it includes it. The shim keeps the .user.ini value stable and innocuous while the real logic lives in the hidden file. If a defender removes only the hidden file, the shim silently does nothing and the site stays up, which buys the attacker time while another component recreates the hidden file. In this example, the file was named c1b12371.php, but the name varies from site to site. It is commonly located in the wp-content directory.
  3. The string-table loader. The hidden dot-prefixed file is the first real loader. It locates the fake plugin and rebuilds it in mu-plugins from three sources tried in order: an existing copy in the plugins folder, an encoded stub in the cache directory, and a ZIP restore bundle with a random hex name. It writes through a temporary file, sets mode 0644, and calls opcache_invalidate so the freshly written file executes immediately. It also defines a constant that ties it to the auto_prepend side of the family—for this example, named .c1b12371.php, also located in the wp-content directory.
  4. The db.php drop-in. WordPress loads wp-content/db.php very early during bootstrap. This copy carries the entire backdoor payload inside itself as a gzip plus base64 blob. On any request where the fake plugin is missing or too small, it decodes the blob and writes the plugin straight back to disk.
  5. The advanced-cache.php drop-in. This is the earliest and most dangerous position of all. When caching is enabled, WordPress loads advanced-cache.php before ordinary plugins. Unlike the others, it does not carry the payload inline. It is a finder that can rebuild the plugin from five independent sources in order: an existing mu-plugin, an existing plugin copy, a System V shared-memory segment holding PHP, a ZIP bundle searched for across several folders, and finally the database itself. For the database path, it opens a direct connection using the site’s own credential constants and reads the payload out of a specific option row, decoding the same gzip format. Once it has a copy, it hooks plugins_loaded and includes it.
  6. The theme injection. The active theme’s functions.php runs on nearly every request. The attacker appended a bounded block to the bottom of an otherwise legitimate theme, fenced by begin and end markers. That block is the theme-resident twin of the db.php drop-in: it rewrites the plugin whenever it goes missing.
  7. and 8. The backdoor payload, in two places. The actual malware is a large WordPress-aware file installed as both a must-use plugin and a normal plugin. The two copies are identical. The plugin folder even ships a convincing fake settings page, a shortcode, and an activation hook in plain sight, so a quick look suggests a real caching plugin. The must-use copy loads automatically and invisibly. Keeping two copies is deliberate redundancy, and the payload competitively updates one from the other so both stay on the same version.

Summary Of The Key Components

#Path / LocationTypeWhat It DoesLoads / Triggers Via
1.user.iniConfigSets auto_prepend_file to run #2 before every PHP request in the tree. Cached by PHP.PHP-FPM, every request
2wp-content/c1b12371.php *ShimIncludes the hidden #3 file if it exists. Silently no-ops if removed.The .user.ini prepend
3wp-content/.c1b12371.php *String-table loaderRebuilds the mu-plugin and defines SC_AUTO_PREPEND.The shim (#2)
4wp-content/db.phpBootstrap drop-inCarries the full backdoor as a gzip+base64 blob. Decodes and re-deploys the plugin whenever it’s missing or too small.WordPress bootstrap (loads early)
5wp-content/advanced-cache.phpEarliest drop-inRebuilds the plugin, hooks plugins_loaded, and includes it.WordPress bootstrap when WP_CACHE is set
6wp-content/themes/khorshidi/functions.phpInjected blockTheme-resident twin of db.php that carries the same payload and re-drops the plugin.Active theme, every request
7wp-content/mu-plugins/hyper-engine-kit.php *Backdoor payloadSelf-hiding payload with multiple persistence and remote-control capabilities.MU-plugin autoload, every request
8wp-content/plugins/hyper-engine-kit/hyper-engine-kit.php *Copy of #7Duplicate copy of the same payload used for redundancy.Plugin autoload

* File names vary from site to site.

What the Backdoor Actually Does

The payload is the reason the rest of the mesh exists. Once running, it performs several distinct jobs.

It hides itself. It filters the plugin list, the update transient, and the site and network plugin views to remove its own entry, so it does not appear in the admin plugins screen or in update checks. It also emits admin JavaScript to scrub itself from the plugin table as a fallback.

It talks to command and control over blockchain infrastructure. Rather than a single hardcoded server, the payload carries a list of roughly twenty public Ethereum RPC gateways and a set of smart-contract method selectors. It sends requests to those gateways to read instructions from a smart contract. Because these legitimate third-party gateways are being abused as transport, blocking only the one seen in traffic leaves the rest available as fallbacks, so the whole set must be blocked together.

It fingerprints the site and fetches a payload. It collects the site URL and host, the WordPress and plugin versions, path hashes, the active themes, the mu-plugin list, and the current administrator session tokens, encrypts that bundle, and posts it to the resolved endpoint. The reply can carry front-end JavaScript to inject (which, on a store, enables checkout skimming), new PHP to install, and lists of security plugins to deactivate and delete. When told to remove a security plugin, it can deactivate it, wipe its directory, and even reassign or elevate another account first.

It creates a hidden administrator. It either adopts an existing hidden admin or generates a new one, writing the account directly into the users and usermeta tables when the normal API is unavailable. It stores the capabilities under the default capabilities meta key, then hides the account from the user list, user counts, and role views through query and count filters. It also forges valid authentication cookies for that account, so the operator can log in without a password.

It exposes a live beacon. A magic request parameter in the form of a short prefix followed by a fixed value is handled before WordPress finishes loading. While the mesh is present, that request returns a normal response with content and then exits.

It rebuilds the entire mesh. This is the part that explains the reinfection loop. The payload hooks a large set of actions and filters whose only purpose is to write every other component back out. It regenerates the db.php and advanced-cache.php drop-ins, the theme injection, the hidden loader and its .htaccess and .user.ini prepend directives, the ZIP restore bundle, the shared-memory segment, and the database copy of the payload. It even opens a raw database connection to insert its payload option directly, bypassing WordPress.

Where It Stores Persistence Off Disk

The single most important lesson from this case is that persistence isn’t limited to files. We recovered live copies of the payload in three non-file locations, any one of which will rebuild the whole set on the next request:

  • The database. The full payload sits in an options row under a random name, stored in the same gzip and base64 format. The advanced-cache.php drop-in reads it back through a direct database connection. This is why a complete and correct file cleanup fails: the very next page load restores everything from the row.
  • Shared memory. On servers that support System V shared memory, the payload is written into a segment identified by a fixed numeric key. That segment lives in RAM, so it survives file deletion and database cleanup alike, and on shared hosting it can even be owned by a different account.
  • Scheduled tasks. The infection registers cron hooks, including randomized names alongside a known fetch hook. System cron runs the WordPress cron file, not visitor traffic, then triggers redeployment on schedule.

Alongside these, we saw database triggers used by related SC variants that recreate an administrator on insert. A trigger runs inside the database, so it survives a full file restore and even the deletion of the account it keeps recreating, which makes cleaning users pointless until the trigger is gone.

Indicators of Compromise

  • Unexpected PHP blocks inside wp-content/db.php or wp-content/advanced-cache.php, each opening with an SC_-style begin marker and a guard constant.
  • A bounded block at the bottom of the active theme’s functions.php, fenced by begin and end markers, sometimes with the begin marker duplicated.
  • A .user.ini, php.ini, or .htaccess carrying an auto_prepend_file directive that points at a hidden dot-prefixed PHP file.
  • A fake plugin present in both wp-content/mu-plugins and wp-content/plugins, with matching contents and a plausible settings page.
  • A ZIP restore bundle with a random hex name in wp-content, in wp-content/uploads, or in a theme folder.
  • An options row with a random name holding a large gzip and base64 blob, plus control options and transients using an sc_ style prefix.
  • A shared-memory segment holding readable PHP that starts with an opening tag.
  • A fully privileged administrator that might not appear in the user list, with capabilities stored under the default capabilities meta key, and sometimes an orphaned option pointing at that account’s ID left behind after a partial cleanup.
  • Outbound requests to public Ethereum RPC gateways from the web server.

How to Clean It Up

Because every component can rebuild the others, the order of operations matters more than the individual deletions. Removing files first, in the wrong order, simply triggers a rewrite. The approach that works is to cut off execution, clear the off-disk copies before the on-disk ones, and only then remove the files, all in a single pass.

  1. Neutralize the prepend before deleting its target. Because the auto_prepend_file value is cached by PHP for up to 300 seconds, empty the prepend target to an inert stub first, then strip the directive from the .user.ini, php.ini, and .htaccess. Deleting the target while the cache is still warm takes down every PHP request on the account.
  2. Clear the off-disk payload copies. Remove the payload row from the options table, purge the shared-memory segment, and delete the control options and transients. If any of these remain, the next request through advanced-cache.php will rewrite the whole set back to disk. On shared hosting, a shared-memory segment may be owned by another account, in which case only that account or the host can remove it, and it becomes harmless once the drop-ins that read it are gone.
  3. Remove the scheduled tasks and any database triggers. Clear the malicious cron hooks so that system cron cannot redeploy on schedule, and audit information_schema.TRIGGERS for any trigger that recreates an administrator on insert. A surviving trigger will rebuild access even after a full file restore.
  4. Remove the hidden administrator. Delete the account whose capabilities are stored under the default capabilities meta key, and clean up the orphaned option that points to its ID.
  5. Clean the files in one pass. Remove the standalone loaders and the shim, delete both copies of the fake plugin from the mu-plugins and plugins folders, delete the ZIP restore bundle wherever it was hidden, and remove the injected drop-ins. For db.php and advanced-cache.php, this means deleting the malicious files. For the theme’s functions.php, it means trimming only the bounded block between the begin and end markers so the legitimate theme code stays intact.
  6. Scan again and watch for recreated files. Run a complete scan, then monitor the affected paths. A returning component means a persistence point survived or the original entry path is still open.

Prevention

  • Keep everything patched. Most compromises we handle exploit known, already-fixed vulnerabilities in outdated components. Prompt updates shorten the window an attacker has to work with.
  • Use a Web Application Firewall. A firewall can block exploit attempts before they reach the application, catch the beacon parameter, and help stop the outbound requests to the command channel.
  • Audit the places this family hides. Review the options table, scheduled tasks, database triggers, and user accounts on a schedule. These are exactly where SC keeps the persistence that lets it come back.
  • Treat any reappearance as unfinished cleanup. If a file returns, a copy of the payload still lives somewhere off disk or a scheduled task is still armed. Go back to the off-disk sources rather than deleting the same file again.

Conclusion

SC is a reminder that a modern WordPress infection can be a system rather than a file. This toolkit spreads identical copies of one backdoor across drop-ins, the theme, a fake plugin in two locations, the database, and shared memory, hides its command channel inside legitimate blockchain infrastructure, and rewrites itself from any surviving copy on the very next request.

A durable fix treats the files, the database, and shared memory as one problem, disables execution before deletion, removes the persistence in every form it takes, closes the original entry point, and rotates every credential the attacker could have touched. Anything less invites the mesh to rebuild itself.

If your WordPress site keeps reinfecting after you remove the obvious malware, or you find unfamiliar code on your website, our analysts can help with a full audit, cleanup, and ongoing monitoring.


Chat with Sucuri


文章来源: https://blog.sucuri.net/2026/09/sc-wordpress-malware-a-self-healing-mesh-of-loaders-drop-ins-and-a-blockchain-controlled-backdoor.html
如有侵权请联系:admin#unsafe.sh