MAATRIX / Блог / Как проверить сайт на закладки, если исходников нет в git

Как проверить сайт на закладки, если исходников нет в git

MAATRIX

Сайту три года, а то и семь, он ни разу не переезжал в репозиторий — правки вносились прямо на проде через FTP или админку. А потом появляется подозрение: то ли сайт разослал фишинг, то ли хостинг написал про подозрительный трафик. И тут выясняется главная проблема — сравнивать не с чем. Нет коммита, который можно взять за эталон «как должно быть». Разбираем, как в такой ситуации всё-таки найти закладку — и что сделать, чтобы в следующий раз этой проблемы не было.

Почему без git это сложнее, но не тупик

Git нужен как источник истины: «вот файл, каким он был в момент последнего осознанного изменения». Без него эталон приходится восстанавливать из трёх источников, и по отдельности ни один не даёт полной картины, но вместе — почти всегда достаточно:

  • Дистрибутив CMS/фреймворка той же версии — ловит подмену файлов ядра.
  • Метаданные файловой системы (даты изменения) — ловит то, что тронуто позже нормального цикла обновлений.
  • Сигнатуры подозрительного кода — ловит закладки в кастомном коде и теме, для которых эталона в принципе не существует.

Шаг 1. Соберите чистый эталон той же версии

Сначала точно узнайте версию движка: для WordPress — wp-includes/version.php (переменная $wp_version), для PHP-фреймворков — composer.lock, для Node — package-lock.json. Если lock-файла нет, версию иногда можно вычислить по changelog в readme.html или по структуре директорий конкретного релиза.

Дальше скачиваете официальный релиз той же версии в отдельную директорию вне веб-корня:

mkdir -p /root/reference && cd /root/reference
wget https://wordpress.org/wordpress-6.6.2.tar.gz
tar xzf wordpress-6.6.2.tar.gz

Если на сервере есть WP-CLI, для WordPress есть более быстрый путь — встроенная сверка контрольных сумм с официальным репозиторием:

wp core verify-checksums --path=/var/www/site
wp plugin verify-checksums --all --path=/var/www/site

Команда сравнивает каждый файл по хешу и печатает список того, что отличается или отсутствует — это сразу отсекает большинство файлов как «точно родные». Для темы отдельной команды обычно нет: темы почти всегда кастомизированы, и «чистой» версии у вас, скорее всего, никогда не было.

Для фреймворков без такого инструмента — то же руками: composer.lock называет точные версии пакетов, скачиваете их с packagist.org или из GitHub-релиза в vendor.reference/ и сравниваете с рабочим vendor/. Нюанс: в vendor/ иногда бывают законные локальные патчи — не путайте их с закладкой.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Шаг 2. Сравните файлы построчно

Когда эталон собран — обычный diff по всему дереву:

diff -rq /var/www/site/wp-includes /root/reference/wordpress/wp-includes
diff -u /root/reference/wordpress/wp-includes/functions.php \
        /var/www/site/wp-includes/functions.php

Флаг -q важен на больших деревьях: полный diff -r без него даст тысячи строк, в которых потеряется единственная значимая правка. Сначала смотрите список отличающихся файлов, и только по конкретным — полный дифф. Расхождение при совпадающем имени — либо легитимная локальная правка, либо закладка; различить можно только глазами: странная правка в файле, который логически не должен ничего исполнять динамически или отправлять по сети, — красный флаг.

Шаг 3. Ищите файлы с «неправильной» датой изменения

Файлы вне ядра (кастомный плагин, аплоад, файл темы) сравнивать не с чем — здесь работает дата последнего изменения. Файл «моложе» всех известных легитимных событий (установка, обновление, осознанная правка), который при этом не должен был меняться, — подозрителен.

find /var/www/site -name '*.php' -mtime -30 -printf '%T@ %Tc %p\n' | sort -n
find /var/www/site -name '*.php' -printf '%T@ %Tc %p\n' | sort -rn | head -50

Дальше — ручная сверка каждой даты с тем, что вы реально помните. Файл в wp-content/uploads/ (где по логике не должно быть исполняемого PHP) с расширением .php и датой, не совпадающей ни с чем известным, — почти наверняка веб-шелл. Отдельно это разобрано в статье про поиск чужого PHP в папке uploads.

Оговорка: mtime подделывается командой touch -r/touch -d, так что дата — сигнал для приоритизации, а не доказательство. В массовых автоматизированных атаках маскировкой обычно не заморачиваются, и дата вскрывает закладку сразу; если заморачивались — нужны шаги 1 и 4.

Шаг 4. Ищите характерные паттерны подозрительного кода

Даже без эталона закладку часто выдаёт сам код — легитимный код сайта редко пишется в стиле веб-шеллов. Базовый набор паттернов по всему веб-корню:

