Как PHP2Shell съел мой почтовый ящик
Одно письмо в 13:06, патч в 17:00 только для PHP 8.2 и новее, и 500 клиентских сайтов на 25 с лишним серверах, 10 облачных провайдерах и куче панелей управления, которые я даже не хочу считать.

За неделю до всего этого я наконец сделал то, что откладывал целый год: маленький скрипт, который читает ленты по безопасности, выкидывает всё, что не касается серверов и плагинов, за которые я отвечаю, и присылает мне на почту остаток. Я гордился им, как гордятся полкой, которую сам повесил. Ровная, из настоящего дерева, выдержит небольшого слонёнка, и никто, кроме меня, её не заметит.
За первую неделю он прислал два письма про плагин для WordPress, которым, по-моему, пользуются человека три. Я почувствовал себя очень ответственным и забыл про него.
21 октября в 13:06 он прислал третье.

Patchstack сообщал о двух CVE, и в строке с пакетом не было ни плагина, ни темы. Там было написано “PHP core”. Весь язык целиком. Кто-то уже успел назвать это PHP2Shell, а такое хорошо не кончается.
Баги, у которых бренд появляется раньше патча, обычно портят мне неделю.
Сначала кофе
Я не поверил. Моему скрипту было семь дней от роду. Куда вероятнее было, что я накосячил с фильтром и он орёт на пустом месте. Поэтому я сделал то, что делает любой разумный инженер со страшным письмом, которому не доверяет: оставил его открытым на экране и пошёл варить кофе.
Меня не было минут шесть. Я вернулся с полной кружкой к одиннадцати новым письмам и двум пропущенным.
Скрипт был в порядке. Баг был настоящий. Удалённое выполнение кода в той части PHP, которая обрабатывает веб-запросы, а это, по сути, всё: PHP-FPM, mod_php, php-cgi, LiteSpeed, FrankenPHP. Если оно запускает PHP и смотрит в интернет, оно в деле.
Письма шли и шли, тем самым тоном, каким пишут люди, которые уже переслали что-то начальству и теперь им нужно, чтобы отвечал кто-то другой. Аналитики, сисадмины, проджекты и CEO, и все вставляли какой-нибудь бюллетень или статью, которая по сути говорила “нас касается??”. Чей-то бухгалтер, клянусь, спрашивал, не то ли это, что по новостям.
Меня это рассмешило. Годами всем было глубоко плевать на версии PHP и на уязвимости вообще. Можно звонить, писать, прислать ссылку на работающий эксплойт, и всё равно тебе не дадут и часа на патч, потому что кому какое дело? А потом это попадает в новости, и внезапно это главный приоритет у всех.
И под всем этим один вопрос: когда починят?
Исправление не торопилось
Пока нет. Детали утекли раньше, чем команда PHP была готова, и патч назначили на 17:00 по моему времени. Четыре часа, когда все уже знают, а сделать почти ничего нельзя.
Четыре часа, на бумаге. Запомните эту часть.
Некоторые правительства даже не пытались. Финский государственный IT-провайдер Valtori просто выключил госсайты до выхода исправления.

