Главная / Блог / Race Condition в CTF: от ручного Repeater до Turbo Intruder

15 мин.00

Race Condition в CTF: от ручного Repeater до Turbo Intruder

Race Condition в CTF: от ручного Repeater до Turbo Intruder

Race Condition в CTF: от ручного Repeater до Turbo Intruder

Товар за 1337 евро, на счету 50 — промокод PROMO20 даёт скидку 20%, но этого категорически мало. Single-packet attack через Burp Repeater: двадцать одинаковых запросов на применение купона уходят в одном TCP-пакете, купон срабатывает многократно, итоговая цена падает до 37.62 евро. Воспроизводимый результат из лаборатории PortSwigger по limit overrun, описанный в разборе YesWeHack — три минуты на эксплуатацию, ноль строк кастомного кода. Race condition в CTF — одна из немногих уязвимостей, где формально корректное приложение ломается из-за тайминга, а не из-за ошибки валидации. Код правильный, проверки на месте, а оно всё равно ломается. Дальше — пошаговая эксплуатация гонки состояний в веб-задачах: от ручной отправки в Repeater до автоматизации race condition атак через Turbo Intruder, включая паттерны, которые русскоязычные разборы обычно не покрывают: multi-endpoint гонки, partial construction и time-sensitive атаки.

Что ломается при race condition: TOCTOU и три паттерна уязвимости

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

Приложение переходит во временное состояние (sub-state): купон формально доступен, хотя обработка первого запроса уже идёт. Ключевое отличие гонки состояний от XSS или SQLi: код корректен, валидация на месте, проверки работают. Проблема только в порядке и тайминге операций. Автоматические сканеры этот класс не находят — ни nuclei, ни Burp Scanner, ни ZAP. Ни один из них даже не пытается.

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

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

Hidden multi-step sequences — скрытые многошаговые процессы. Один HTTP-запрос запускает цепочку операций на сервере: создание записи, генерация токена, обновление статуса. Между шагами возникают sub-state, в которые можно вклиниться запросом к другому эндпоинту. Этот паттерн лежит в основе multi-endpoint race conditions.

Partial construction — эксплуатация незавершённых объектов. При создании пользователя или токена есть момент, когда запись уже в базе, но поле токена ещё содержит NULL или пустую строку. Запрос на подтверждение с пустым значением может совпасть с неинициализированным полем. Самый изящный из трёх.

Race window при limit overrun обычно составляет 1–5 мс. При partial construction — доли миллисекунды. Чем короче окно, тем точнее должна быть синхронизация и тем важнее выбор инструмента.

Место race condition в цепочке атаки

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

Зачем атакующему это нужно: race condition позволяет обойти финансовые ограничения (купить товар дешевле), получить привилегии (подтвердить чужой email), или выполнить действие, которое бизнес-логика запрещает (многократно использовать одноразовый ресурс). В багбаунти классифицируется по OWASP Top 10 A01:2021 — Broken Access Control.

Цепочка в контексте CTF-задачи:

  1. Разведка — изучение эндпоинтов через Burp Proxy, поиск операций с общим состоянием (баланс, купоны, токены)
  2. Идентификация race window — анализ, какие операции выполняются неатомарно (predict — probe — prove, методология PortSwigger)
  3. Эксплуатация race condition (текущий шаг) — отправка параллельных запросов через Repeater или Turbo Intruder
  4. Пост-эксплуатация — использование полученного преимущества: покупка товара, доступ к закрытому функционалу, получение флага

Пример из продакшна: CVE-2022-4037 в GitLab CE/EE (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) при обработке email-верификации позволяла подделать подтверждённый email и захватить чужие аккаунты через OAuth. Затрагивала все версии GitLab до 15.5.7, с 15.6 до 15.6.4 и с 15.7 до 15.7.2. По данным CISA-ADP, эксплуатация в дикой природе не зафиксирована (SSVC decision: Track), но технический импакт — partial. Паттерн атаки — тот же partial construction, что и в лабораториях PortSwigger. Лабораторная задачка — а в продакшне GitLab лежит ровно тот же баг.

Limit overrun через Burp Repeater: single-packet attack по шагам

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

  • Burp Suite Professional 2023.9+ — параллельная отправка в Repeater доступна только в Pro-версии (Turbo Intruder работает и в Community Edition)
  • Цель с поддержкой HTTP/2 — для single-packet attack. Если цель работает только по HTTP/1.1, Burp автоматически переключится на last-byte synchronization (менее точная синхронизация)
  • Стабильное соединение с CTF-платформой — без дополнительных прокси-слоёв, минимальный сетевой jitter
  • RAM: от 2 ГБ свободной для Burp (Java-приложение, при активных расширениях потребление растёт)

