Главная / Блог / Race Condition в веб-задачах CTF: воспроизводим через Burp и забираем флаг

14 мин.00

Race Condition в веб-задачах CTF: воспроизводим через Burp и забираем флаг

Race Condition в веб-задачах CTF: воспроизводим через Burp и забираем флаг

Race Condition в веб-задачах CTF: воспроизводим через Burp и забираем флаг

На CTF задача стоила 300 очков. Я час писал многопоточный скрипт на Python — requests, threading, asyncio. Ни одна попытка не попала в race window. Потом открыл Burp Repeater, сгруппировал двадцать вкладок с одинаковым запросом на применение купона, нажал Send group (parallel) — в ответах скидка применилась шестнадцать раз. Баланс аккаунта ушёл ниже цены товара, корзина оформилась, флаг в кармане. Три минуты вместо часа, потому что правильный инструмент бьёт правильный код. Дальше — пошаговая эксплуатация race condition в CTF-задачах через Burp Repeater и Turbo Intruder, от простого limit overrun до partial construction.

TOCTOU и race window: почему состояние гонки ломает веб-приложения

Race condition — класс уязвимостей, при котором сервер обрабатывает несколько запросов параллельно без синхронизации доступа к общим данным. В CTF веб-задачах это сводится к одному: между проверкой условия (check) и выполнением действия (use) есть временной промежуток — race window. В этот промежуток можно вклиниться вторым, третьим, двадцатым запросом. Подробнее — в нашем обзоре пентест веб-приложений.

Классическое название паттерна — Time-of-Check to Time-of-Use (TOCTOU). Сервер проверяет: «купон уже использован?» — нет. Применяет скидку. Обновляет базу: «купон использован». Между первым и третьим шагом проходят миллисекунды. Если в это окно прилетают ещё девятнадцать запросов с тем же купоном — все двадцать проходят проверку, потому что запись в базе ещё не обновлена. Приложение проваливается во временное состояние (sub-state), где купон формально ещё доступен, хотя обработка первого запроса уже пошла.

По классификации PortSwigger из whitepaper «Smashing the state machine» (Black Hat USA 2023), race condition в вебе делится на три паттерна — каждый встречается в CTF:

  • Limit overrun — обход ограничения на однократное действие (купон, бонус, голосование). Самый частый тип в CTF-задачах.
  • Single-endpoint collision — два запроса на один эндпоинт с разными данными конкурируют за общее поле в базе.
  • Partial construction — эксплуатация момента, когда объект уже создан в базе, но ещё не полностью инициализирован.

В терминах MITRE ATT&CK race condition при эксплуатации веб-приложения попадает под Exploit Public-Facing Application (T1190, Initial Access). На CTF это почти всегда начальный вектор: ломаем бизнес-логику, получаем доступ к ресурсу, забираем флаг.

Принципиальное отличие race condition от XSS, SQLi или SSRF: код приложения формально корректен. Проверки на месте, валидация работает. Проблема только в порядке и времени выполнения операций. Автоматические сканеры эту штуку не находят — в CTF она закономерно идёт как задача повышенной сложности.

Инструменты для эксплуатации race condition в CTF

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

Прежде чем атаковать race condition на CTF, проверьте готовность:

  • Burp Suite Professional 2023.9+ — с этой версии Repeater поддерживает Send group (parallel). Community Edition не умеет параллельную отправку нативно, но работает с Turbo Intruder.
  • Turbo Intruder — расширение из BApp Store. Установка: Extender → BApp Store → поиск «Turbo Intruder» → Install. Нужно базовое знание Python (Jython-синтаксис). Расширение активно поддерживается — обновление для single-packet attack вышло вместе с Burp 2023.9.
  • HTTP/2 на целевом сервере — для single-packet attack (самый надёжный метод). При HTTP/1.1 Burp переключается на last-byte synchronization — менее точный метод, но рабочий.
  • ОС: любая с Java 17+ (для Burp). RAM: от 4 ГБ (Burp + браузер + расширения). Сеть: подключение к CTF-платформе, стабильный канал без прокси-фильтрации HTTP/2.

Burp Repeater или Turbo Intruder: когда что применять

Выбор инструмента зависит от сложности задачи. Вот decision tree, который экономит время на соревнованиях:

Критерий Burp Repeater Turbo Intruder
Порог входа Минимальный — GUI Средний — Python-скрипт
Количество запросов До 20–30 в группе Сотни и тысячи
Single-packet (HTTP/2) Нативно Нативно через Engine.BURP2
Last-byte sync (HTTP/1) Нативно Через Engine.THREADED
Разные запросы одновременно Да — разные вкладки в группе Да — через переменные
Многоэтапная координация Нет Да — несколько gates
Типичная задача Простой limit overrun Partial construction, multi-step

