
На CTF задача стоила 300 очков. Я час писал многопоточный скрипт на Python — requests, threading, asyncio. Ни одна попытка не попала в race window. Потом открыл Burp Repeater, сгруппировал двадцать вкладок с одинаковым запросом на применение купона, нажал Send group (parallel) — в ответах скидка применилась шестнадцать раз. Баланс аккаунта ушёл ниже цены товара, корзина оформилась, флаг в кармане. Три минуты вместо часа, потому что правильный инструмент бьёт правильный код. Дальше — пошаговая эксплуатация race condition в CTF-задачах через Burp Repeater и Turbo Intruder, от простого limit overrun до partial construction.
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:
В терминах MITRE ATT&CK race condition при эксплуатации веб-приложения попадает под Exploit Public-Facing Application (T1190, Initial Access). На CTF это почти всегда начальный вектор: ломаем бизнес-логику, получаем доступ к ресурсу, забираем флаг.
Принципиальное отличие race condition от XSS, SQLi или SSRF: код приложения формально корректен. Проверки на месте, валидация работает. Проблема только в порядке и времени выполнения операций. Автоматические сканеры эту штуку не находят — в CTF она закономерно идёт как задача повышенной сложности.
Прежде чем атаковать race condition на CTF, проверьте готовность:
Выбор инструмента зависит от сложности задачи. Вот 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.
[Применимо: 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-лимита.
Когда 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-стендах — не факт.
[Применимо: 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 — нужно компенсировать разрыв количеством.
[Применимо: 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, и коллизия возможна.
Не каждая 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:
По методологии 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 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...