Перейти к основному содержанию
Блог

Новости

Разбор бэкдора в WordPress: как реальный взлом нашли и вычистили

Вход через устаревший плагин, фальшивые плагины, скрытые папки и однострочный загрузчик в файлах ядра. Пошаговый разбор реального взлома WordPress.

· 12 мин. чтения

Фото: Gmoran6, CC BY-SA 4.0 · Wikimedia Commons

Недавно мы работали с WordPress-сайтом нашего клиента: сайт был взломан, и чужой код работал на нём несколько месяцев. В этой статье покажем, как атакующий попал внутрь, что он оставил, как мы это нашли и как вычистили. Цель — не напугать, а показать, как распознать похожую ситуацию на своём сайте и с чего начинать.

Данные клиента обезличены: домен, тип бизнеса и другие подробности не приводятся.

Точка входа: устаревший плагин

На сайте стоял плагин Backup Migration версии 1.4.0. В этой версии есть известная уязвимость — CVE-2023-6553: удалённое выполнение кода без аутентификации (unauthenticated RCE). То есть атакующему не нужны ни логин, ни пароль: отправив специальный запрос, он может выполнить свой PHP-код на сервере.

Уязвимость была опубликована и исправлена давно. Но плагин не обновляли, и в августе атакующий разместил веб-шеллы прямо в папке этого плагина. Через два месяца пришла вторая волна: новые файлы, новые способы маскировки.

Большинство взломов начинается не со сложной атаки, а с одного необновлённого плагина.

Что оставил атакующий

В ходе анализа нашлось следующее:

  • около 20 фальшивых плагинов со случайными именами — w2s и затем hex-символы (например, w2s3fa1c9). В админ-панели среди обычных плагинов они не бросаются в глаза.
  • Фальшивая тема рядом с настоящими.
  • Дропперы wp-validate.php во множестве папок. Имя похоже на файл WordPress, но в ядре такого файла нет.
  • Скрытые папки — начинающиеся с точки, со случайным hex-суффиксом, внутри пара из маленьких .htaccess и index.php. Они размещены внутри настоящих плагинов — WooCommerce, Yoast, Elementor — и внутри wp-includes. Поэтому «удалить чужой плагин» их не уберёт.
  • PHP-файлы в uploads — в этой папке PHP вообще не должно быть.
  • Загрузчики, скачивающие код с внешнего домена, обфусцированные через rot13.
  • Подделанные временные метки — дата изменения файлов выставлена на 2020 год (timestomping), чтобы они выглядели как «старые нетронутые файлы».
  • Удалённый wp-login.php — стандартная страница входа уничтожена.

Каждый из этих пунктов — отдельный путь повторного входа. Оставите один — атакующий вернётся, что и показала вторая волна.

Самое скрытое: однострочный загрузчик в файлах ядра

Самой интересной находкой стал постоянный (persistent) загрузчик. В 9 файлов ядра WordPress и в wp-config.php была добавлена одна строка:

@include_once("php://filter/read=zlib.inflate|string.rot13|convert.base64-decode/resource=wp-includes/...");

Что она делает? С помощью обёртки потоков php://filter PHP читает файл с диска, последовательно декодирует его из base64, применяет rot13 и распаковывает zlib — а результат выполняет как PHP-код. То есть вредоносный код не лежит на диске в виде открытого PHP.

Кроме того:

  • Строка записана в виде hex-escape ("\x70\x68\x70...") и перемешана со случайными комментариями — простой grep 'php://filter' её не находит.
  • Основная нагрузка (payload) спрятана в фальшивых файлах .svg, .css, .js. Имя и расширение выглядят безобидно.
  • Нагрузка при каждом открытии страницы делала внешний запрос. Это проявлялось в PHP error log странными строками — именно это и стало первой зацепкой для анализа.

Как мы это нашли

1. Сравнение файлов ядра с официальными контрольными суммами

WordPress публикует MD5-суммы файлов ядра для каждой версии через api.wordpress.org/core/checksums. WP-CLI выполняет эту проверку одной командой:

wp core verify-checksums
wp plugin verify-checksums --all

Первая команда показывает изменённые файлы ядра и файлы, которых в ядре быть не должно. Вторая проверяет плагины из каталога wordpress.org (для платных плагинов контрольных сумм может не быть). Именно здесь всплыли 9 изменённых файлов ядра.

2. Поиск PHP в uploads

find wp-content/uploads -type f -iname '*.php*'

На нормальном сайте эта команда ничего не выводит.

3. Поиск по известным признакам

find . -name 'wp-validate.php'
find wp-content/plugins -maxdepth 1 -type d -name 'w2s*'
find . -type d -name '.*' ! -name '.' ! -name '.well-known'
grep -rlE '(\\x[0-9a-fA-F]{2}){8,}' --include='*.php' .
grep -rl 'php://filter' --include='*.php' .