Правило: начинайте с Repeater. Если за 5–10 попыток limit overrun не сработал — Turbo Intruder. Если задача требует координации разных запросов (регистрация + подтверждение, смена email + перехват токена) — сразу Turbo Intruder.

Limit overrun через Burp Repeater: пошаговая атака на CTF веб-задачу

[Применимо: CTF-задачи, внешний пентест. Работает если: эндпоинт принимает повторные запросы без серверного лока или atomic-операции в базе данных.]

Limit overrun — самый распространённый паттерн race condition в CTF. Типичный сценарий: интернет-магазин, купон на скидку, ограничение «использовать один раз». Задача — применить купон многократно и купить товар дешевле баланса аккаунта. По данным разбора аналогичной лаборатории от YesWeHack, при эксплуатации промокода PROMO20 на товар стоимостью 1337 евро single-packet attack позволил снизить цену до 37.62 евро — купон применился достаточное количество раз, чтобы уложиться в баланс аккаунта (50 евро).

Шаг 1. Найти уязвимый эндпоинт. Через Burp Proxy перехватите запрос на применение купона. Обычно это POST на /apply-coupon, /redeem, /discount или аналог. Запомните параметры: код купона, ID сессии, ID корзины.

Шаг 2. Отправить в Repeater и проверить. Правый клик на запрос в Proxy History → Send to Repeater. Отправьте один запрос — убедитесь, что купон применяется. В ответе должна измениться цена или появиться подтверждение.

Шаг 3. Создать группу вкладок. Дублируйте вкладку 19 раз (Ctrl+R или правый клик → Duplicate tab). Итого 20 вкладок. Выделите все → правый клик → Add tabs to group → Create new group.

Шаг 4. Отправить параллельно. Выпадающая стрелка рядом с Send → Send group (parallel). Burp автоматически выберет технику: single-packet attack для HTTP/2 или last-byte synchronization для HTTP/1.1. Все 20 запросов уходят одновременно.

Шаг 5. Прочитать ответы. Пройдитесь по вкладкам. Если атака сработала — несколько ответов покажут успешное применение купона (одинаковый код 200 с изменённой ценой). При limit overrun из 20 запросов обычно 5–15 проходят проверку.

Почему двадцать запросов, а не два? Теоретически для race condition достаточно двух параллельных запросов. На практике серверная задержка (server-side jitter) делает попадание двух запросов в одно race window маловероятным. Двадцать запросов компенсируют разброс: даже если серверная обработка добавляет 1–2 мс дисперсии, из двадцати хотя бы несколько попадут в окно. На этапе разведки 20–30 запросов — оптимальное число.

Вариации limit overrun в CTF-задачах, помимо купонов: погашение подарочной карты несколько раз, многократное голосование, вывод средств сверх баланса, повторная активация инвайт-кода, обход CAPTCHA-лимита.

Turbo Intruder и single-packet attack: параллельные HTTP-запросы под контролем

Когда Repeater недостаточно — нужно больше запросов, координация между эндпоинтами или многократные попытки с разными параметрами — переходим на Turbo Intruder.

Механизм gate. Turbo Intruder использует концепцию «ворот» (gates). Запросы ставятся в очередь с привязкой к именованному gate. Пока gate закрыт, запросы удерживаются — Burp не отправляет их на сервер. Вызов engine.openGate('name') отправляет все запросы из gate одновременно. При HTTP/2 они упаковываются в один TCP-пакет — сетевой jitter полностью исключён.

Пошаговая настройка: в Burp перехватите запрос → правый клик → Extensions → Turbo Intruder → Send to Turbo Intruder. Выберите шаблон examples/race-single-packet-attack.py и адаптируйте под задачу.

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2)
    for i in range(20):
        engine.queue(target.req, gate='race1')
    engine.openGate('race1')

def handleResponse(req, interesting):
    table.add(req)

Ключевые параметры: concurrentConnections=1 — одно соединение, чтобы все запросы ушли в одном пакете. engine=Engine.BURP2 — HTTP/2 single-packet attack. Если цель поддерживает только HTTP/1, замените на Engine.THREADED — Turbo Intruder переключится на last-byte synchronization. Автоопределения нет, указывать вручную.

Чтение результатов. После нажатия Attack в таблице результатов отсортируйте по Status или Length. Ответы с отличающимся размером тела или кодом статуса — кандидаты на успешную эксплуатацию. При limit overrun ищите несколько ответов с кодом 200 и телом, подтверждающим применение действия.