Предусловия и ограничения

Работает если: эндпоинт принимает повторные запросы без серверной блокировки, операция в БД не выполняется атомарно (нет SELECT ... FOR UPDATE, нет advisory lock), отсутствует session-based locking.

Не работает если: фреймворк сериализует запросы по сессии (PHP с дефолтным session handler блокирует параллельную обработку одной сессии), сервер проверяет idempotency key до начала операции, БД использует UNIQUE constraint на уровне записи «купон + пользователь».

[Применимо: CTF-задачи, внешний пентест веб-приложений с HTTP/2.]

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

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

Шаг 3. Дублируйте вкладку 19 раз (Ctrl+R). Итого 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. Пройдитесь по вкладкам. Успешная эксплуатация race condition: несколько ответов с кодом 200 и изменённой ценой. При типичном limit overrun из 20 запросов 5–15 проходят проверку — купон применяется многократно.

Почему именно двадцать, а не два? Серверная задержка (server-side jitter) добавляет 1–2 мс дисперсии к обработке каждого запроса. Два запроса могут не попасть в одно race window. Двадцать компенсируют разброс: из пачки хотя бы несколько гарантированно совпадут по таймингу. По данным разбора YesWeHack, при эксплуатации промокода PROMO20 на товар стоимостью 1337 евро single-packet attack через Turbo Intruder позволил снизить цену до 37.62 евро при балансе аккаунта 50 евро.

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

Автоматизация race condition атак в Turbo Intruder

Repeater хорош для быстрого зондажа, но у него потолок. Нужно больше 30 запросов, координация между разными эндпоинтами или многократные попытки с разными параметрами — тут уже Turbo Intruder. Расширение доступно в BApp Store, работает в Burp Suite Community и Professional.

Механизм gate

Turbo Intruder использует концепцию «ворот» (gates). Запросы ставятся в очередь с привязкой к именованному gate. Пока gate закрыт — Burp удерживает запросы, не отправляя на сервер. Вызов engine.openGate('name') отправляет все накопленные запросы одновременно. При 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 — одно соединение, чтобы все запросы уложились в один TCP-пакет. engine=Engine.BURP2 — HTTP/2 single-packet attack. Для целей с HTTP/1.1 замените на Engine.THREADED — переключение на last-byte synchronization. Автоопределения протокола нет, engine указывается вручную — ошибётесь, и запросы не синхронизируются. Проверяйте заранее.

Чтение результатов

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

Ограничения Turbo Intruder

Single-packet attack работает исключительно с HTTP/2. На HTTP/1.1 last-byte synchronization менее точна — может потребоваться 50–100 запросов вместо 10–20. На CTF-платформах PortSwigger Web Security Academy HTTP/2 поддерживается; на самодельных стендах HackTheBox или DownUnderCTF — не гарантировано. Проверяйте заголовки ответа или результат ALPN-negotiation перед выбором engine.

Multi-endpoint race condition: гонка между разными эндпоинтами

Этот паттерн русскоязычные разборы CTF-задач практически не покрывают, хотя на соревнованиях уровня 300+ очков он появляется регулярно. Суть: два разных эндпоинта модифицируют общее состояние (баланс, корзину, статус заказа), и их race window нужно совместить.

Предусловия и ограничения

Работает если: два эндпоинта читают и пишут в одну таблицу без блокировки, фронтенд не сериализует запросы к разным путям, оба эндпоинта доступны в рамках одной сессии.

Не работает если: бэкенд использует distributed lock (Redis-based), каждый микросервис работает с отдельной копией данных, front-end load balancer хеширует запросы по cookie на разные бэкенды (два запроса могут попасть на разные ноды и вообще не конкурировать за одну запись).

[Применимо: CTF-задачи с checkout-процессами, сменой email, двухфакторной аутентификацией.]

Классический CTF-сценарий: добавление товара в корзину и одновременное оформление заказа. Или — отправка запроса на смену email и одновременный запрос подтверждения со старым токеном. По методологии PortSwigger (Tier-1 research), для поиска multi-endpoint race condition нужны три шага:

  1. Predict — найти два эндпоинта, работающих с общими данными (POST /cart/add и POST /cart/checkout, POST /email/change и GET /email/confirm)
  2. Probe — отправить пачку запросов к обоим эндпоинтам через один gate в Turbo Intruder, отсортировать ответы по аномалиям
  3. Prove — воспроизвести коллизию: товар добавлен после checkout, скидка применена к уже оформленному заказу, email подтверждён с чужим токеном