Поиск последовательностей hex-escape помогает найти обфусцированные строки.

4. Группировка файлов по времени

Если сгруппировать файлы по времени изменения, видна «пачка», созданная в одну и ту же минуту. Но из-за timestomping mtime доверять нельзя. Поэтому проверяется и ctime (время изменения inode) — подделать его обычными средствами гораздо сложнее:

find . -type f -printf '%C+ %T+ %p\n' | sort | awk '{print substr($1,1,16)}' | uniq -c | sort -rn | head

Файлы, у которых mtime показывает 2020 год, а ctime — август, сразу подозрительны.

5. Анализ логов

В access-логе нашлись POST-запросы к файлам веб-шеллов и обращения к эндпоинту уязвимого плагина, а в PHP error log — строки, связанные с внешними запросами загрузчика. Это помогло определить время обеих волн.

Как мы чистили

Порядок важен. Мы работали в такой последовательности:

  1. Сначала сохранить доказательства. Полная копия файлов сайта и базы, хеши файлов, логи веб-сервера и PHP были перенесены в отдельное место. Ответить на вопрос «что же произошло» после очистки можно только по этим копиям.
  2. Заблокировать пути бэкдоров на веб-сервере. Чтобы атакующий не мог воспользоваться файлами, пока мы с ними работаем, известные пути были закрыты на уровне веб-сервера:
location ~* /wp-content/uploads/.*\.php$ { deny all; }
location ~* /wp-validate\.php$          { deny all; }
location ~* /\.(?!well-known)           { deny all; }
  1. Карантин, а не удаление. Вредоносные файлы не удалялись, а переносились в папку карантина за пределами веб-корня. Это позволяет продолжать анализ.
  2. Заменить ядро чистыми файлами. Ядро WordPress было заново загружено из официального источника, контрольные суммы проверены повторно:
wp core download --version=$(wp core version) --force --skip-content
wp core verify-checksums
  1. Убрать загрузчики из остальных файлов. Добавленные строки в wp-config.php и других файлах были удалены вручную.
  2. Удалить уязвимый плагин. Backup Migration 1.4.0 удалён полностью.
  3. Сменить соли WordPress. Ключи и соли в wp-config.php обновлены — это аннулирует все существующие сессии:
wp config shuffle-salts

Шаги, которые ещё нужно выполнить

Очистка файлов — только часть работы. После неё обязательно выполняется следующее:

  • Аудит базы данных: посторонние пользователи-администраторы, внедрённые записи в таблице wp_options, скрипты внутри постов, спам-страницы.
  • Переустановка плагинов из чистых источников — вместо попыток вычистить существующие папки.
  • Смена всех паролей: пользователи WordPress, база данных, панель хостинга, FTP/SFTP, SSH.
  • Обновление всего: ядро, плагины, темы, PHP.

Начальные команды для проверки базы:

wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp db query "SELECT option_name FROM wp_options WHERE option_value LIKE '%<script%' OR option_value LIKE '%eval(%';"
wp db query "SELECT ID, post_title FROM wp_posts WHERE post_content LIKE '%<script%';"

Почему это важно для экспертизы

Худший сценарий при официальной экспертизе взломанного сайта — когда проверяющий сам находит веб-шелл или посторонние файлы. Даже если сайт вроде бы работает нормально, уязвимая версия плагина, исполняемые PHP-файлы в uploads, открытые скрытые папки — каждое из этого отдельная находка. Поэтому перед подачей WordPress-сайта на экспертизу нужно убедиться, что он чист: одного «обновления» недостаточно. Профилактические шаги мы собрали в статье чек-лист безопасности WordPress.

Короткий чек-лист

  1. wp core verify-checksums и wp plugin verify-checksums --all дают чистый результат.
  2. В wp-content/uploads нет PHP-файлов, и веб-сервер запрещает там выполнение PHP.
  3. В списке плагинов нет незнакомых плагинов или тем со случайными именами.
  4. Нет скрытых папок, начинающихся с точки, и посторонних файлов вроде wp-validate.php.
  5. В файлах ядра и wp-config.php нет php://filter, длинных hex-строк или посторонних include.
  6. В PHP error log нет непонятных внешних запросов.
  7. Список администраторов проверен, пароли и соли сменены.
  8. Все плагины последней версии, неиспользуемые удалены.

Итог

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

Хотите убедиться, что на вашем WordPress-сайте нет похожих следов? Оставьте заявку на сканирование — агент Claude AI и наш специалист изучат файлы, плагины и настройки сервера, а мы поможем закрыть найденные дыры.