Ограничение: single-packet attack работает исключительно с HTTP/2. Если сервер поддерживает только HTTP/1.1, last-byte synchronization даёт менее точную синхронизацию. Может потребоваться 50–100 попыток вместо 5–10. На CTF-платформах типа PortSwigger Web Security Academy HTTP/2 поддерживается; на самодельных CTF-стендах — не факт.

Partial construction: гонка с токеном верификации в CTF

[Применимо: CTF-задачи с регистрацией, подтверждением email, двухэтапной валидацией. Работает если: между созданием записи в базе и записью токена есть временной промежуток.]

Partial construction — самый изящный паттерн race condition уязвимости в CTF. Суть: когда сервер создаёт нового пользователя, есть момент, когда запись уже в базе, но токен подтверждения ещё не записан. Поле токена содержит null (или пустую строку, или дефолтное значение). Если одновременно с регистрацией отправить запрос на подтверждение с пустым токеном — он может совпасть с неинициализированным значением.

Лаборатория PortSwigger «Partial construction race conditions» демонстрирует этот вектор. Нужно зарегистрировать аккаунт с email на недоступном домене, обойдя верификацию. Разведка показывает: в JS-файле /resources/static/users.js утекает эндпоинт подтверждения POST /confirm?token=.... Экспериментируем с параметром: произвольный токен даёт Incorrect token, пустой параметр — Forbidden, а пустой массив token[]= — ответ Invalid token: Array. Пустой массив проходит проверку типа и может совпасть с неинициализированным значением в базе.

Эксплуатация требует координации: POST /register и POST /confirm?token[]= должны попасть в окно между созданием записи и генерацией токена. Turbo Intruder здесь незаменим:

def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2)
    confirmReq = '''POST /confirm?token[]= HTTP/2
Host: TARGET.web-security-academy.net
Content-Length: 0

'''
    for attempt in range(20):
        username = 'User' + str(attempt)
        engine.queue(target.req, username, gate=str(attempt))
        for i in range(50):
            engine.queue(confirmReq, gate=str(attempt))
        engine.openGate(str(attempt))

Каждая итерация отправляет регистрацию с уникальным username и 50 подтверждений с пустым токеном. Из 20 попыток хотя бы одна попадает в race window. Успешный ответ — 200 с сообщением Account registration for user UserN successful.

Почему 50 запросов confirm? Подтверждение обрабатывается быстрее регистрации (меньше серверных операций). Массовая отправка повышает вероятность попадания хотя бы одного confirm в момент после создания записи, но до записи токена. Confirm «быстрее» register — нужно компенсировать разрыв количеством.

Single-endpoint collision: TOCTOU уязвимость на примере CVE-2022-4037

[Применимо: CTF-задачи со сменой email, сбросом пароля, привязкой телефона. Работает если: промежуточные данные хранятся в общем поле без row-level locking.]

Single-endpoint collision отличается от limit overrun: два запроса не повторяют действие, а конкурируют за одно поле с разными данными.

Показательный реальный пример — CVE-2022-4037 в GitLab CE/EE (все версии до 15.5.7, с 15.6 до 15.6.4, с 15.7 до 15.7.2). По данным NVD, CVSS 6.4 (MEDIUM), вектор CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N. Классификация — CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization). Race condition позволяла подделать верифицированный email и захватить сторонний аккаунт при использовании GitLab как OAuth-провайдера.

Вектор атаки: Network (AV:N), сложность атаки низкая (AC:L), нужны минимальные привилегии (PR:L — валидный аккаунт), взаимодействие пользователя не требуется (UI:N). Scope Changed (S:C) — влияние выходит за границы уязвимого компонента (захват стороннего аккаунта через OAuth).

Механика: атакующий отправляет два параллельных запроса на смену email. Первый — new_email = attacker@evil.com, второй — new_email = victim@target.com. Оба потока пишут в поле pending_email. Второй поток перезаписывает значение. Первый поток читает pending_email для генерации токена — а там уже victim@target.com. Но письмо отправляется на адрес из параметра запроса — attacker@evil.com. Итог: атакующий получает токен, привязанный к чужому email.

В CTF этот паттерн встречается в задачах со сменой email, сбросом пароля, привязкой телефона. Для воспроизведения в Burp Repeater: создайте группу из двух вкладок с запросами на один эндпоинт, но с разными email, отправьте параллельно. Повторите 10–15 раз — коллизия произойдёт. Ключевой индикатор: если при последовательной отправке двух запросов первая ссылка подтверждения становится невалидной — сервер хранит только один pending_email, и коллизия возможна.

Когда race condition не срабатывает: ограничения техники

