Руководства по безопасности

Что проверить после экстренного обновления PHP: веб-PHP и PHP в командной строке, set -e и коды возврата diff, а также повторная проверка на взлом

После срочного обновления веб-PHP и CLI-PHP могут отличаться. Как проверить оба, set -e и diff, и проверка на взлом.

Опубликовано 2026-11-01 Обновлено 2026-11-01 Последняя проверка 2026-11-01 12 мин чтения
На этой странице

Аудитория: люди, которые управляют сайтами на PHP и применяют экстренное обновление PHP или только что его применили и беспокоятся о пробелах.

В октябре 2026 года были выпущены исправленные версии для двух критических уязвимостей PHP (CVE-2026-109618 и CVE-2026-109619). Сайт, которым управляет оператор этого сайта, был обновлён в ту же ночь. Эта страница переводит произошедшее там, два первых предположения, которые оказались неверными, и повторную проверку на взлом, примененную затем ко всем серверам, в общие шаги. Об уязвимостях и исправленных версиях рассказано в статье Две критические уязвимости PHP и экстренные исправленные выпуски (октябрь 2026).

Что произошло (на сайте, которым управляет оператор этого сайта)

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

  1. После объявления

    Переход на страницу обслуживания и ожидание исправленной версии
  2. После выпуска

    Установка исправленной версии и снятие страницы обслуживания
  3. Проверка после обновления

    Веб-часть была обновлена, но PHP в командной строке, которым пользовался cron, всё ещё указывал на старую версию, поэтому его перенастроили
  4. В ту же ночь

    Временный скрипт, который снимал режим обслуживания, остановился до записи чего-либо. Причина: set -e в сочетании с diff. Вреда не было, скрипт переписали
  5. Позже

    Тот же чек-лист проверки на взлом применили к каждому серверу, а один подозрительный запрос, пришедший до обновления, проверили сравнением файлов

Позднее американское CISA добавило обе уязвимости CVE в свой каталог уязвимостей, известных как эксплуатируемые (KEV) (объявление CISA). Это подтверждает решение спешно обновиться, но патч лишь закрывает дверь. Проверка того, что никто уже не проник внутрь, — отдельная задача.

Предположение 1: «Веб-PHP обновлён, значит, всё готово»

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

PHP может быть разной программой в зависимости от способа запуска (SAPI, Server API). php.net отмечает, что CLI SAPI во многом отличается от других SAPI (PHP: использование PHP из командной строки). Если на одной машине установлено несколько версий, веб может работать на новой версии, а командная строка — на старой.

Веб-запросы → PHP-FPM / модуль Apache

Работает обновлённая версия (видна в Site Health и похожих экранах)

cron и скрипты оболочки → PHP в командной строке

Всё ещё указывает на старую версию, которую веб-проверка не покажет

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

Проверяйте веб и CLI отдельно

1

Проверьте версию PHP в командной строке

php -v показывает версию PHP, которую вызывает эта оболочка. php.net указывает, что php -v также сообщает, является ли PHP сборкой CGI или CLI. which php показывает расположение исполняемого файла, который действительно вызывается.

2

В системах в стиле Debian с несколькими версиями проверьте, куда указывает команда

update-alternatives --display php показывает, на какую версию указывает команда php (руководство update-alternatives). --config меняет её, но это изменение конфигурации сервера, поэтому сначала уточните порядок действий и способ отката.

3

Указывайте полный путь к PHP в заданиях cron

cron запускает задания в собственной среде, отдельной от интерактивной оболочки входа (crontab(5)). Если вы пишете только php, то выбранный PHP зависит от этой среды. Совет этого сайта: укажите полный путь к версии, которую вы обновили, чтобы проверка после обновления давала однозначный ответ.

4

Проверьте версию веб-PHP со стороны веба

В WordPress её показывает Site Health на экране администратора. В коде доступны константы PHP_VERSION (версия) и PHP_SAPI (способ запуска PHP: cli для CLI, fpm-fcgi для PHP-FPM, apache2handler для модуля Apache) (php_sapi_name).

5

Удаляйте страницу phpinfo() сразу после проверки

Публичный файл, который вызывает phpinfo(), показывает версию, но также раскрывает настройки конфигурации и внутренние детали сервера. Удалите его сразу после работы. Неоставление его на сайте — условие использования этого метода.

Предположение 2: «diff либо успешен, либо завершается с ошибкой»

Временный скрипт, который снимал режим обслуживания, был написан так, чтобы перед записью сравнить текущее и новое содержимое с помощью diff. В нём был включён set -e.