def queueRequests(target, wordlists):
    engine = RequestEngine(endpoint=target.endpoint,
                           concurrentConnections=1,
                           engine=Engine.BURP2)
    engine.queue(addItemReq, gate='race1')
    for i in range(20):
        engine.queue(checkoutReq, gate='race1')
    engine.openGate('race1')

Проблема тайминга при multi-endpoint: разные эндпоинты обрабатываются с разной скоростью. Если /cart/add выполняется за 50 мс, а /cart/checkout за 200 мс — race window не совпадёт. Решение — выравнивание: согласно рекомендациям из HackTricks, можно добавить «балласт» к быстрому запросу через padding в теле или избыточные заголовки, чтобы замедлить обработку более быстрого эндпоинта. Грубо, но работает.

Ещё одна засада: session-based locking. PHP по умолчанию сериализует все запросы одной сессии. Для multi-endpoint это убийственно — два запроса с одним session cookie обработаются последовательно, и гонки не будет. Решение: разные session tokens для каждого запроса, если логика задачи позволяет, или искать эндпоинты без привязки к сессии.

Partial construction: эксплуатация незавершённых объектов в CTF

Partial construction — самый изящный и сложный паттерн уязвимости состояния гонки. На CTF задачи с ним обычно стоят от 400 очков. И не зря.

Предусловия и ограничения

Работает если: создание объекта и инициализация его полей выполняются в разных SQL-запросах, между INSERT и UPDATE поле содержит NULL или пустую строку, эндпоинт верификации доступен и корректно обрабатывает пустое значение токена.

Не работает если: ORM выполняет INSERT ... VALUES (...) со всеми полями атомарно, база использует NOT NULL DEFAULT gen_random_uuid(), или эндпоинт верификации отклоняет пустые значения на уровне валидации до обращения к БД.

[Применимо: CTF-задачи с регистрацией, верификацией email, двухэтапным созданием объектов.]

Суть: при регистрации пользователя сервер выполняет INSERT INTO users (email), а затем отдельным запросом UPDATE users SET token='abc123' WHERE email='...'. Между этими двумя операциями поле token содержит NULL. Если отправить запрос на подтверждение с пустым токеном одновременно с регистрацией — он может совпасть с неинициализированным значением. Окно — доли миллисекунды. Но оно есть.

Разведка при partial construction отличается от limit overrun:

  1. Найти эндпоинт подтверждения. Часто он утекает в JS-файлах — в лаборатории PortSwigger по partial construction эндпоинт POST /confirm?token=... обнаруживается в /resources/static/users.js.

  2. Протестировать поведение параметра. Произвольный токен даёт Incorrect token, пустой параметр token=Forbidden, а пустой массив token[]=Invalid token: Array. Ответ Invalid token: Array — маркер уязвимости: сервер сравнивает значение с записью в базе, а не отклоняет на уровне формата. Видите такой ответ — копайте дальше.

  3. Синхронизировать запросы. Turbo Intruder обязателен: нужно отправить регистрацию и десятки запросов подтверждения с token[]= через один gate. Race window при partial construction — доли миллисекунды, поэтому требуется 50+ запросов подтверждения на каждую попытку регистрации, и циклический перезапуск скрипта с новым email на каждой итерации.

Time-sensitive attacks: гонка без общего ресурса

Отдельный подтип, о котором русскоязычные разборы race condition в CTF практически не упоминают. Time-sensitive attack не требует параллельных запросов к общему ресурсу — здесь нет классического TOCTOU. Вместо этого эксплуатируется предсказуемость серверных значений, зависящих от времени.

Сценарий: сервер генерирует токен сброса пароля на основе текущего timestamp (через time(), microtime() или Math.random() без криптографически стойкого seed). Если два пользователя запрашивают сброс в одну миллисекунду, их токены совпадут. Отправив два запроса на сброс (для своего аккаунта и для жертвы) через single-packet attack, атакующий получает идентичные токены — и использует свой для сброса чужого пароля. Элегантно и больно.

Работает если: сервер использует предсказуемый PRNG для генерации секретов. Не работает если: токен формируется через /dev/urandom, secrets.token_hex() или аналогичный криптографически стойкий источник.

По классификации PortSwigger этот паттерн не относится к TOCTOU — коллизия возникает из-за совпадения входных данных для генератора, а не из-за конкурентного доступа к общему ресурсу.

Repeater vs Turbo Intruder: когда что использовать