Не каждая CTF-задача с параллельными запросами — race condition. Конкретные ситуации, когда техника бессильна:

Session-based locking. PHP с file-based sessions, ASP.NET с сессионным state — фреймворк блокирует параллельную обработку запросов одной сессии. Признак: из 20 параллельных запросов все возвращаются с нарастающими временными метками, как при последовательной отправке. Обход: использовать разные сессии (несколько аккаунтов) или найти эндпоинт без привязки к сессии.

Database-level atomicity. SELECT ... FOR UPDATE, serializable isolation level, хранимые процедуры с explicit lock — race window отсутствует. В CTF это встречается в задачах-обманках: race condition выглядит возможным, но не эксплуатируется. Если после 30 попыток ни один запрос не проскочил — вероятно, это тот случай.

Rate limiting. После 5–10 быстрых запросов сервер возвращает 429 Too Many Requests. Решение: уменьшить количество параллельных запросов или добавить задержку между группами (в Turbo Intruder — time.sleep() между openGate()).

HTTP/1.1 без HTTP/2. Single-packet attack невозможен. Last-byte synchronization менее точна — race window должно быть достаточно широким (десятки миллисекунд). На самодельных CTF-стендах без HTTP/2 может потребоваться в 10 раз больше попыток.

Decision tree — стоит ли искать race condition:

  1. Есть лимит на действие (одноразовый купон, один голос, один перевод)? Да → пробуйте limit overrun.
  2. Есть двухэтапный процесс с общим состоянием (регистрация + подтверждение, запрос + верификация)? Да → partial construction или single-endpoint collision.
  3. Сервер возвращает разные ответы при последовательной отправке одного запроса дважды (первый — успех, второй — ошибка)? Да → есть состояние для атаки.
  4. Ответы идентичны при любом количестве повторов? Race condition маловероятен — ищите другой вектор.

Методология поиска race condition в CTF веб-задачах: predict, probe, prove

По методологии PortSwigger, поиск race condition состоит из трёх этапов.

Predict (предсказать коллизию). Изучите функциональность приложения. На CTF обращайте внимание на: системы баланса и внутренней валюты, промокоды и инвайт-коды, регистрацию с подтверждением, голосования и рейтинги, любые действия с пометкой «один раз». Видите форму «Введите код купона» или «Активируйте бонус» — это первый кандидат.

Probe (прощупать поведение). Отправьте запрос и зафиксируйте ответ. Отправьте повторно — сравните. Если ответы отличаются (первый — успех, второй — ошибка «уже использовано»), есть состояние, за которое можно конкурировать. Теперь отправьте группу из 5–10 запросов параллельно через Repeater и сравните с последовательной отправкой. Любые отличия в количестве успешных ответов — индикатор race condition.

Prove (доказать эксплуатируемость). Увеличьте параллельные запросы до 20–30, добейтесь стабильного воспроизведения. На CTF достаточно одного удачного прогона — нужен флаг, а не стабильный эксплойт для production.

В полной цепочке атаки race condition — чистая exploitation бизнес-логики. На внешнем пентесте уязвимость эксплуатируется чаще post-authentication: аккаунт есть, через гонку обходятся ограничения. В CTF цепочка короче: получить аккаунт → эксплуатировать race condition → получить флаг. Reconnaissance сводится к анализу JS-файлов и поведения эндпоинтов, а post-exploitation обычно не требуется — флаг выдаётся как результат сломанной бизнес-логики.

Восемь из десяти веб-задач на race condition, которые я решал на CTF за последние два года, были limit overrun. Простые, прямолинейные, решаемые за минуты через Burp Repeater. При этом на соревнованиях среднего уровня их решает треть участников — остальные не пробуют, потому что race condition кажется «ненадёжной» техникой, зависящей от удачи.

Это заблуждение. Single-packet attack через HTTP/2 убирает сетевой jitter полностью — все запросы приходят из одного TCP-пакета. Это не вероятностная атака, а детерминированное попадание в race window. Если окно существует — двадцать запросов его накроют. Настоящая сложность начинается с partial construction и multi-endpoint collision, где нужно координировать запросы с разными скоростями обработки на сервере. Но в CTF такие задачи — редкость и всегда стоят максимальных очков.

На каждом CTF с веб-категорией первым делом ищите одноразовые действия. Купон, инвайт, голосование, бонус — если действие ограничено «один раз», отправьте двадцать запросов параллельно. В худшем случае потеряете минуту. В лучшем — заберёте флаг раньше остальных. Если хочешь не один writeup, а системную отработку таких цепочек с лабой на каждый вектор — на WAPT в Codeby разбирают эту механику в отдельных модулях.

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

Поделиться

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

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

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