- PVSM.RU - https://www.pvsm.ru -

Как я восстанавливал WordPress после взлома и дважды ошибся

После восстановления сайт клиента снова открывался, страницы вернулись, картинки загружались. Только в футере вместо иконки адреса красовалась панель фильтров товаров, а у 54 страниц в canonical стоял адрес вида /?page_id=101. Обе проблемы я получил уже в процессе ремонта, своими руками.

Пришлось разбираться, почему правильный файл на сервере не исправляет картинку у клиента и почему опубликованная страница продолжает отдавать старые SEO-данные. Расскажу, как до этого дошло и на чём я сам попался.

С чего всё началось

В постоянной работе у меня 13 сайтов небольших компаний. В начале сентября на одном из них пропали фотографии товаров, страницы услуг стали отдавать 404, а вместо главной показывалась запись из блога. Одновременно пришло письмо от хостинга [1]: антивирус нашёл и удалил вредоносные файлы. Домен не называю, сайт клиентский.

Этот сайт уже чистили 24 июля: убрали найденные шеллы, сменили пароль администратора. Одну закладку пропустили, и она была не в uploads, а в виде обычного с виду плагина. По журналам сервера и базе восстановилась такая хроника:

  • 24 июля. В каталоге плагинов появляется «инструмент доступности» с обработчиком, который принимает команду параметром запроса. Во время июльской чистки его не заметили: он выглядел как установленный плагин, а не как посторонний файл.

  • 29 июля, 22:32. Через пять дней после смены пароля создаётся администратор с именем вида support_a73db02f3003.

  • 2–3 августа. Заливаются 66 поддельных плагинов и 56 пустых тем с однотипными именами. Каждый умеет ровно две вещи: принять файл и сообщить, под каким пользователем работает.

  • 10 августа. Я захожу на сайт по другой задаче, ставлю фильтр от ботов в формах. Фильтр сделал, а список пользователей и плагинов не посмотрел. Вот это моя недоработка: о недавнем взломе я знал, и проверить эти два списка стоило независимо от текущей задачи.

  • 17–27 августа. С разных адресов ставятся файловый менеджер, обфусцированный плагин удалённой публикации и плагин с подписанными REST-эндпоинтами для вставки блоков в футер.

  • 29 августа, 17:54. Последовательность действий: вход, правка профиля, установка ещё двух плагинов, создание двух администраторов, обращения к аналитике заказов WooCommerce и настройкам оплаты. Затем удаление пользователя admin и ещё одной учётной записи.

Хроника между двумя чистками

Хроника между двумя чистками

Разные адреса и разные инструменты навели меня на мысль о перепродаже доступа. Были и странные попытки входа, где в поле пароля подставлялся адрес самого сайта, похоже на ошибку разбора чужого списка учётных данных. Но перепродажа и повторная утечка пароля через стилер остаются версиями: этих наблюдений недостаточно, чтобы установить число атакующих или источник пароля. Дальше речь о последствиях, которые видно на самом сайте.

Что удаление пользователя сделало с сайтом

Много лет сайт наполняли под admin. Соответственно, страницы и загруженные изображения числились за ним. При удалении этой учётной записи контент другому пользователю не передали.

В WordPress это существенный выбор. Если вызвать wp_delete_user() без переназначения автора, движок запускает удаление связанного контента. Под него попадают типы записей, для которых предусмотрено удаление вместе с пользователем; набор зависит от их регистрации и фильтров.

На этом сайте получилось следующее:

  • 59 страниц оказались в корзине, включая главную. Настройка «главная страница сайта» слетела на вывод последних записей, поэтому на месте главной и открывался пост из блога.

  • 192 объекта медиатеки удалились вместе со связанными файлами.

  • У 56 записей исчезли связи с изображениями превью.

Что тянет за собой удаление одного пользователя

Что тянет за собой удаление одного пользователя

Страницы при включённой корзине ещё можно вернуть. С вложениями сложнее: корзина для них по умолчанию отключена, а при окончательном удалении wp_delete_attachment() удаляет и файлы, включая созданные размеры изображений. Поэтому 192 объекта медиатеки — не обязательно 192 файла на диске, их там больше.

Сайт около пяти дней отдавал ошибки и показывал пустые места вместо картинок. Часть рекламных посадочных при этом сохранилась, заявки продолжали приходить, и проблему заметили не сразу.

Отдельно стоит проговорить: для такого удаления не нужен ни шелл, ни эксплойт. Достаточно прав администратора. Антивирус хостинга [1] уберёт найденные вредоносные файлы, но посторонние учётные записи он не тронет.

Почему не подошёл полный откат из бэкапа

Дамп базы был двухнедельной давности. За эти две недели на сайте появились заказы, заявки и правки. Подменить рабочую базу старой означало вместе с восстановленным контентом устроить ещё одну потерю данных.

Поэтому дамп развернули рядом, в той же базе под отдельным префиксом таблиц, и использовали для сравнения. Статусы 59 страниц я вернул прямым SQL-запросом, ориентируясь на дамп: опубликованные снова опубликовали, черновики оставили черновиками, вышло 60 и 5. Главную заново назначили в настройках чтения.

Затем восстановили 192 записи вложений, 387 строк их метаданных и связи с превью. Здесь есть момент, который легко упустить: в базе хранится только путь к изображению, а сама фотография лежит на диске. После возврата записей WordPress снова знает, где её искать, но по указанному адресу пока ничего нет.

