Короткий ответ

Если разбор показал, что вход был через плагин на /var/www/site.example: уберите плагин с диска (не только Deactivate), проверьте ядро и uploads, смените все секреты, которые мог прочитать PHP (БД, соли wp-config.php, SMTP, API, SSH деплоя). Хост site.example, 10.0.20.40. Полный malware-runbook: заражение. Здесь — фокус на плагине как векторе, без разбора эксплойта.

Не называйте CVE, если не подтвердили версию пакетом/readme. Не оставляйте plugin-name.disabled внутри plugins/ если PHP всё ещё может include через другой файл.

Deactivate в админке недостаточен: PHP плагина вызывают прямым URL. Каталог должен исчезнуть с диска после tar-копии. Секреты БД и соли wp-config.php после RCE считайте скомпрометированными — ротируйте до возврата сайта в интернет. Прямой вызов wp-content/plugins/.../file.php в access.log после удаления должен давать 404, не 200.

Симптомы и как отличить

  • В логах POST на wp-admin/admin-ajax.php?action=... или прямой PHP плагина до появления шелла.
  • wp plugin list — плагин не из wordpress.org / nulled.
  • Тема цела, mu-plugins пуст, а в каталоге одного плагина лишние .php.

Отличие от brute login: в auth.log WP / audit мало failed login, зато странные hit в plugin URL. Brute: rate limit.

Возможные причины

  1. Плагин не обновлялся годы.
  2. Nulled/«взломанная» премиум-копия с бэкдором из коробки.
  3. Upload-форма плагина без авторизации.
  4. Демо-данные плагина с PHP в uploads.
  5. Цепочка: слабый admin + установка произвольного плагина.

Диагностика

Копия сначала — как в соседней статье, тот же tar/sql.

sudo -u www-data wp --path=/var/www/site.example plugin list --fields=name,status,version
ls -la /var/www/site.example/wp-content/plugins
sudo find /var/www/site.example/wp-content/plugins -name '*.php' -mtime -7

Access log:

sudo awk '$7 ~ /wp-content\/plugins/ {print}' /var/log/nginx/access.log | awk '{print $1,$7}' | sort | uniq -c | sort -nr | head

readme.txt плагина vs заявленная версия. Сверяйте установленную версию, не анонс вендора.

Проверьте wp-config.php на auto_prepend и неизвестные константы. Не вставляйте содержимое ключей в чат/тикет — только факт «ротировано».

Решение

Сценарий A. Плагин не нужен бизнесу

sudo tar -C /var/www/site.example/wp-content -czf /root/plugins-before-rm-$(date +%F).tar.gz plugins
sudo -u www-data wp --path=/var/www/site.example plugin deactivate offending-plugin
sudo rm -rf /var/www/site.example/wp-content/plugins/offending-plugin

Проверьте, не осталось ли offending-plugin.php в корне plugins/ (drop-in).

Сценарий B. Плагин нужен

Снимите с диска, поставьте чистый zip той же или новой версии с официального канала, не из «папки с бэкапа заражённого сервера». Прогоните checksum, если WP.org:

sudo -u www-data wp --path=/var/www/site.example plugin install offending-plugin --force
sudo -u www-data wp --path=/var/www/site.example plugin verify-checksums offending-plugin

Имя подставьте реальное. Премиум без checksum — сверка с вендорским архивом по SHA256.

Сценарий C. Ротация секретов (обязательна после RCE)

  1. Пароль MySQL пользователя сайта, обновить wp-config.php.
  2. Новые AUTH_KEY / SECURE_AUTH_KEY / salts — генерация с wordpress.org salt API с машины админа, вставка в конфиг.
  3. Пароли всех администраторов WP, сброс сессий (wp user session destroy).
  4. FTP/SFTP, ключи CI, токены WooCommerce/платежей, SMTP.
  5. Ключи в wp-options (wp option list ищите *_key, *secret* — осторожно с выводом).

После смены пароля БД — проверьте, что FPM читает новый конфиг (reload не обязателен для PHP, каждый worker читает файл запроса, но opcache мог закэшировать редко — reload FPM безвреден).

sudo systemctl reload php8.3-fpm

Сценарий D. Запрет исполнения в uploads

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

nginx -t && reload. Apache: php_admin_flag engine off в Directory uploads или Require all denied для php.

Сценарий E. Nulled-плагин

Считайте бэкдор встроенным. Удалите, купите лицензию или замените функциональность. Ротация как после RCE.

Сценарий F. Следы в базе после удаления плагина

Плагин мог записать в wp_options объект с PHP-сериализацией или JS-редиректом. После снятия каталога:

sudo -u www-data wp --path=/var/www/site.example db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%eval(%' OR option_value LIKE '%base64%' LIMIT 20;"

Префикс таблиц подставьте свой. Ложные срабатывания на нормальный base64-контент возможны — смотрите глазами, не DELETE пачкой. Проверьте wp_posts на редирект-вставки в post_content (часто footer скрипт). wp post list не заменит grep по дампу.

Файл wp-content/object-cache.php и advanced-cache.php живут вне списка plugin list. Сверьте с дистрибутивом drop-in Redis/Memcached. Лишний include — удалить после tar.

Ротация: даже «только плагин» = RCE = чтение wp-config.php. Пароль БД, соли, токены Woo, SSH. Иначе реинфекция через день тем же входом в админку.

Как проверить, что проблема устранена

sudo -u www-data wp --path=/var/www/site.example plugin list
sudo -u www-data wp --path=/var/www/site.example core verify-checksums
sudo find /var/www/site.example/wp-content/uploads -name '*.php'
sudo -u www-data wp --path=/var/www/site.example user list --role=administrator

Нет уязвимого каталога, нет PHP в uploads, админы известны, логин с новым паролем работает, старый — нет. Мониторьте access.log сутки на старые URI плагина (должны быть 404).

Если не помогло

  • Реинфекция: второй плагин или тема, или запись в БД (eval в option). Вернитесь к полной чистке.
  • Drop-in object-cache.php / advanced-cache.php заражён — это не «плагин в списке».
  • Компрометация хоста вне WP (другой vhost) — секреты всё равно ротировать.

Профилактика

  • Минимум плагинов, автообновление minor, staging.
  • Запрет установки плагинов из админки в проде (DISALLOW_FILE_MODS) при деплое из git.
  • 2FA администраторов.
  • Заголовки не заменят патч плагина.
  • Инвентарь плагинов в git/wiki: имя, версия, зачем нужен. Nulled-копии запрещены политикой. После любого RCE считайте wp-config.php прочитанным — ротация не опциональна. На прод не возвращайте, пока find uploads -name '*.php' пуст.

FAQ

Deactivate достаточно?

Нет. Файл вызывают напрямую по URL.

Можно ли просто понизить версию?

Нет смысла, если дыра в этой версии. Нужен патч или удаление.

DISALLOW_FILE_MODS после инцидента

Да, чтобы не поставили второй шелл через админку, пока ключи живые.

Нужно ли менять префикс wp_?

Не вместо ротации. Можете заодно, это не серебро.

Плагин «security» не спас

Он сам может быть вектором, если старый. Смотрите его версию так же.