
Куртка за $1337, купленная за $37.62 двадцатью параллельными POST-запросами к одному эндпоинту — не выдумка, а результат эксплуатации limit overrun race condition в лабе PortSwigger. На CTF гонка состояний встречается в каждом втором веб-таске с бизнес-логикой: двойное применение купона, обход лимитов промокодов, раздувание баланса через параллельные рефанды. Между «я знаю что такое race condition» и «я забрал флаг через race window в 2 мс» — пропасть из конкретных техник, инструментов и понимания того, как сервер обрабатывает конкурентные запросы. Разберём эту пропасть: от механики TOCTOU до рабочих скриптов Turbo Intruder и разбора реальных CTF-тасков.
Race condition — CWE-362 по классификации MITRE, «Concurrent Execution using Shared Resource with Improper Synchronization». Формулировка из каталога CWE прямая: продукт содержит конкурентную последовательность кода, которая требует временного эксклюзивного доступа к общему ресурсу, но существует временное окно, в которое этот ресурс может быть изменён другой последовательностью, работающей параллельно. Последствия по CWE-362: DoS через потребление CPU, аварийное завершение процесса, утечка данных. Платформы, где CWE-362 чаще всего встречается по данным MITRE: C, C++, Java, Mobile, ICS/OT — но веб-приложения на любом стеке точно так же подвержены этой слабости.
Подтип, который на CTF эксплуатируют чаще всего — TOCTOU (CWE-367, Time-of-Check to Time-of-Use). CWE-367 — дочерний элемент CWE-362, описывает конкретный паттерн: продукт проверяет состояние ресурса перед использованием, но между проверкой и использованием состояние может измениться. Последствия по CWE-367 — нарушение целостности: изменение логики исполнения, модификация данных приложения.
Представьте интернет-магазин в CTF-таске. Два потока на сервере одновременно обрабатывают запросы на покупку последнего товара (stock = 1). Оба читают stock = 1, оба проходят if stock > 0, оба декрементят — финальное значение -1. Ни один поток не увидел запись другого. Промежуток между чтением и записью — race window. Именно в это окно атакующий вклинивает свой второй (третий, двадцатый) запрос.
По классификации OWASP (данные OWASP Tier-1 research), race condition затрагивает сразу несколько категорий OWASP Top 10: A08 (Software and Data Integrity Failures) — TOCTOU и повреждение состояния при конкурентном доступе; A01 (Broken Access Control) — обход проверок прав через гонку; A04 (Insecure Design) — когда архитектура вообще не предусматривает синхронизацию.
В терминах MITRE ATT&CK эксплуатация race condition в веб-приложении ложится на технику Exploit Public-Facing Application (T1190, Initial Access). Последствия — Stored Data Manipulation (T1565.001, Impact) при модификации данных в БД и Financial Theft (T1657, Impact) при double-spend сценариях.
Race condition — не техническое упражнение ради красоты. В CTF это путь к флагу, в bug bounty — вектор с финансовым импактом. Мотивация атакующего конкретна.
Double spending — один запрос на оплату порождает два заказа, потому что проверка баланса и списание — два отдельных шага. Атакующий получает товар (или флаг) дважды за одну оплату.
Limit overrun — многократное применение промокода, который должен быть одноразовым. Скидка 20% превращается в скидку 400%, потому что счётчик использований не инкрементирован атомарно.
Account takeover — подмена email или верификация стороннего адреса через гонку в OAuth-потоке. Два одновременных запроса: один на привязку нового email, второй на верификацию — и атакующий получает контроль над чужим аккаунтом.
Privilege escalation — одновременное назначение роли и проверка прав: поток A получает привилегии до того, как поток B завершит валидацию текущего уровня доступа.
На CTF race condition обычно маркируется тасками с темой «магазин», «баланс», «голосование», «рейтинг» — любая операция, где пользователь может повторно влиять на числовое значение. Видите в описании слова «balance», «coupon», «redeem», «transfer» — проверяйте эндпоинты на гонку первым делом.
Главная техническая проблема при эксплуатации race condition — не «отправить много запросов», а доставить их на сервер одновременно. Сетевой jitter — случайные задержки на каждом хопе — размазывает запросы во времени. Race window в 2-5 мс легко пропустить, если запросы приходят с разницей в 50 мс. Для минимизации jitter разработаны две техники.
Принцип: отправляем несколько запросов, удерживая последний байт тела каждого. Веб-сервер не начинает обработку, пока не получит запрос целиком. Когда все запросы «взведены» — одновременно досылаем финальные байты. Влияние сетевых задержек минимизируется: по сети в критический момент уходит всего несколько байт, а не полные тела запросов.
Ограничение: HTTP/1.1 поддерживает один запрос на соединение. Для 20 параллельных запросов нужно 20 TCP-соединений, каждое вносит задержку при установке. По данным Vaadata, метод позволяет доставить до 20 запросов на сервер в пределах ~1 мс — для большинства CTF-тасков хватает за глаза.
Техника, описанная Джеймсом Кеттлом (James Kettle, PortSwigger Research) в статье «The single-packet attack: making remote race-conditions local». HTTP/2 поддерживает мультиплексирование — несколько запросов через одно TCP-соединение. Атакующий отправляет фрагменты каждого запроса, удерживая финальный фрагмент. Затем все финальные фрагменты уходят в одном TCP-пакете.
Результат: все запросы приходят на сервер одновременно, полностью исключая сетевой jitter. Удалённая race condition фактически становится локальной. В один пакет реально упаковать 20-30 запросов.
Для CTF single-packet attack — основной метод. Если таск поддерживает HTTP/2 (а большинство современных CTF-серверов поддерживают), шансы на эксплуатацию гонки значительно выше. При HTTP/1.1 last-byte sync работает, но требует больше попыток из-за невозможности полностью устранить jitter.
Turbo Intruder — расширение Burp Suite с Jython-движком, поддерживающее и last-byte sync, и single-packet attack. Ключевая концепция — gate: запросы ставятся в очередь с пометкой и не отправляются до вызова openGate(). Когда все запросы в очереди — gate сбрасывает их одновременно, одним пакетом.
Скрипт для single-packet attack (по данным OWASP и руководства YesWeHack):
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=10,
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.BURP2 — включает HTTP/2 single-packet mode. Если цель поддерживает только HTTP/1.1, используйте Engine.THREADED (last-byte sync) или Engine.BURP. Выбор неправильного engine — частая ошибка, из-за которой эксплоит тупо не срабатывает. На CTF видел это раз десять: человек копирует скрипт, запускает — ничего. Потому что сервер на HTTP/1.1, а в скрипте стоит BURP2.
gate='race1' — маркер группы. Все запросы с одинаковым gate отправляются вместе при openGate('race1'). Можно использовать несколько gate для многофазных атак.
range(20) — количество параллельных запросов. Для CTF обычно достаточно 10-30. Для bug bounty может потребоваться 50-100 с повторными попытками, потому что серверная нагрузка и балансировщики добавляют непредсказуемость.
Признак успешного попадания в race window: отрицательный timestamp в ответах Turbo Intruder. Это значит, что сервер начал отвечать до полной отправки запроса — запросы обработались параллельно и наложились друг на друга.
С 2023 года Burp Suite поддерживает параллельную отправку нативно. Перехватите целевой запрос через Proxy, отправьте в Repeater, создайте tab group с 10-20 дубликатами и выберите «Send group in parallel». Burp отправит все запросы одновременно через last-byte synchronization.
Этот метод проще Turbo Intruder — не нужно писать Jython-скрипты. Но менее гибок: нет HTTP/2 single-packet, сложнее масштабировать до 50+ запросов, нельзя варьировать payload между запросами. Для быстрой проверки гипотезы на CTF — Repeater. Для серьёзной эксплуатации с тонкой настройкой — Turbo Intruder.
Разбор основан на лабораторной задаче PortSwigger «Limit overrun race conditions», описанной в руководстве YesWeHack.
Условие: баланс $50, в магазине «Lightweight L33t Leather Jacket» за $1337. Промокод PROMO20 на 20% скидку. Одно применение даёт цену $1069.60 — далеко за пределами бюджета.
Эксплуатация — делай раз, делай два, делай три:
POST /cart/coupon с телом csrf=...&coupon=PROMO20). Отправьте в Turbo Intruder.engine=Engine.BURP2, range(20), gate race1. Запустите атаку.Результат: промокод применяется ~20 раз. Итоговая скидка — $1299.38, финальная цена — $37.62. Флаг получен.
Почему работает: race window находится между SQL-операциями SELECT used FROM coupons WHERE code='PROMO20' (возвращает false для всех 20 запросов, потому что ни один ещё не записал true) и UPDATE coupons SET used=true WHERE code='PROMO20' (выполняется только после SELECT). Все 20 запросов проходят проверку, все 20 применяют скидку. Проверка и действие — не атомарная операция. Вот и вся фундаментальная причина.
На m0leCon CTF 2023 задача «goldinospizza2» использовала WebSocket (по данным writeup'а). Стартовый баланс — 30 евро, цель — купить «GOLDEN Pizza» стоимостью значительно дороже.
В исходном коде были функции order (уменьшает баланс на price * quantity) и cancel (увеличивает баланс обратно — рефанд). Обе работали с общим балансом в БД без блокировок. Красный флаг размером с футбольное поле.
Эксплуатация: одновременная отправка множества order-запросов через WebSocket. Каждый запрос читал текущий баланс (30 евро), проходил balance >= price, создавал заказ. Все запросы видели одно значение баланса — деньги списывались некорректно, создавалось больше заказов, чем баланс позволял. Дальше — массовый cancel, рефанд за каждый заказ. Баланс растёт экспоненциально: 30 → 60 → 120 → ... до покупки GOLDEN Pizza.
Момент для CTF-игроков: WebSocket — stateful-протокол. Turbo Intruder для WebSocket не подходит напрямую. В оригинальном writeup автор запускал JavaScript-скрипт из DevTools браузера, который открывал WS-соединение и отправлял 100 order-сообщений в цикле без ожидания ответа — все сообщения попадали в серверный event loop практически одновременно. Грязно, но работает.
По данным Vaadata, на реальном аудите e-commerce платформы два разных купона нельзя было совместить. При последовательной отправке сервер отвечал: первый купон — 200 OK с текстом «Coupon applied», второй — 500 Internal Server Error с «One coupon already applied».
При одновременной отправке двух запросов POST /redeem_coupon (один с Coupon=Holidays20, другой с Coupon=Christmas10) через single-packet attack — оба возвращали 200 OK. Проверка «сколько купонов в корзине» возвращала 0 для обоих запросов, потому что ни один ещё не был записан в БД на момент проверки. Оба купона успешно применялись.
Тот же паттерн переносится на CTF-таски: если есть ограничение «одно применение» — пробуйте параллельную отправку. Ограничение реализовано как check-then-act, а не как атомарная операция с уникальным constraint.
Гонки состояний — не академическая тема и не исключительно CTF-вектор. CVE-2022-4037 в GitLab CE/EE — наглядный пример.
Затронутые версии: все до 15.5.7, серия 15.6 до 15.6.4, серия 15.7 до 15.7.2.
Суть: race condition позволяла подделать верифицированный email и захватить аккаунт на сторонних сервисах при использовании GitLab как OAuth-провайдера.
Технические данные (NVD): CVSS 6.4 (MEDIUM), вектор CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N — атака по сети, низкая сложность, нужны минимальные привилегии (авторизованный пользователь), без участия жертвы. Scope: Changed — уязвимость бьёт за пределами самого GitLab (на сторонние сервисы, использующие GitLab OAuth). Слабость: CWE-362.
Механизм: два одновременных запроса к GitLab OAuth — один на привязку email, второй на верификацию. Из-за отсутствия блокировки между проверкой «этот email свободен» и действием «привязать email» атакующий мог привязать чужой верифицированный email к своему аккаунту. Классический TOCTOU.
По данным CISA (SSVC decision: Track), активная эксплуатация в дикой природе не зафиксирована, автоматизация атаки невозможна (Automatable: no). EPSS — 0.0064 (percentile 0.4956, ниже медианы). Но для OAuth-цепочки импакт серьёзен: захват аккаунтов на всех сервисах, делегирующих аутентификацию GitLab.
Для CTF-контекста CVE-2022-4037 интересен как паттерн: race condition не обязательно про деньги. Одновременная смена email на двух аккаунтах, одновременная верификация двух адресов через один токен — те же задачи встречаются на соревнованиях.
Перед запуском Turbo Intruder проверьте четыре условия:
SELECT → проверка в коде → UPDATE. Два SQL-запроса вместо одного атомарного — красный флаг.SELECT ... FOR UPDATE, нет UNIQUE constraints, нет мьютексов (threading.Lock() в Python, sync.Mutex в Go, synchronized в Java). Если ничего из этого не найдено рядом с критической операцией — синхронизации нет.По чек-листу OWASP:
Check-then-act в Python: if user.balance >= amount: user.balance -= amount — две операции, race window между ними. В Node.js: const balance = await getBalance(userId); if (balance >= amount) { await deductBalance(userId, amount); } — два await, между которыми другой запрос может вклиниться.
SQL без атомарности — самый распространённый паттерн в CTF:
-- Уязвимо: два запроса, race window между ними
SELECT qty FROM inventory WHERE id = 1;
UPDATE inventory SET qty = qty - 1 WHERE id = 1;
-- Безопасно: атомарный условный декремент
UPDATE inventory SET qty = qty - 1 WHERE id = 1 AND qty > 0;
Второй вариант — единый запрос: проверка qty > 0 и декремент выполняются атомарно внутри СУБД. Race window исчезает. Если в исходниках CTF-таска видите первый вариант — вы нашли вектор.
Filesystem TOCTOU (CWE-367): if os.path.exists(path): os.remove(path) — между exists() и remove() атакующий может подменить файл на symlink. В CTF встречается реже, но в задачах на pwn и misc — вполне.
Для обнаружения TOCTOU в исходниках — Semgrep с правилом toctou-filesystem-check-then-act (severity: WARNING, confidence: HIGH, CWE-367). Правило ловит конструкции os.path.exists() / os.path.isfile() / os.access() с последующим os.remove() или open().
Для тестирования веб-эндпоинтов — Nuclei с community-шаблонами: nuclei -t vulnerabilities/generic/ -tags race-condition -u https://target. Шаблоны отправляют параллельные запросы и анализируют ответы на признаки двойного срабатывания.
Для Go-сервисов — встроенный race detector: go test -race ./... или go run -race main.go. Ловит data races на этапе тестирования.
Понимание защиты — часть навыка CTF-игрока. Если видите в исходниках одну из этих конструкций, race condition скорее всего закрыта, и стоит искать другой вектор.
Уровень БД (самый надёжный): атомарные условные операции (UPDATE ... SET qty = qty - 1 WHERE qty > 0 вместо SELECT + UPDATE); блокировка строк SELECT ... FOR UPDATE; уникальные constraints UNIQUE(user_id, coupon_id) — БД сама отклонит дубликат.
Уровень приложения: мьютексы и блокировки (threading.Lock() в Python, sync.Mutex в Go); distributed locks через Redis (SET key value NX EX ttl) для горизонтально масштабируемых сервисов; идемпотентные ключи — каждый запрос содержит уникальный idempotency_key, сервер обрабатывает запрос с одним ключом ровно один раз.
Архитектурный уровень: queue-based processing — все операции с критическим ресурсом проходят через очередь, исключая параллелизм; optimistic locking с version check: UPDATE ... SET qty = qty - 1, version = version + 1 WHERE id = 1 AND version = 42 — если кто-то успел изменить version, UPDATE не применится.
Если рядом с операциями записи баланса, применения купонов или голосования ни одного из этих механизмов нет — пробуйте race condition.
Гонка состояний — одна из тех уязвимостей, где 90% работы не в написании эксплоита, а в понимании модели состояний приложения. Часто наблюдаю одну и ту же ошибку у новичков на CTF: сразу хватаются за Turbo Intruder и начинают засыпать эндпоинт сотней запросов, не разобравшись, где именно race window и какие два запроса должны столкнуться. Без модели состояний это brute-force по всей поверхности атаки, который срабатывает только случайно. Правильный подход — нарисовать диаграмму переходов состояний (хотя бы в голове): промокод не применён → проверка валидности → проверка лимита → пересчёт корзины → промокод применён. Гонка возникает на конкретном переходе между «проверка лимита» и «промокод применён», и если этот переход не найден — количество запросов не поможет. Чрезмерная нагрузка убивает таск-сервер и создаёт DoS вместо эксплуатации. На WAPT эту цепочку «модель состояний → нахождение race window → точечная эксплуатация» разбирают в двух модулях с лабами, где каждый шаг отрабатывается отдельно — от анализа исходников до финального флага.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...