С файлами помог хостинг [1]: у него оказались ночные копии за 30 дней. В копии за 29 августа фотографии ещё были, резервирование прошло ночью, а удаление вечером. Из неё вернули uploads: 1 194 файла общим объёмом 212 МБ. Это объём восстановленного каталога целиком, его не стоит путать с числом удалённых объектов медиатеки.

Папки плагинов из старой копии не возвращали. Сам uploads тоже требует проверки на вредоносные файлы: название каталога ничего не гарантирует.

О наличии и глубине хранения этих копий я узнал, когда они уже понадобились. Разумеется, выяснять такое лучше заранее. Но прежде чем добраться до бэкапа хостинга [1], я успел попробовать другой способ вернуть картинки.

Первая ошибка: нашёл файл с тем же именем

Пока выгружался тяжёлый архив, мне захотелось сэкономить время. Я поискал недостающие изображения по именам в других каталогах, нашёл b1.png и b2.png и скопировал их на место. Файлы существуют, имена совпадают, на первый взгляд всё в порядке.

На самом деле это были демонстрационные картинки из плагина фильтров товаров. В результате в футере вместо скромной иконки адреса раскорячилась панель фильтрации. Я проверил, что файл есть, но не проверил, что именно в нём нарисовано.

Через 13 минут восстановление из бэкапа перезаписало эти файлы правильными. А ещё через пару часов клиент пишет, что в футере что-то чужое. На сервере уже всё правильно, а у него по-прежнему панель фильтров.

Причина оказалась в кеше. Изображения отдавались со сроком кеширования на год. Тем, кто открыл сайт в эти 13 минут, браузер сохранил неправильную картинку и продолжал показывать её после замены файла на сервере: URL не изменился, значит, можно взять из кеша.

Помогло добавление версии к адресу изображения, браузер запросил его заново. Но это уже лечение последствий. При поиске исходного файла нужно проверять путь, содержимое и размеры изображения, а при наличии эталона — контрольную сумму. Имя вроде b1.png само по себе не значит ничего, в каком бы каталоге оно ни нашлось.

Я хотел сэкономить время на поиске нормального бэкапа. Потратил его на разбор чужой картинки в футере.

Вторая ошибка: страница открывается, значит, восстановлена

С возвратом страниц всё выглядело ещё убедительнее. Запрос выполнен, статусы восстановлены, страницы открываются, текст на месте. Открываю исходный код страницы услуг и вижу в canonical адрес вида /?page_id=101 вместо /uslugi/.

Оказалось, что я восстановил основные данные, а производные данные Yoast остались в прежнем состоянии. Плагин хранит SEO-сведения об объектах сайта в собственных таблицах indexables. При обычном изменении записи срабатывает цепочка обработчиков WordPress. Прямой UPDATE в базе эту цепочку не вызывает.

Поэтому страница уже открывалась как опубликованная, а Yoast продолжал отдавать сведения на момент аварии. У 54 страниц неправильными оказались canonical, og:url и идентификаторы в микроразметке. Проверка «ссылка открывается» этого не показывает вообще никак.

Данные Yoast пересобрали точечно, для затронутых страниц. Для полной пересборки у плагина есть штатная команда WP-CLI:

wp yoast index --reindex

Она удаляет существующие indexables и строит их заново. Перед полной пересборкой нужна резервная копия, а на большом сайте стоит учесть нагрузку.

После исправления восстановленные адреса отправили на переобход.

Вывод, который я забрал себе: после восстановления через SQL надо проверять и исходный HTML, то есть canonical, Open Graph, микроразметку и ссылки на изображения. И заранее выяснять, какие производные данные хранят установленные плагины и как они обновляются. У другого SEO-плагина механизм может отличаться от Yoast.

Что поменяли после разбора

Помимо восстановления контента, удалили четырёх посторонних администраторов, переназначили контент своей учётной записи и обновили соли аутентификации, чтобы старые cookie входа стали недействительными. Подозрительные плагины сохранили для разбора вне каталога, доступного через веб-сервер. У владельца теперь отдельная учётная запись.

Штатную установку и изменение файлов через админку закрыли настройкой DISALLOW_FILE_MODS. Она заодно отключает редактор плагинов и тем, отдельно задавать DISALLOW_FILE_EDIT для этого не требуется. У ограничения есть цена: нужен контролируемый порядок обновлений.

Перед wp-login.php добавили Basic Auth с отдельным паролем. Это дополнительный барьер на входе, не двухфакторная аутентификация и не защита всех остальных путей доступа. И оговорюсь честно: уже оставленный вредоносный код перечисленные ограничения не обезвреживают.

Что закрыли после разбора

Что закрыли после разбора

И ещё две команды, которые стоило выполнить 10 августа, когда я заходил ставить фильтр от ботов:

wp user list --role=administrator
wp plugin list

Полного аудита они не заменяют, но посторонних администраторов я увидел бы за минуту, за три недели до того, как удалили контент.

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

Если сталкивались с похожим восстановлением, интересно, какие ещё данные плагинов приходилось пересобирать после прямых изменений в базе.

Автор: itmaks

Источник [2]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/wordpress/457695

Ссылки в тексте:

[1] хостинга: https://www.reg.ru/?rlink=reflink-717

[2] Источник: https://habr.com/ru/articles/1080612/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1080612