grep -rlE 'eval\s*\(|base64_decode\s*\(|gzinflate\s*\(|str_rot13\s*\(' --include='*.php' /var/www/site
grep -rlE 'create_function\s*\(|assert\s*\(|system\s*\(|shell_exec\s*\(|passthru\s*\(' --include='*.php' /var/www/site
grep -rlE '\$_(GET|POST|REQUEST|COOKIE)\s*\[.{0,40}\]\s*\(' --include='*.php' /var/www/site

Каждое совпадение по отдельности — не приговор: eval() и base64_decode() изредка встречаются и в легитимном коде. Но «файл вне обычной структуры темы/плагина» + совпадение по паттерну + подозрительная дата из шага 3 — уже основание разбираться детально.

Отдельный сильный сигнал — обфускация через склейку коротких строк, чтобы обойти сигнатурные сканеры: вместо eval($x) в файле будет что-то вроде $a='e'.'v'.'a'.'l';$b=chr(36).chr(120);$a($b);. Ищется по плотности точек и коротких строковых литералов: grep -rlE '(\.\s*[\x27"][a-zA-Z0-9+/=]{1,4}[\x27"]\s*\.){5,}' --include='*.php' /var/www/site. Ещё признак — файл почти целиком на одной длинной строке без переносов (минификация): find /var/www/site -name '*.php' -exec awk 'END{if(NR<3 && length($0)>300)print FILENAME}' {} \;.

Антивирусный сканер с базами сигнатур веб-шеллов (например, clamscan -r --infected /var/www/site) — быстрый первый проход, но не более: сигнатурный сканер ловит известные образцы, а написанный под конкретную цель код пройдёт незамеченным.

Что делать с находкой — не удаляйте сразу

Нашли подозрительный файл — не стирайте его сразу. Один файл почти никогда не единственная закладка: получив доступ, злоумышленник обычно оставляет несколько точек входа на случай, если одну найдут. А сам файл — источник информации о способе проникновения: удалите его сразу, и причина (открытая уязвимость, слитые учётные данные) останется, доступ появится снова через день-два.

Разумный порядок: скопировать файл в отдельное защищённое место, посмотреть логи веб-сервера на обращения к нему (часто виден и способ проникновения, и то, использовалась ли закладка активно), проверить cron на вторую точку закрепления — и только после этого чистить и менять пароли и ключи, до которых мог дотянуться скомпрометированный код. Общий порядок действий разобран в статье про план действий при инциденте.

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

Начните вести git прямо сейчас

Всё описанное выше — способ разобраться постфактум, когда эталона нет и его приходится восстанавливать окольными путями. Он работает, но требует часов ручной работы. В следующий раз всё будет быстрее, если эталон появится сам собой — из истории git. Завести репозиторий никогда не поздно:

cd /var/www/site
git init

Добавьте .gitignore, чтобы не тащить кеш, логи и загруженные файлы:

wp-content/cache/
wp-content/uploads/
*.log

Затем базовый коммит как отправная точка:

git add -A
git commit -m "baseline: снимок состояния на $(date +%Y-%m-%d), до этого версионирования не было"

Коммит не идеален (в нём, возможно, уже сидит закладка), но с этого момента появляется история: следующее изменение файла будет видно через git diff. Дальше стоит настроить автоснимки — cron, коммитящий изменения раз в сутки:

# /etc/cron.d/site-snapshot
0 3 * * * www-data cd /var/www/site && git add -A && git diff --cached --quiet || git commit -m "auto snapshot $(date +\%Y-\%m-\%d)"

Это не замена нормальному деплою через git, но даже пассивный снимок радикально меняет ситуацию при следующем подозрении: вместо «сравнивать не с чем» будет «вот дифф за последние три дня».

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Точную версию CMS установить не получается?

Проверьте readme.html, changelog, composer.lock или package-lock.json хотя бы частично. Если версия совсем неизвестна — возьмите ближайший по времени выпуск как приближённый эталон, учитывая, что часть расхождений — разница версий, а не закладка.

Официального дистрибутива нужной версии больше нет в открытом доступе?

Для WordPress все версии доступны в архиве релизов на официальном сайте. Для менее популярных проектов — архив на GitHub (Releases/Tags) или пакетного менеджера.

Антивирус ничего не находит — значит, всё чисто?

Нет. Сигнатурный антивирус ловит известные образцы, а написанный под конкретную цель код легко проходит мимо баз.

Можно ли доверять только датам изменения, без сравнения с эталоном?

Только как первый быстрый проход: даты подделываются командой touch, и опытный злоумышленник это делает.

Как часто теперь коммитить?

Минимум раз в сутки автоматическим cron-снимком, а лучше — перейти на нормальный процесс деплоя через git.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →