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

Считайте это инцидентом, не «почистить антивирусом на хостинге». Для site.example на 10.0.20.40 и /var/www/site.example: (1) изолируйте исходящий трафик и при необходимости входящий, (2) снимите копию файлов+БД, (3) ищите индикаторы (PHP в uploads, eval(base64, неизвестные mu-plugins, cron, пользователи), (4) чистите или поднимайте из чистого релиза + контент. Не rm -rf /var/www/site.example до копии и не включайте display_errors «посмотреть payload» в проде.

Смежный вход через плагин: взлом через плагин. Дальше — защита админки.

Не публикуйте payload шелла и не включайте display_errors «посмотреть вирус» — это утечка путей атакующему. Сначала tar webroot и дамп БД вне /var/www.

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

  • Поисковики: «сайт взломан», редирект только для User-Agent Google.
  • Письма с www-data на 25/tcp.
  • index.php отличается от дистрибутива той же версии.
  • 500 из обфускации: 500.

Не путать с легитимным кэшем/CDN, который отдаёт старый HTML.

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

  1. Уязвимый плагин/тема (отдельная статья).
  2. Слабый wp-login без rate limit.
  3. www-data пишет в wp-content шире, чем нужно, плюс RCE.
  4. Украденный FTP/SFTP/deploy ключ.
  5. Старый PHP-эксплойт в забытом /backup.zip в webroot.
  6. Соседний vhost на том же UID.

Мы не разбираем эксплуатацию. Фиксируем последствия.

Диагностика

1. Изоляция (минимально разрушительная)

Согласуйте с бизнесом. Пример nft: запрет исходящего с UID www-data на 25, 587, и подозрительные 80 к чужим IP — осторожно, сломаете легитимные API. Часто достаточно: снять сайт в return 503 на nginx кроме вашего IP 10.0.20.0/24, сохранить access.log.

Не ufw disable. Не открывайте всё «чтобы сканер зашёл».

2. Копия до любых удалений

sudo tar -C /var/www -czf /root/incident-site.example-$(date +%F).tar.gz site.example
sudo -u www-data wp --path=/var/www/site.example db export /root/incident-site.example-$(date +%F).sql
sudo cp -a /etc/nginx /root/incident-nginx-$(date +%F)

Храните копию вне webroot и вне публичного S3 без ACL.

3. Индикаторы файлов

sudo find /var/www/site.example -name '*.php' -path '*/uploads/*'
sudo grep -R --include='*.php' -lE 'eval\s*\(|assert\s*\(|gzinflate\s*\(|base64_decode\s*\(' /var/www/site.example/wp-content | head
ls -la /var/www/site.example/wp-content/mu-plugins
crontab -u www-data -l 2>/dev/null
sudo crontab -l
ls /etc/cron.d/

Смотрите wp-config.php на лишние include, изменённые ключи — не печатайте секреты в тикет.

4. Пользователи и опции

sudo -u www-data wp --path=/var/www/site.example user list --role=administrator
sudo -u www-data wp --path=/var/www/site.example option get siteurl

Неизвестные admin — индикатор. Не удаляйте, пока не экспортировали список в заявку.

5. Сеть процесса

sudo ss -tp | grep php-fpm

Исходящие к странным IP во время хита — полезно для IOC, не для «убить PID и забыть».

Решение

Сценарий A. Точечные IOC, ядро целое

Удалите конкретные вредоносные файлы после копии. Переустановите ядро:

sudo -u www-data wp --path=/var/www/site.example core download --force --skip-content

Плагины/темы — из чистых zip тех же версий, затем обновления. Ротация: пароли БД, FTP, AUTH_KEY в wp-config.php (новые соли), application passwords, nonce cookie — пользователи перелогинятся.

Сценарий B. Неясно, насколько глубоко

Поднимите новый каталог из чистого WP + дамп БД, пропущенный через поиск IOC (wp_options siteurl, неизвестные cron, widget с JS). Старый каталог оставьте read-only для экспертизы.

Не копируйте wp-content/uploads оптом, если там PHP: копируйте по расширениям изображений, PHP не переносите.

Сценарий C. Спам-бот в cron

Удалите crontab www-data, проверьте WP-Cron хуки (wp cron event list). Закройте xmlrpc.php если не нужен.

Сценарий D. Доступ через веб-шелл

Смените все секреты, SSH-ключи деплоя, пароль панели. Смотрите access.log на POST к странным URI до очистки — иначе потеряете вход.

Не публикуйте найденные шеллы как «PoC».

Сценарий E. PHP в uploads и nginx

Пока чистите, закройте исполнение:

location ~* ^/wp-content/uploads/.*\.(php|phtml|phar)$ {
    deny all;
}

Это не удаляет шелл, но режет вход. Затем удалите файлы по IOC из копии-исследования. Проверьте wp-content/upgrade, wp-content/mu-plugins, wp-content/languages на лишние php, .user.ini / auto_prepend_file в .htaccess.

Ротация секретов обязательна даже при «нашли один шелл»: он мог прочитать wp-config.php. Смените пароль БД, соли, админов, ключи деплоя, пароли SMTP. Не оставляйте WP_DEBUG_DISPLAY true.

Если сайт на общем хостинге с соседями под тем же UID — изоляции нет, рассматривайте переезд на отдельного пользователя/open_basedir/новый VPS после чистки.

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

  • Поиск PHP в uploads: пусто.
  • Админы только известные.
  • curl --resolve главная без редиректа на чужой домен, с UA Googlebot отдельно проверьте.
  • Исходящий спам прекратился (mailq, логи).
  • Файлы ядра совпадают с дистрибутивом (wp core verify-checksums).
sudo -u www-data wp --path=/var/www/site.example core verify-checksums
sudo -u www-data wp --path=/var/www/site.example plugin verify-checksums --all

Плагины без checksum в каталоге WP — ручная сверка.

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

  • Реинфекция через сутки: вход жив (плагин, ключ, соседний сайт).
  • Редирект на CDN/WAF уровне — не в файлах.
  • База с триггерами MySQL — редкость, но смотрите SHOW TRIGGERS.
  • Нужен перенос на чистый сервер: миграция.

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

  • Обновления, минимум плагинов, 2FA, brute force.
  • Запрет исполнения PHP в uploads (nginx location ^~ /wp-content/uploads/ { ... !php }).
  • Файлы не 777, деплой не от www-data с записью в ядро.
  • Бэкап вне сервера, immutable если есть.
  • Запрет PHP в uploads держите постоянно, не только в инцидент. После чистки проверьте, что auto_prepend_file в .user.ini снова пуст.

FAQ

Достаточно ли плагина «сканер»?

Как индикатор — да. Как единственная чистка — нет. Сканер не видит все обфускации.

Можно ли chmod 000 на uploads?

Сломаете медиа. Запретите PHP в location, не всю запись.

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

Слабая мера. Секреты и вход важнее.

Хостер предлагает «маливар ремов» без копии

Снимите свою копию сначала. Их чистка без артефактов бесполезна для разбора.

index.html в корне редиректит, index.php чистый

Смотрите index.html и autoindex/index директиву nginx (index index.html index.php — html победит).