Сначала я прогнал бюллетень через окно чата и попросил две строки: что уязвимо, что исправлено. Потом пошёл на php.net и нашёл оригинальный текст. ИИ в тот день никакие серверы чинить не собирался, а я не собирался давать ему root посреди живого инцидента. Ему достался скучный средний слой.
В оригинале был список версий. 8.2, 8.3, 8.4, 8.5.
Это был весь список. 8.1, 8.0, 7.4 и всё, что старше, уже вне поддержки, так что, похоже, мне конец. Команда безопасности выложила на GitHub минимальные бэкпорты для 7.4, 8.0 и 8.1, что прекрасно, если вы сами собираете PHP. Я не знаю ни одного клиента, который так делает. Большинство из них не скажут, какой у них PHP, и, как выяснилось в тот день, я тоже не мог.
Почему меня это задело сильнее, чем должно было
У меня есть основная работа разработчиком, а ещё я консультирую другие компании. Некоторые из них - друзья с сайтом на WordPress и мечтой. Некоторые я здесь назвать не могу.
Бизнесы совсем разные, но почти у всех есть две общие черты. Они работают в рабочие часы, так что патч выйдет ровно тогда, когда все, кто умеет его ставить, идут домой. И многие не могут просто поднять версию PHP: то плагин 2019 года, который умирает на восьмёрке, и плагин, который зависит от этого плагина, то панель хостинга, которая предлагает то, что предлагает, то “парень, который настраивал сервер”, ушедший в 2021-м вместе с паролем от root (серьёзно).
Так что речь шла не об одном сервере. Обо всех серверах сразу, и ставить пока было нечего.
Инвентаризация, которая, как мне казалось, у меня есть
Я открыл Obsidian на одном экране и свою недоделанную серверную панель на другом, потому что именно так я паникую организованно. Через десять минут до меня дошло, что я на самом деле не знаю, что где крутится. Моя панель знает про 15 серверов. Обслуживаем мы где-то от 25 до 30. Точного числа не знает никто.
Так устроено в большинстве фирм, с которыми я работаю. Нет DevOps, нет QA, обычно нет сеньора. Данные о серверах лежат в какой-нибудь заметке в monday.com, счёт за сервер, который давно никому не нужен, приходит каждый месяц, а вся работа идёт от пожара к пожару. Я примерно знал, что у нас есть. А “примерно” не пропатчишь.
Когда я всё сложил:
- около 500 клиентских сайтов
- от 25 до 30 серверов, смотря кого спросить
- 10 облачных провайдеров
- панелей управления больше, чем серверов
Последняя строчка не опечатка. Где-то одна панель, где-то две, а на одном сервере кто-то наполовину удалил панель, бросил это дело и настроил nginx руками поверх того, что осталось. В остальном это cPanel/WHM, Plesk, DirectAdmin, пара серверов на CloudPanel, Elementor Hosting (да, такое существует), Cloudways и шаред-хостинг, где у меня только FTP-логин и надежда.
Управляемые хостинги - отдельный квест. PHP там патчишь не ты, а они. Но сделают ли, и когда? Нужно ли им сначала твоё разрешение? Переведут ли они тебя на 8.2 сами или оставят на 7.4, потому что ты выбрал её в 2020-м? Все эти вопросы вспоминаешь задать провайдеру ровно в тот момент, когда проблема уже случилась.
Каждый из них ставит PHP по-своему, обновляется по своему календарю и прячет конфиги в свою любимую папку. Мне нужен был один ответ для каждого сайта: какой PHP на самом деле его обслуживает?
php -v - не ответ
Если вынесете из этого поста одну вещь, пусть будет эта.
$ php -vPHP 8.3.12 (cli)Отлично, 8.3, патч получим, расходимся. Только это PHP командной строки, тот, который ваш терминал нашёл первым. Сайт его мнения не спрашивал.
На большинстве серверов с панелями стоят несколько версий PHP рядом, и каждый сайт выбирает свою из выпадающего списка, в котором кто-то кликнул много лет назад. Терминал может говорить 8.3, а половина сайтов на том же сервере тихо работает на 7.4. Спрашивать сервер, какой у него PHP, - всё равно что спрашивать многоэтажку, какого цвета у неё стены. Каждая квартира красила по-своему.
Поэтому я перестал спрашивать серверы и начал спрашивать сайты. Они охотно рассказывают, если знать, где смотреть:
- WordPress: Инструменты → Здоровье сайта → Информация → Сервер. Версия PHP прямо там, без всякого shell. Большинство моих 500 сайтов на WordPress, так что это сделало основную работу.
- cPanel: MultiPHP Manager, одна таблица, все домены, у каждого версия. (Есть и способ через командную строку, про который я уже писал. В тот день он отбился с лихвой.)
- Plesk: для каждого домена, в настройках PHP. У DirectAdmin и CloudPanel то же самое, только под другими названиями и в других местах, а как же иначе.
- Управляемые хостинги вроде Cloudways и Elementor Hosting: в их панели, если, конечно, найдёшь от неё логин.
Для всего остального, шаред-хостингов с FTP-логином и надеждой, я закидывал однострочный PHP-файл, который печатает свою версию, открывал его в браузере и тут же удалял. Некрасиво, быстро, готово.
Эти одноразовые скрипты писал ИИ: один под CLI cPanel, вариант под Plesk и простой PHP-файл для хостов, где есть только FTP. Я бы написал все три рано или поздно. “Поздно” было единственным, чего у меня не было.
К 15:30 у меня был настоящий список, и он разложился на три кучи.
Три кучи
| Куча | PHP | План на 21-е |
|---|---|---|
| Счастливчики | 8.2+ | Обновить в 17:00, перезапустить FPM, проверить, что сайт открывается |
| Может быть | 8.0–8.1 | Проверить на 8.2 в staging, поднять, когда выйдет исправленная сборка |
| Некрасивая куча | 7.4 и старше | Сегодня без исправления. Сделать из них цель похуже |
Ещё до всего этого я отложил примерно четверть списка: сайты на PHP в районе пятой версии и старые кастомные сборки WordPress. Их за день никто бы не обновил, так что они сразу пошли по плану некрасивой кучи.
Счастливчики (8.2+). План у них был скучным в лучшем смысле. Сложным было быть готовым к 17:00, потому что половина этих серверов принадлежит компаниям, где к тому часу уже никого нет. Я потратил день, собирая разрешения трогать серверы, к которым меня обычно не подпускают. Часть владельцев успела согласиться. Остальные увидят это утром, а их сайты на ночь получили обработку некрасивой кучи.
Может быть (8.0 и 8.1). Достаточно близко к 8.2, чтобы поднятие версии могло сработать. Могло. Где у сайта были тесты, я их запускал, и случалось это примерно так часто, как вы думаете. Где не было, переключал staging-копию на 8.2 и кликал по ней как разъярённый клиент. Deprecation-предупреждения повсюду, но большинство выжило и встало в очередь на переезд, как только исправленная сборка появится в панели. Те, что не выжили, ушли в некрасивую кучу.
Некрасивая куча (7.4 и старше). Не сдвинуть без переписывания, на панелях, которые 7.4 собирать не будут, у владельцев, которые не одобрят это переписывание без часового созвона и письма от CEO. Плюс отложенная четверть, сайты на 8.x, упавшие на staging, и те, кто ещё ждал одобрения. Ничто здесь не успевало починиться до 17:00, так что задачей было сделать из них цель похуже:
- Cloudflare спереди везде, где его ещё не было, и managed-правила для этой CVE, как только они появились. Ничего не чинит. Покупает время.
- Закрыть всё, чему нечего делать в публичном доступе. Старые
phpinfo.php, забытые админки, папки/test/. Я нашёл больше, чем мне удобно признать. - Смотреть логи. Большая часть того, что прилетало на мои серверы в тот вечер, была прощупыванием, а не настоящей эксплуатацией, и это совпадало с тем, что видел Patchstack. Но “большая часть” - не “всё”, а другие сообщали и о настоящих атаках.
- Сказать владельцу правду, письменно. Вы на версии, которую никто не патчит. Вот что я сделал сегодня, вот сколько стоит настоящее исправление, вот дата, к которой мне нужен ответ.
Последний пункт не технический, и важнее него ничего нет. Временное решение без письменного “это временно” тихо становится постоянным. Я видел, как это происходит. Я сам так делал.
Я написал одну честную версию этого письма и попросил ИИ подогнать её под каждого клиента, с его собственным “вот что я сделал сегодня ночью на вашем сервере”. К тому моменту я уже слишком выгорел, чтобы сам нормально сформулировать абзац “это временно, вот во что обойдётся настоящее исправление”, а это было самое важное из всего, что я отправил за день.
17:00
На бумаге это был финиш. Патч вышел вовремя: 8.2.35, 8.3.36, 8.4.27, 8.5.12.
А потом какое-то время ничего не происходило, потому что патч на php.net - это не патч на вашем сервере. Дистрибутивы и панели собирают и выпускают свои пакеты, каждый по своим часам. И управляемые хостинги, о которых я переживал раньше, оказались как раз быстрыми:
| Откуда берётся PHP | Исправленная сборка |
|---|---|
| Управляемые хостинги: uPress, Cloudways, Elementor Hosting | За несколько часов. Мне делать ничего не пришлось |
| Панели на серверах, за управление которыми мы платим: cPanel/WHM, DirectAdmin, CloudPanel | Не раньше следующего дня. Некоторые выпускают до сих пор |
Панели делают простые 95% простыми, а в тот единственный день, когда это важно, ты ждёшь чужого графика релизов. Это нормальная сделка. Просто знай, что ты её заключил.
На основной работе исправленные сборки для большинства наших серверов пообещали первым делом следующим утром. Те, что ещё ждали, получили правило в Cloudflare и строчку в календаре на следующий день, и около 18:00 делать там было уже нечего, кроме как ждать.
Телефон затих. Все ушли домой.
У серверов моих частных клиентов никакого обещания на утро не было, так что ими я продолжил заниматься. Около 01:00 я спрашивал ИИ, достаточно ли 8.1 близка к 8.2, чтобы рискнуть слепым апгрейдом. Тут я и закончил.
Обещанные утренние сборки пришли не все. Пока я пишу это 25 октября, через четыре дня после патча, часть тех серверов всё ещё ждёт своих. Вот вам и четыре часа.
Кофе так и стоял на столе, полный и ледяной.
PoC
Тогда рабочий proof of concept (PoC) был слухом. @lcfr_hax одним из первых показал его на видео, и картинка в начале поста - кадр оттуда: локальный WordPress на уязвимом PHP, эксплойт роняет безобидный pwned.txt, а браузер рядом показывает, что файл появился. Замените pwned.txt на веб-шелл, и вот оно, настоящее.
Payload я сюда вставлять не буду. Рабочий эксплойт PHP2Shell в посте помогает ровно одному типу читателей, и это не вы. Хватит и того, как он, по слухам, устроен: use-after-free глубоко в том, как PHP убирает за собой после запроса. Интерпретатор освобождает кусок памяти, а потом снова к нему обращается, и особым образом собранный запрос решает, что там будет лежать в этот момент. Один запрос к любому PHP-эндпоинту, без логина, и у атакующего выполнение кода.
Сообщений о том, что так реально взломали сайт, я не видел. Сообщений о вероятном PoC видел полно.
То, что определило, как пройдёт моя неделя, - это разрыв между “PoC существует” и “мои клиенты запатчены”, и успею ли я его закрыть раньше, чем туда доберётся кто-то с PoC на руках. Те же инструменты, что позволили мне разобрать 500 сайтов за день, ровно так же работают и на них.
Почему ничего не изменится
Большинство агентств похожи на свет в том самом физическом эксперименте: ведут себя прилично, только когда есть наблюдатель. В среду смотрел весь мир, и все вдруг озаботились версиями PHP. На следующей неделе всем будет плевать. Письма “нас касается??” прекратятся, а сайты на 7.4 снова станут строчкой на совещании по бюджету, которую каждый раз переносят на следующий квартал.
Прилично они себя не ведут, потому что настоящее исправление стоит денег и времени. Wordfence, Kinsta и Patchstack (тот самый, что меня предупредил) каждый продаёт реальный, полезный кусок. Файрвол не обновит ваш PHP. Managed-хостинг не перепишет плагин 2019 года, который умирает на 8. Лента уязвимостей скажет, что вы уязвимы, но не как протащить сайт с WordPress 4.9 на 7.1.3, не уронив по дороге оформление заказа.
Цена для одного сайта на WordPress:
- Одна проверка (сервер, пользователи, база, плагины, кастомный код): четыре-пять честных часов, около $400 один раз.
- Оставаться в безопасности: гонять эту проверку каждый месяц, ближе к $1000 в месяц.
- С поддержкой, накопившимися доработками и хостингом сверху: две, а то и три тысячи в месяц.
На живом сайте, который катит изменения каждую неделю, эта математика работает. На среднем сайте это продать сложно. А фирмы, которые могут оплатить настоящую команду безопасности, получают другую проблему: скорость. Согласование, выделенное окно, код-ревью, прогон QA. Пока “обновить PHP на проде” пройдёт через всё это, фора в четыре часа, что была у атакующих, давно в прошлом. Либо слишком бедный, чтобы быть защищённым, либо слишком медленный, чтобы быть эффективным. Почти все - одно из двух.
Готов к следующему
Годами я говорил клиентам “нам бы обновить PHP” тем тоном, каким говорят “вам бы зубной нитью пользоваться”. После среды я вместо этого держу такой список:
- Система уведомлений. Ленты по безопасности, отфильтрованные под серверы и плагины, за которые вы отвечаете. Моя окупилась за неделю: я узнал об этом раньше клиентов.
- Инвентаризация, которая спрашивает сайты, а не
php -v. Сайт, сервер, панель, версия PHP, кто платит по счёту. У меня это теперь cron, который каждую ночь спрашивает каждый сервер, до которого я дотягиваюсь, какой PHP у каждого сайта, и записывает всё в один файл. В следующий раз первый вопрос займёт десять секунд, а не два часа. - План, написанный сейчас. Что проверяете первым, в каком порядке и что значит “готово”. План, написанный в 13:10 в тот самый день, - не план. Мой чек-лист безопасности WordPress - хорошее начало.
- Бэкапы, из которых вы хотя бы раз реально восстанавливались.
- Письменные разрешения заранее. “Да, давай, делай что нужно” стоит гораздо больше спокойным вторником, чем в 16:55.
- Порядок эскалации. Кто кому звонит и кто имеет право сказать “выключайте сайт”, когда больше никто не отвечает.
А если вы всё ещё на 7.4: бэкпорт лежит на GitHub, и кто-то из ваших знакомых умеет его собрать. Попросите вежливо. Сводите на ужин.
Комментарии
Комментарии живут в GitHub Discussions (через giscus), так что для участия нужен аккаунт GitHub.