Недавно сообщество WordPress столкнулось с массовой волной взломов через уязвимость в REST Batch API (также известную как wp2shell). Вектор атаки позволяет злоумышленникам удаленно выполнять произвольный код (RCE) без авторизации.

В сети уже появились десятки стандартных гайдов: "Обновите ядро до 6.9.5/7.0.2, удалите подозрительные файлы из /wp-content/uploads/, поменяйте соли в wp-config.php и очистите базу через SQL".

Спойлер: В 80% случаев на реальных VPS/VDS этого недостаточно.


При обработке инцидента на сервере клиента мы обнаружили, что атакующие не ограничились папкой сайта (public_html / webroot). Получив RCE, боты моментально начали осваиваться в директории системного пользователя Linux, прописали автозапуск бинарников, вшили SSH-ключи и заблокировали файлы от удаления через chattr.

Ниже - практическое руководство по тому, как провести полноценное расследование, вычистить следы на уровне ОС и закрыть админку намертво.

1. Базовый минимуму: Что сделать в самом WordPress (Short Recap)

Если вы еще не сделали стандартные шаги, выполните их в первую очередь:

  1. Обновите ядро WP до пропатченных версий (6.9.6 / 7.0.3+).

  2. Перегенерируйте соли (Salt Keys) в wp-config.php это мгновенно сбросит все активные сессии.

  3. Запретите редактирование и установку плагинов из админки в wp-config.php:

    define( 'DISALLOW_FILE_EDIT', true );
    define( 'DISALLOW_FILE_MODS', true );
  4. Удалить подозрительные php-файлы в каталогах /wp-content/plugins/, /wp-content/mu-plugins/, /wp-content/cache/. В моём случае ещё был /wp-content/object-cache.php.

  5. Очистите специфичный мусор в БД, созданный эксплоитом (записи с датой 2020-01-01 00:00:00 и домен example.invalid):

    DELETE FROM wp_posts WHERE post_type IN ('customize_changeset', 'nav_menu_item') AND post_date = '2020-01-01 00:00:00';
    DELETE FROM wp_postmeta WHERE meta_value LIKE '%example.invalid%';

На этом стандартные инструкции заканчиваются. Но если остановиться здесь, через пару дней сайт взломают снова, потому что процессы и закладки уже живут в системе.

2. Побег из webroot: Ищем закрепление на уровне Linux (OS Persistence)

Когда эксплоит выполняет код, он работает от имени системного пользователя (например, www-data или пользователя панели управления вроде username). Все последующие действия происходят в /home/username/. В моём случае действия происходили под панелью управления Hestia.

Шаг 2.1: Проверка SSH-ключей (authorized_keys)

Первое, что делает скрипт - прописывает свой публичный SSH-ключ, чтобы хакер мог заходить на сервер в обход веб-сервера и любых плагинов.

cat ~/.ssh/authorized_keys

Если нет бэкапа, чтобы сравнить, то смотрите дату модификации файла. Если она очень близко к дате взлома, лучше сгенерить новые ключи.

Шаг 2.2: Автозапуск в профиле shell (.profile / .bashrc)

Атакующие вшивают команды автозапуска в файлы окружения пользователя. При любом переподключении или запуске процессов под этим пользователем поднимается фоновый демон.

Проверьте файлы ~/.profile, ~/.bashrc и ~/.bash_profile. В нашем случае в самом конце ~/.profile была обнаружена строка:

nohup /home/username/.cache/.cache-mgr.sh > /dev/null 2>&1 &

Удалите вредоносные строки из конфигурационных файлов shell.

Шаг 2.3: Очистка процессов и скрытых бинарников в ~/.cache