diff возвращает код выхода 1, когда находит различия. В руководстве GNU 0 означает отсутствие различий, 1 — что различия найдены, а 2 — проблему (GNU diffutils: diff). Поиск различий как раз и ожидался скриптом. Но set -e (errexit) останавливает оболочку при ненулевом коде, кроме таких мест, как условия. Скрипт остановился из-за найденных различий и так и не дошёл до записи.

Поэтому шаг снятия режима обслуживания остановился без записи, и вреда не было. Он завершился в безопасную сторону, но обратное сочетание, когда скрипт не останавливается там, где должен, было бы опасным. Скрипт переписали.

Команды, где код выхода 1 не означает «ошибку»

КомандаЗначение 1Значение 2Источник
diffНайдены различияПроблемаGNU diffutils: diff
cmpНайдены различияПроблемаGNU diffutils: cmp
grepНе выбрано ни одной строки (нет совпадений)Произошла ошибкаGNU grep

Безопасные шаблоны

Руководство GNU Bash говорит, что при set -e оболочка не завершается, если команда возвращает ненулевой код как часть проверки в if, в списке && или || не на последнем месте или если её статус инвертирован с помощью ! (GNU Bash: The Set Builtin). Используйте это.

Останавливает скрипт или скрывает реальный сбой

  • Голый diff a b при set -e (останавливается только из-за найденных различий)
  • diff a b || true (поглощает и «различия», и реальную ошибку (2))
  • || true после grep, из-за которого «нет совпадений» и «не удалось прочитать файл» выглядят одинаково

Обрабатывайте коды выхода отдельно

  • Сохраняйте код выхода в переменную и разветвляйте по 0, 1 и 2 (рекомендуемый шаблон)
  • Если нужно только знать, есть ли различия, if diff -q a b; then … подойдёт, но учтите, что 2 (ошибка) попадает в ту же ветку, что и «различия»
  • Если используете || true, сначала решите, допустимо ли пропустить 2 (реальную ошибку)

Вот пример, который различает 0, 1 и 2. Сбой в левой части || освобождён от errexit, поэтому скрипт не останавливается, а rc содержит код выхода.

#!/usr/bin/env bash
set -euo pipefail
 
rc=0
diff -rq deployed/ live/ || rc=$?
 
case "$rc" in
  0) echo "no differences" ;;
  1) echo "differences: review before continuing" ;;
  *) echo "diff failed" >&2; exit 2 ;;
esac

set -e не является полной страховкой. У него много исключений, которые могут скрывать ошибки, поэтому для важных ветвей проще самому читать код выхода и принимать решение (совет этого сайта).

После обновления: выполните те же проверки на каждом сервере

Обновление закрывает точку входа. Оно не говорит, проник ли кто-то до обновления. Для уязвимостей KEV CISA просит проверить наличие взлома до обновления (см. «Подтверждённая эксплуатация и добавление в KEV CISA» в статье о PHP). Чтобы проверки не отличались от сервера к серверу, оператор применил один и тот же чек-лист ко всем серверам.

ПунктЧто искатьПример команды
Ключи для входаПубличные ключи, которые вы не помните как добавлялиОткрыть authorized_keys
ПользователиНеизвестные пользователи, изменения в административных группахgetent passwd, getent group sudo
Изменённые файлы пакетовФайлы, установленные пакетом, которые изменились с момента поставкиdebsums -s (семейство Debian), rpm -Va (семейство RHEL)
Исполняемые файлы без владельца-пакетаНовые исполняемые файлы в /usr/local, /opt или в PATH, которые не принадлежат ни одному пакетуdpkg -S <file> (семейство Debian) или rpm -qf <file> (семейство RHEL); ищите файлы без пакета-владельца
SUIDНовые файлы, которые запускаются с правами владельцаfind / -xdev -perm -4000 -type f
cron и systemdНеизвестные запланированные задания и таймерыcrontab -l, /etc/crontab, /etc/cron.d, systemctl list-timers
/tmpИсполняемые файлы, размещённые тамfind /tmp /var/tmp -type f -perm /111
Исходящие соединенияСоединения с адресатами, которые вы не узнаётеss -tunp
Внутри контейнеровЗапущенные процессы и файлы, записанные после стартаdocker top, docker diff

crontab -l показывает только задания текущего пользователя, поэтому проверяйте /etc/crontab, /etc/cron.d и спулы cron для каждого пользователя отдельно. Чтобы ss -tunp показал процесс за каждым соединением, нужны права root. dpkg -S и rpm -qf сообщают, какой пакет владеет файлом; «нет пакета-владельца» может означать файл, который вы разместили сами, поэтому сверяйте его с тем, что, как вы помните, устанавливали.

