Недавно сообщество 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)
Если вы еще не сделали стандартные шаги, выполните их в первую очередь:
Обновите ядро WP до пропатченных версий (6.9.6 / 7.0.3+).
Перегенерируйте соли (Salt Keys) в
wp-config.phpэто мгновенно сбросит все активные сессии.Запретите редактирование и установку плагинов из админки в
wp-config.php:define( 'DISALLOW_FILE_EDIT', true ); define( 'DISALLOW_FILE_MODS', true );Удалить подозрительные php-файлы в каталогах
/wp-content/plugins/,/wp-content/mu-plugins/,/wp-content/cache/. В моём случае ещё был/wp-content/object-cache.php.Очистите специфичный мусор в БД, созданный эксплоитом (записи с датой
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/).
Проверьте запущенные процессы и завершите их:
ps aux | grep -E "cache-mgr|miner" pkill -9 -f cache-mgrДа в моём случае пытались запустить майнер, но для него не хватило места на диске, так как предварительно делали бэкап всех файлов и базы.
Проверьте директорию
~/.cache/на наличие скрытых файлов (начинаются с точки):ls -la ~/.cache/Удалите вредоносные бинарники и скрипты:
В моём случае были файлы:.so, .php-fpm.so, .cache-mgr.shrm -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; }