Атакующие часто прячут скомпилированные .so библиотеки, шеллы и майнеры в скрытые папки домашнего каталога (например, ~/.cache/).

  1. Проверьте запущенные процессы и завершите их:

    ps aux | grep -E "cache-mgr|miner"
    pkill -9 -f cache-mgr

    Да в моём случае пытались запустить майнер, но для него не хватило места на диске, так как предварительно делали бэкап всех файлов и базы.

  2. Проверьте директорию ~/.cache/ на наличие скрытых файлов (начинаются с точки):

    ls -la ~/.cache/
  3. Удалите вредоносные бинарники и скрипты:
    В моём случае были файлы: .so, .php-fpm.so, .cache-mgr.sh

    rm -f ~/.cache/.so ~/.cache/.php-fpm.so ~/.cache/.cache-mgr.sh

Шаг 2.4: Снятие атрибута неизменяемости (chattr +i)

Возможно для усложнения восстановления, был поврежден Файл Менеджер в Hestia. Если вы пытаетесь удалить файл или папку под root, а система выдает Operation not permitted - атакующие установили атрибут +i (immutable).

# Найти все файлы с атрибутом +i в папке сайта
lsattr -R /home/username/public_html | grep "\----i"

# Снять блокировку с файла или директории
chattr -i /path/to/locked/file

Шаг 2.5: Поиск всех файлов, измененных в период активности атакующих (Date Range Find)

Когда атакующие получают доступ, они не только загружают новые файлы, но и модифицируют существующие (например, вшивают реверс-шеллы в index.php и functions.php файлы активной темы или плагинов). Зная примерные даты атаки (от даты создания первого фейкового админа до дня проведения зачистки), обязательно пройдитесь поиском по всем файлам, измененным в этом временном окне:

# Ищем файлы, измененные между 18 и 26 июля (замените даты и путь на свои) 
find /home/username/public_html -type f -newermt "2026-07-18" ! -newermt "2026-07-26" 

Шаг 2.6: Проверка системных временных каталогов (/tmp/, /var/tmp/, /dev/shm/)

Поскольку каталоги /tmp, /var/tmp и /dev/shm имеют глобальные права на запись для всех пользователей, RCE-скрипты первой делом сбрасывают туда временные шеллы, бинарники майнеров, скомпилированные библиотеки .so и обертки для запуска.

Проверьте содержимое временных папок:

ls -la /tmp /var/tmp /dev/shm
  • На что обратить внимание:

    • Скрытые файлы и каталоги (начинающиеся с точки, например .ice-unix-bak, .cache-mgr).

    • Файлы с расширениями .sh, .so, .php, .py, .pl.

    • Бинарные исполняемые файлы с рандомными именами (например, x86_64_mgr).

3. Глубокая зачистка базы данных (Прямой SQL-аудит)

Не надейтесь на интерфейс /wp-admin/ - скрытые администраторы могут быть сгенерированы с поврежденными метаданными или маскироваться под системные записи. В моём случае было добавлено 30+ скрытых админов.

Запустите прямые SQL-запросы в базы данных (через Adminer / PHPMyAdmin / MySQL CLI):

-- 1. Вывести всех пользователей с правами administrator
SELECT u.ID, u.user_login, u.user_email, u.user_registered 
FROM wp_users u 
JOIN wp_usermeta m ON u.ID = m.user_id 
WHERE m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%administrator%';

-- 2. Удалить фейковых админов (wp2shell, wpsvc_, w2s_...)
DELETE FROM wp_users 
WHERE user_email LIKE '%@wp2shell%' 
   OR user_email LIKE '%@wordpress-svc.internal%'
   OR user_login LIKE 'w2s_%';

-- 3. Очистить осиротевшие метаданные пользователей
DELETE FROM wp_usermeta WHERE user_id NOT IN (SELECT ID FROM wp_users);

4. Защита на уровне Nginx: Переходим в Stealth Mode

Плагины вроде Change wp-admin URL больше не спасают: боты находят кастомные адреса за секунды через эндпоинты REST API (/wp-json/).

Защищать сайт желательно до того, как запрос попадет в Wordpress. Так как уязвимости находят постоянно, только 3 дня назад вышла версия 7.0.3 c 12 закрытыми уязвимостями.

1. Запрет исполнения PHP в папке uploads

Даже если через уязвимость нулевого дня на диск залетит .php файл, Nginx откажется его выполнять.