find -perm -4000 находит файлы, у которых установлены все заданные биты (find(1)). docker diff перечисляет файлы, изменённые с момента создания контейнера (A: добавлен, C: изменён, D: удалён), а docker top показывает запущенные процессы (docker container diff, docker container top). Контейнеры обычно пересобирают, а не меняют, поэтому исполняемый файл, показанный docker diff, считайте неожиданным и разбирайтесь с ним (совет этого сайта).

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

Разрешите подозрительную запись в журнале сравнением файлов

Во время этой проверки команда обнаружила, что до обновления POST-запрос пришёл в контактную форму с Tor-узла выхода (точки выхода сети, которая скрывает отправителя). Журнал показывает только то, что запрос пришёл. Он не показывает, было ли что-то записано в результате.

Поэтому они сравнили живые публичные файлы с копией, сохранённой при развёртывании. Они совпали полностью, лишних файлов PHP не было. Эта проверка не нашла признаков того, что какой-либо файл был изменён.

1

Храните копию развёрнутого вне сервера

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

2

Сравните его с живыми файлами

Используйте diff -rq, чтобы найти различающиеся файлы, или сравните списки хешей по каждому файлу (sha256sum). Если что-то отличается, проверьте, было ли это изменение намеренным.

3

Ищите и добавленные файлы

Даже если содержимое совпадает, новый файл PHP отсутствует в исходной копии, поэтому его нужно искать отдельно. diff -rq также сообщает о файлах, которые есть только с одной стороны.

4

Совпадение не является доказательством безопасности

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

Взгляд этого сайта: составьте чек-лист до ночи экстренных работ

В спешке ночью то, что на каком сервере проверили, обычно различается. Если список пунктов определён заранее, каждый сервер проходит одинаковые проверки, и пробелы видны. Старый PHP в командной строке удалось найти именно потому, что проверка после обновления не ограничилась вебом.

Что сделать сегодня

  1. Если можете войти на сервер, выполните php -v и which php и укажите полный путь к PHP в заданиях cron
  2. Проверьте версию веб-PHP со стороны веба, например в Site Health (и удалите страницу phpinfo() сразу после использования)
  3. Найдите в скриптах с set -e вызовы diff, grep или cmp и решите, как обрабатывать код выхода 1
  4. Храните копию опубликованного вне сервера и сравнивайте её после обновлений
  5. Сделайте чек-лист проверки на взлом одинаковым на каждом сервере и проходите его пункт за пунктом

Источники

Читайте дальше

FAQ

QМожет ли PHP остаться старой версии после того, как я его обновил?
A

Да. PHP, который обрабатывает веб-запросы (PHP-FPM или модуль Apache), и PHP в командной строке (CLI), который вызывается из оболочки или cron, могут быть отдельными программами с разными версиями. php.net описывает, как PHP работает через SAPI, и указывает, что CLI SAPI во многом отличается от других. Веб-часть может быть обновлена, а PHP, которым пользуется cron, останется старым.

QКак проверить, какая версия PHP реально работает?
A

Для командной строки выполните php -v. Для веба проверяйте со стороны веба; в WordPress это Site Health на экране администратора. Если вы используете страницу phpinfo(), удалите её сразу после работы. which php показывает, какой исполняемый файл вызывается, а в системах в стиле Debian с несколькими версиями update-alternatives --display php показывает, куда указывает команда php. В заданиях cron полный путь к PHP устраняет сомнения в том, какой вариант запустится.

QПочему скрипт оболочки с set -e останавливается при использовании diff?
A

diff возвращает код выхода 1, когда находит различия. В руководстве GNU 0 означает отсутствие различий, 1 — различия, а 2 — проблему. set -e (errexit) останавливает скрипт при ненулевом коде, поэтому сам факт найденного различия его останавливает. Поместите команду в условие if или сохраните код выхода в переменную и обрабатывайте 0, 1 и 2 отдельно. grep (1 означает отсутствие совпадений) и cmp (1 означает различие) работают так же.

QКак решить, был ли я взломан, если запись в журнале выглядит подозрительно?
A

Один только журнал этого не решит. Журнал фиксирует попытку, которая пришла, но не обязательно успешную. Сравните живые публичные файлы с копией, которую вы сохранили при развёртывании, и проверьте, что они совпадают и что нет лишних файлов PHP. Совпадение позволяет сказать, что изменений файлов не обнаружено. Это не доказательство безопасности, поэтому сочетайте его с другими проверками.