Критерий Burp Repeater (parallel) Turbo Intruder
Сложность настройки Минимальная: дублируй вкладки, Send group Python-скрипт, знание API
Максимум запросов за залп 20–30 вкладок Сотни и тысячи
Координация эндпоинтов Нет: все вкладки — один запрос Да: разные запросы в одном gate
Многократные итерации Ручной перезапуск Циклический запуск в скрипте
HTTP/2 single-packet Да (автоматически) Да (Engine.BURP2, вручную)
HTTP/1.1 last-byte sync Да (автоматически) Да (Engine.THREADED, вручную)
Версия Burp Suite Только Professional 2023.9+ Community и Professional
Когда использовать Быстрый зондаж limit overrun Multi-endpoint, partial construction
Когда НЕ использовать Partial construction, multi-endpoint Простой limit overrun (избыточно)

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

Для HTTP/3 существует экспериментальная синхронизация через QUIC. Согласно данным HackTricks, утилита QuicDraw и библиотека H3SpaceX (Go) реализуют last-frame synchronization: несколько QUIC-стримов завершаются (FIN) в одной UDP-датаграмме. Классический last-byte sync неприменим к QUIC из-за UDP-транспорта. В CTF-задачах HTTP/3 пока встречается редко, но для багбаунти на production-сервисах с QUIC — инструмент уже рабочий.

Типичные ошибки при эксплуатации race condition в CTF

Слишком мало запросов. Два параллельных запроса — теоретический минимум. На практике server-side jitter делает попадание двух запросов в одно race window маловероятным. Отправляйте минимум 20 для зондажа.

Игнорирование HTTP-версии. Single-packet attack работает только с HTTP/2. Если цель поддерживает только HTTP/1.1, а в Turbo Intruder указан Engine.BURP2 — запросы не синхронизируются. Проверяйте протокол через ALPN или заголовки ответа перед выбором engine. Я видел, как люди по полчаса бьются, не понимая, почему ничего не летит — а там HTTP/1.1.

Session-based locking. PHP с дефолтным session_start() сериализует все запросы одной сессии: параллельная обработка невозможна. Решение: разные session tokens для каждого запроса (если задача позволяет), или поиск эндпоинтов без привязки к сессии.

Кастомный Python вместо Repeater. На CTF часто вижу, как участники тратят час на asyncio + aiohttp, пытаясь синхронизировать конкурентные запросы. Сетевой jitter Python-клиента — 5–50 мс, что больше типичного race window в 1–5 мс. Burp с single-packet attack решает задачу за минуту без единой строки кода. Формула на бумаге понятна, но race window по-настоящему ощущается только когда сам прогоняешь запросы и видишь тайминги в таблице результатов.

Повторное использование аккаунта. После неудачной попытки состояние аккаунта может измениться: купон помечен как использованный, баланс обнулён. Создавайте новый аккаунт для каждой серии попыток. На CTF аккаунты бесплатные — не экономьте.

Путаница между race condition и brute force. Race condition — не перебор. Это эксплуатация конкретного временного окна. Если 1000 запросов не дали результата — проблема не в количестве, а в неправильной идентификации race window, неподходящем паттерне атаки или в отсутствии уязвимости. Больше запросов ≠ ближе к цели.

Race condition в веб-приложениях — единственный класс уязвимостей, для которого не существует автоматического сканера. sqlmap находит инъекции, nuclei проверяет CVE по шаблонам, ffuf перебирает пути — всё автоматизировано. Для гонки состояний нет кнопки «scan», и не будет, потому что уязвимость живёт не в коде, а во взаимодействии запросов с бизнес-логикой. Turbo Intruder — не детектор, а инструмент ручной эксплуатации с точной доставкой пакетов. Разница принципиальная, и именно она делает race condition одной из самых плохо покрываемых тем в стандартной подготовке. Большинство CTF-игроков останавливаются на limit overrun: двадцать одинаковых запросов, купон применился шесть раз, задача закрыта. На уровне 300+ очков появляются partial construction и multi-endpoint гонки, где пачка одинаковых запросов бесполезна — нужно понимать серверную архитектуру, определять какие операции неатомарны, знать какой эндпоинт дёрнуть в момент, пока объект ещё не полностью создан. Этот навык переносится на реальные багбаунти лучше большинства CTF-дисциплин: CVE-2022-4037 в GitLab — ровно тот паттерн partial construction, только в продакшне и с CVSS 6.4. На WAPT эту цепочку проходят в течение двух модулей с лабами.

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

Поделиться

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

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

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