- PVSM.RU - https://www.pvsm.ru -
После восстановления сайт клиента снова открывался, страницы вернулись, картинки загружались. Только в футере вместо иконки адреса красовалась панель фильтров товаров, а у 54 страниц в canonical стоял адрес вида /?page_id=101. Обе проблемы я получил уже в процессе ремонта, своими руками.
Пришлось разбираться, почему правильный файл на сервере не исправляет картинку у клиента и почему опубликованная страница продолжает отдавать старые SEO-данные. Расскажу, как до этого дошло и на чём я сам попался.
В постоянной работе у меня 13 сайтов небольших компаний. В начале сентября на одном из них пропали фотографии товаров, страницы услуг стали отдавать 404, а вместо главной показывалась запись из блога. Одновременно пришло письмо от : антивирус нашёл и удалил вредоносные файлы. Домен не называю, сайт клиентский.
Этот сайт уже чистили 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 файла на диске, их там больше.
Сайт около пяти дней отдавал ошибки и показывал пустые места вместо картинок. Часть рекламных посадочных при этом сохранилась, заявки продолжали приходить, и проблему заметили не сразу.
Отдельно стоит проговорить: для такого удаления не нужен ни шелл, ни эксплойт. Достаточно прав администратора. Антивирус
Дамп базы был двухнедельной давности. За эти две недели на сайте появились заказы, заявки и правки. Подменить рабочую базу старой означало вместе с восстановленным контентом устроить ещё одну потерю данных.
Поэтому дамп развернули рядом, в той же базе под отдельным префиксом таблиц, и использовали для сравнения. Статусы 59 страниц я вернул прямым SQL-запросом, ориентируясь на дамп: опубликованные снова опубликовали, черновики оставили черновиками, вышло 60 и 5. Главную заново назначили в настройках чтения.
Затем восстановили 192 записи вложений, 387 строк их метаданных и связи с превью. Здесь есть момент, который легко упустить: в базе хранится только путь к изображению, а сама фотография лежит на диске. После возврата записей WordPress снова знает, где её искать, но по указанному адресу пока ничего нет.
С файлами помог
Папки плагинов из старой копии не возвращали. Сам uploads тоже требует проверки на вредоносные файлы: название каталога ничего не гарантирует.
О наличии и глубине хранения этих копий я узнал, когда они уже понадобились. Разумеется, выяснять такое лучше заранее. Но прежде чем добраться до бэкапа
Пока выгружался тяжёлый архив, мне захотелось сэкономить время. Я поискал недостающие изображения по именам в других каталогах, нашёл 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
Нажмите здесь для печати.