Раунд 1. «Админка не сохраняет изменения»
Первый симптом был совсем не похож на взлом: при сохранении страниц WordPress выдавал ошибку. Выяснилось, что база данных выросла с 46 МБ до 1,1 ГБ. В ней было больше 4000 спам-записей про казино на разных языках, датированных 2015 годом, чтобы не бросаться в глаза в списке. Вместе с ревизиями и метаданными — около 14 000 лишних строк.
Хостинг увидел превышение лимита на размер базы и автоматически отключил право записи. Поэтому админка и перестала сохранять. Это не было действием злоумышленника, но выглядело как саботаж.
Кроме спам-записей:
- главная страница была переключена со статичной страницы на ленту записей — посетители видели спам;
- в текст настоящих страниц был дописан спам и скрипт с всплывающим окном на казино;
- появились страницы-«визитки» взломщиков с заголовками вида «hacked by …».
Спам удалили, база вернулась к 46 МБ, хостинг сам восстановил права. Пароль администратора сменили.
Раунд 2. Спам вернулся через несколько часов
В названиях пунктов меню снова появились приписки вида «слово @ спам-домен». Пароль к этому моменту уже был новым — значит, у злоумышленника был другой ключ.
Нашёлся пароль приложения — отдельный токен для REST API, привязанный к аккаунту администратора. Его последний раз использовали с чужого IP. Смена основного пароля такие токены не отзывает, поэтому про них часто забывают. Токен удалили, создание новых запретили.
Раунд 3. Двадцать девять администраторов
Через две недели в базе нашлось 29 администраторов, которых владелец не создавал. Они появлялись примерно по одному в день, а их почтовые адреса содержали название автоматического инструмента для массового взлома WordPress. Сайт атаковал не конкретный человек, а бот, который обходит тысячи сайтов.
Чужих администраторов удалили. А затем впервые провели сверку файлов ядра WordPress с официальными контрольными суммами — и нашли то, что пропустили все предыдущие проверки.
wp-includes лежал посторонний файл с именем, похожим на настоящий файл ядра. Внутри — зашифрованный код, который выполнялся на каждой загрузке страницы. Дата изменения файла была подделана на 2020 год, а комментарии в нём забиты случайными словами, чтобы обмануть сканеры. Подключали его шесть настоящих файлов ядра, в каждый была добавлена одна незаметная строка.Почему его не нашли раньше: поиск вирусов шёл по файлам, изменённым за последние 30–45 дней, и по типичным словам вроде eval и base64. Подделанная дата и шифрование обходили обе проверки. Сверка с контрольными суммами WordPress находит такие файлы сразу, потому что ей не важны ни дата, ни содержимое — только «совпадает ли файл с оригиналом».
Посторонний файл удалили, шесть файлов ядра заменили оригинальными из официального репозитория WordPress той же версии.
Раунд 4. Один администратор без следов в логах
Через пять часов появился ещё один администратор. На этот раз в журнале доступа не было ни одного запроса в момент его создания. Файлы ядра оставались чистыми.
Это навело на мысль о прямом доступе к базе данных. Пароль от неё ни разу не меняли за все раунды — его сменили.
Раунд 5. Настоящий канал
Смена пароля базы не помогла: в тот же день появились ещё два администратора. Зато в этот раз журнал доступа показал причину. Сотни POST-запросов в день с 13 разных IP-адресов шли на пакетный эндпоинт REST API — /wp-json/batch/v1 — и все получали успешный ответ.
Пакетные запросы нужны редактору блоков, то есть авторизованным пользователям. Анонимному посетителю они не нужны вообще. Мы закрыли их для неавторизованных запросов небольшим must-use плагином — такой плагин загружается всегда и не отключается из админки.
Почему понадобилось пять раундов
Каждая чистка находила реальную проблему: спам, токен, бэкдор, старый пароль. Но каждая из них была следствием, а не входом. Пока открыт канал, через который бот создаёт администраторов, он вернётся и заново сделает всё остальное.
Правильный порядок при повторном заражении:
- Сверка ядра с контрольными суммами — первым делом, а не в последнюю очередь. Команда
wp core verify-checksumsили скрипт, сравнивающий файлы с официальным списком WordPress. - Журнал доступа за дни, когда появлялись чужие администраторы. Ищите POST-запросы к
/wp-json/,?rest_route=,admin-ajax.phpиxmlrpc.php. - Все виды доступа сразу: пароли администраторов, пароли приложений, секретные ключи, пароль базы, пароль хостинга.
- Закрыть то, что анонимам не нужно: пакетный REST, список пользователей через REST, XML-RPC.
- Наблюдение после чистки. Если взлом повторится, о нём нужно узнать в первый час, а не через две недели.
Подробнее про чужих администраторов — в статье «В WordPress появился неизвестный администратор», про поиск скрытого кода — в разборе eval(base64_decode).
Частые вопросы
Почему антивирусный плагин не нашёл бэкдор?
Код был зашифрован, а комментарии забиты случайным текстом, чтобы не совпасть с сигнатурами. Такой файл находит только сверка с оригинальными файлами WordPress: она не смотрит на содержимое, а проверяет, совпадает ли файл с эталоном.
Зачем злоумышленнику подделывать дату файла?
Многие ищут вирусы среди файлов, изменённых за последний месяц. Файл с датой 2020 года в такой поиск не попадёт, хотя появился на сайте недавно.
Не сломает ли закрытие пакетного REST редактор?
Нет, если закрывать его только для неавторизованных запросов. Редактор работает от имени вошедшего пользователя, и для него ничего не меняется.
Сколько стоит такое расследование?
Срочная чистка с поиском точки входа — от 4 990 ₽. Если сайт заражают повторно, начинаем именно с поиска канала, а не с удаления спама.