
Куртка за €1 337, на балансе — €50. Двадцать параллельных запросов на применение одноразового промокода PROMO20 через single-packet attack в Turbo Intruder — и финальная цена падает до €37.62. Купон сработал не один раз, а столько, сколько запросов проскочило: сервер просто не успел пометить его использованным до того, как обработал всю пачку. Это лаба PortSwigger «Limit overrun race conditions», и такие же задачи регулярно всплывают на CTF. Race condition эксплуатация — навык, который превращает «непредсказуемый баг» в воспроизводимую атаку. Но только если понимаешь механику race window и не пытаешься ловить тайминг на глаз.
PortSwigger на Black Hat USA 2023 показали (whitepaper «Smashing the state machine: The true potential of web race conditions»), что race condition в вебе — это не только «применить купон дважды». Три основных типа покрывают большинство CTF-сценариев и баунти-отчётов. Подробнее — в нашем обзоре пентест веб-приложений.
Самый частый тип race condition в CTF. Сервер проверяет условие (Time of Check), потом выполняет действие (Time of Use). Между проверкой и действием — race window, окно в единицы миллисекунд, куда можно протолкнуть параллельные запросы.
Паттерн в коде:
# Time of Check — проверяем
coupon = db.query("SELECT used FROM coupons WHERE code = ?", code)
if coupon.used:
return "Купон уже использован"
# Race window — другие потоки ещё видят used=false
# Time of Use — действуем
db.execute("UPDATE coupons SET used = true WHERE code = ?", code)
apply_discount(order)
Все запросы, попавшие в race window, проходят проверку if coupon.used с результатом False — ни один ещё не дошёл до UPDATE. Одноразовый купон применяется столько раз, сколько запросов успели проскочить.
TOCTOU уязвимость в CTF встречается в задачах на:
В баунти-программах этот класс приносит стабильные выплаты. На HackerOne (report #759247) описан ровно такой сценарий: одна подарочная карта погашалась параллельными запросами и применялась многократно. По данным PortSwigger, limit overrun — подтип TOCTOU-уязвимостей, но есть и более хитрые разновидности.
Более тонкий тип гонки потоков в веб-приложении. Два запроса к одному эндпоинту одновременно меняют одно и то же поле. Второй перезаписывает данные первого до того, как первый их использует.
Классический пример — CVE-2022-4037 в GitLab CE/EE. По данным 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. Затрагивало все версии GitLab до 15.5.7, от 15.6 до 15.6.4 и от 15.7 до 15.7.2. Механика:
pending_email = "attacker@evil.com"pending_email = "victim@target.com"pending_email для генерации токена — а там уже victim@target.comattacker@evil.com, но токен привязан к victim@target.comАтакующий получает на свою почту токен подтверждения для чужого email. Это ведёт к подделке верифицированных email и захвату аккаунтов при использовании GitLab как OAuth-провайдера. По данным CISA (SSVC Decision: Track), эксплуатация не автоматизируема и технический импакт partial — но в CTF-задачах single-endpoint collision составители обожают. У PortSwigger есть отдельная лаба на этот вектор.
Самый сложный тип. Гонка происходит между запросами к разным эндпоинтам, которые работают с общими данными. Типичный CTF-сценарий: один эндпоинт добавляет товар в корзину, другой применяет скидку, третий инициирует оплату. Если запрос на оплату отправить одновременно с изменением корзины, сервер может обработать оплату по старой цене с новым содержимым.
Многопоточная атака на веб-приложение с multi-endpoint race усложняется тем, что разные эндпоинты обрабатываются с разной скоростью. Тут нужны дополнительные приёмы синхронизации:
Главный враг эксплуатации состояния гонки на удалённом сервере — сетевой джиттер. Вариация задержки в сети — десятки миллисекунд, а race window часто измеряется единицами. Без минимизации джиттера гонка запросов HTTP превращается в лотерею с паршивыми шансами.
Для серверов на HTTP/1.1 применяется техника last-byte synchronization:
Джиттер влияет только на доставку одного байта через уже установленное TCP-соединение — на порядки меньше, чем доставка целого запроса. Burp Suite автоматически применяет эту технику при параллельной отправке на HTTP/1 серверы.
Ограничение: каждый запрос требует отдельного TCP-соединения. При 20-30 запросах это 20-30 параллельных соединений, и можно упереться в серверные лимиты MaxClients или firewall-правила. В CTF обычно проблем нет, в продакшене — как повезёт.
HTTP/2 поддерживает мультиплексирование — несколько запросов через одно TCP-соединение. Single-packet attack race condition использует именно это:
Эту технику впервые показали исследователи PortSwigger на Black Hat USA 2023. Single-packet attack полностью убирает сетевой джиттер: все запросы приходят буквально в одном пакете, разница во времени доставки — ноль. Удалённая race condition становится «локальной» по надёжности.
На практике в один TCP-пакет помещаются 20-30 HTTP/2-запросов — для большинства CTF-задач хватает за глаза. Почему 20-30, а не 2? Серверный джиттер (внутренние задержки при маршрутизации запроса к обработчику) никуда не девается. Чем больше запросов в пачке, тем выше шанс, что несколько из них попадут в race window одновременно. Для разведки — 20-30. Когда поведение подтверждено и нужен чистый эксплойт — сокращаем до 2-5.
| Характеристика | Last-byte sync (HTTP/1) | Single-packet (HTTP/2) |
|---|---|---|
| Устранение сетевого джиттера | Частичное | Полное |
| Число TCP-соединений | Одно на запрос | Одно на все |
| Запросов в пачке | 10-30 | 20-30 |
| Требования к серверу | HTTP/1.1 | HTTP/2 |
| Поддержка в Burp | С версии 2023.9 | С версии 2023.9 |
С версии Burp Suite 2023.9 в Repeater появилась встроенная поддержка параллельной отправки. Для базовой эксплуатации race condition в CTF этого хватает:
POST /apply-coupon с телом code=PROMO20)Ctrl+RBurp сам выбирает технику синхронизации по протоколу. HTTP/2 — single-packet attack. HTTP/1 — last-byte synchronization.
Момент, который часто упускают: если первая попытка не сработала, это не значит, что race condition нет. Серверный джиттер (GC-паузы, блокировки пула потоков, кеш-промахи) может сдвинуть обработку так, что запросы не попадут в одно race window. Повторяйте 5-10 раз, прежде чем отбрасывать гипотезу. В CTF обычно хватает 3-5 попыток.
Для сценариев посложнее — multi-endpoint race, гонки с разным таймингом, тысячи запросов — нужен Turbo Intruder. Скрипт на Jython даёт полный контроль над таймингом и параметрами.
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)
Что тут к чему:
engine=Engine.BURP2 — включает single-packet attack для HTTP/2. Для HTTP/1 берите Engine.THREADED или Engine.BURPconcurrentConnections=1 — одно TCP-соединение, все запросы через мультиплексирование. Для HTTP/1 ставьте 10-30gate='race1' — запросы копятся в «воротах» и не уходят на сервер до вызова openGate('race1')Этот шаблон (race-single-packet-attack.py) лежит в стандартной директории примеров Turbo Intruder. Для CTF-задачи — перехватил запрос, отправил в Turbo Intruder, запустил скрипт. Всё.
Для кастомных сценариев — например, чередование запросов к двум разным эндпоинтам — модифицируем тело запроса перед engine.queue(), подставляя нужный path или параметры. Turbo Intruder позволяет задавать задержку между группами через time.sleep() в Jython — полезно для multi-endpoint race, когда нужно подстроить тайминг.
За пределами Burp Suite есть и другие подходы: кастомные скрипты на Python с asyncio/aiohttp, утилита Race the Web. Но для CTF связка Burp Repeater + Turbo Intruder покрывает 95% задач.
Чтобы понять, к чему приводит эксплуатация состояния гонки за пределами CTF, разберём верифицированные CVE с данными из NVD.
| CVE | Продукт | CVSS | Импакт | AC |
|---|---|---|---|---|
| CVE-2022-24800 | October CMS | 8.1 HIGH | RCE | High |
| CVE-2021-24377 | Autoptimize WP | 8.1 HIGH | RCE | High |
| CVE-2023-24042 | LightFTP | 7.5 HIGH | Path Traversal | High |
| CVE-2022-4037 | GitLab CE/EE | 6.4 MEDIUM | Account Takeover | Low |
| CVE-2021-36532 | portfolioCMS | 8.1 HIGH | RCE | High |
CVE-2022-24800 — RCE в October CMS (пакет october/system в Packagist, исправлено в версии 1.0.476). Когда разработчик позволял пользователю задавать имя файла в методе fromData, неаутентифицированный атакующий мог выполнить произвольный код через race condition во временной директории. Вектор: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H, CWE-362. Обратите внимание на AC:H (Attack Complexity: High) — характерный маркер race condition: нужен точный тайминг.
CVE-2021-24377 — RCE в WordPress-плагине Autoptimize до версии 2.7.8. Плагин пытался удалять вредоносные файлы из загружаемого архива (фича Import Settings), но между извлечением файла на диск и его удалением — race window. Это обход предыдущей CVE-2020-24948 (CVSS 7.2 HIGH, CWE-434 — Unrestricted Upload of File with Dangerous Type). Race condition превратила «исправление» в новый вектор RCE. Поучительный пример: неправильная митигация создаёт вторичную уязвимость. Латали одну дыру — открыли другую.
CVE-2023-24042 — Path Traversal в LightFTP до версии 2.2 (CVSS 7.5 HIGH, CWE-362). Обработчик потока использовал перезаписанный context->FileName, что позволяло реализовать обход пути через специально сформированный FTP-запрос. По данным CISA, для этой уязвимости есть PoC (Exploitation: poc), но автоматизация невозможна.
Все CVE объединяет CWE-362 (Concurrent Execution using Shared Resource with Improper Synchronization). В контексте MITRE ATT&CK race condition может работать как часть техники Exploit Public-Facing Application (T1190, Initial Access) — когда уязвимое веб-приложение торчит в интернет. Результатом может быть Stored Data Manipulation (T1565.001, Impact) или Financial Theft (T1657, Impact) — при двойном списании и обходе платёжных лимитов.
Методология PortSwigger строится в три этапа: предсказание, зондирование, подтверждение.
Этап 1 — предсказываем коллизии. Ищем эндпоинты с конкурентным доступом к ресурсам. В CTF-задачах это:
Для каждого кандидата — три вопроса. Где хранится состояние? Серверная БД — идеальна для эксплуатации. Клиентский JWT — бесполезно. Редактирование или добавление? Операции, меняющие существующие данные (смена email), дают больше потенциала коллизий. Общий ключ? Для успешной гонки нужны две операции, обращающиеся к одному ключу — одному user_id, одному coupon_code.
Этап 2 — ищем подсказки. Отправляем пачку из 20-30 параллельных запросов и сравниваем ответы с нормальным (последовательным) поведением:
Любое отклонение от ожидаемого — подсказка. Авторы CTF обычно оставляют заметные побочные эффекты, чтобы игрок мог подтвердить наличие бага.
Этап 3 — доказываем концепцию. Если при 20 запросах 5 «проскочили» — сокращаем до 2 для чистоты эксплойта. При 2 запросах атака чувствительнее к таймингу — повторяем 10-20 раз. Для CTF-задач типа «купи предмет за 0 монет» или «обойди rate-limit на OTP» Burp Repeater с group send покрывает потребность. Для сложных multi-endpoint сценариев — Turbo Intruder.
Отдельный класс — time-sensitive attacks. Даже если race condition напрямую не эксплуатируется, техника одновременной доставки запросов может вскрыть слабость в генерации токенов. Если токен сброса пароля основан на timestamp, два запроса в одном пакете получат одинаковый токен — время совпало с точностью до миллисекунды. В CTF это встречается как задача на «предсказание reset-токена».
Большинство CTF-команд до сих пор воспринимают race condition как «ненадёжный» класс багов — мол, срабатывает через раз, зависит от везения. После появления single-packet attack в 2023 году это перестало быть правдой. При наличии HTTP/2 на сервере эксплуатация race condition стала практически детерминированной: один TCP-пакет, 20 запросов, нулевой джиттер. Если race window существует — single-packet attack его найдёт.
Проблема не в надёжности техники, а в том, что команды не включают race condition в чек-лист при анализе задач. Они ищут SQLi, SSTI, SSRF — а гонки потоков проверяют в последнюю очередь, когда «ничего не работает». На двух последних CTF, где я участвовал, задачи на race condition решало менее 30% команд — при том что техника эксплуатации заняла бы 10 минут у подготовленного игрока.
Это не сложная уязвимость. Это недооценённая уязвимость. Разница в том, что первую нужно изучать годами, а вторую — достаточно один раз попробовать руками. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в течение двух модулей с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...