location ~* ^/wp-content/uploads/.*\.php$ {
    deny all;
    return 444;
}

2. Stealth Mode (return 444) вместо 403 / 404

Я предпочитаю отдавать ботам ошибку 444, но вы можете настроить 403 Forbidden или 404 Not Found. Код 444 в Nginx мгновенно закрывает TCP-соединение без отправки HTTP-заголовков, т.е. бот оказывается в полной "непонятке", что случилось.

# Полная блокировка xmlrpc без откликов
location = /xmlrpc.php {
    return 444;
}

3. Изоляция /wp-admin/ и /wp-login.php на уровне шлюза

Идеальная схема защиты - полностью закрыть доступ к критическим путям для внешнего мира на уровне Nginx (return 444;), пропустив авторизацию исключительно через отдельный IP-шлюз/прокси/VPN.

Я использовал собственную разработку, в которой при заходе на защищенный сайт расширение для браузера отправляет юзера через правильный прокси. И получается у всей команды, даже если она работает удаленно - один IP. И нет необходимости весь трафик через VPN гонять. Вы же можете использовать любой удобный для Вас способ.

Ниже фрагмент NGINX конфига, блокирующий доступ к опасным путям и разрешающий заход в админку только с одного IP.

Скрытый текст
# ----------------------------------------------------------------------
# 1. IP-ФИЛЬТР ДЛЯ ПРОКСИ
# ----------------------------------------------------------------------
# Проверяем, идет ли запрос от вашего форвард-прокси
set $is_admin_proxy 0;
if ($remote_addr = "99.88.77.66") { # !!! ЗАМЕНИТЕ НА IP ВАШЕГО ПРОКСИ/VPN !!!
    set $is_admin_proxy 1;
}

# ----------------------------------------------------------------------
# 2. ПОЛНАЯ БЛОКИРОВКА ?rest_route= И XML-RPC
# ----------------------------------------------------------------------
# Блокируем параметр ?rest_route= для всех (код 444 - моментальный сброс)
if ($arg_rest_route) {
    return 444;
}

# ----------------------------------------------------------------------
# 3. ОГРАНИЧЕНИЕ ДОСТУПА К REST API (/wp-json/) И АДМИНКЕ
# ----------------------------------------------------------------------
# Блокировка /wp-json/ для всех, кроме вашего прокси
location /wp-json/ {
    if ($is_admin_proxy = 0) {
        return 444;
    }
    try_files $uri $uri/ /index.php?$args;
}

# Блокировка wp-admin и wp-login.php
location ~ ^/(wp-admin|wp-login\.php) {
    if ($is_admin_proxy = 0) {
        return 444;
    }
    index index.php;
    try_files $uri $uri/ /wp-admin/index.php?$args;
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_pass unix:/run/php/php8.2-fpm-finmonitoring.in.ua.sock;
    }

}

# Блокировка кастомного входа (если используется, например /custom-login-wp-2026)
location ~ ^/custom-login-wp-2026 {
    if ($is_admin_proxy = 0) {
        return 444;
    }
    try_files $uri $uri/ /index.php?$args;
}

# Блокировка phpMyAdmin для всех, кроме прокси
location /phpMyAdmin {
    if ($is_admin_proxy = 0) {
        return 444;
    }
    include /etc/nginx/conf.d/phpmyadmin.inc*;
}

# ----------------------------------------------------------------------
# 4. ДРОП СКАНИРОВАНИЯ И СЛУЖЕБНЫХ ФАЙЛОВ
# ----------------------------------------------------------------------
location = /xmlrpc.php {
    return 444;
}

location ~* /(wlwmanifest\.xml|readme\.html|license\.txt) {
    return 444;
}
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы защищаете админку и критические эндпоинты WordPress на своих проектах?
0%Закрываю на уровне веб-сервера / файрвола0
0%Использую плагины безопасности0
0%Ограничиваюсь надежным паролем, 2FA и своевременными обновлениями0
0%Надеюсь на бэкапы и везение0
0%Не использую WordPress0
Никто еще не голосовал. Воздержались 2 пользователя.