Главная / Блог / Race Condition в CTF: эксплуатация на практике

12 мин.00

Race Condition в CTF: эксплуатация на практике

Race Condition в CTF: эксплуатация на практике

На лабе PortSwigger «Limit overrun race conditions» задача выглядит нерешаемой: куртка за €1337, на балансе €50, промокод PROMO20 даёт 20% скидку и применяется однократно. Три минуты работы с Turbo Intruder — двадцать параллельных запросов в одном TCP-пакете — и промокод срабатывает четырнадцать раз. Итоговая цена: €37.62. По данным YesWeHack, именно этот сценарий стал каноническим примером limit overrun в обучении race condition эксплуатации. И вот что тут красиво: разница между «неуязвимо» и «взломано» — четыре микросекунды между запросами. Четыре. Микросекунды.

Как открывается race window: механика TOCTOU уязвимости

Race condition в веб-приложении — конкретный промежуток времени между проверкой условия и выполнением действия, в который параллельный запрос успевает изменить состояние на сервере. Паттерн называется TOCTOU — Time-of-Check to Time-of-Use — и лежит в основе большинства race condition уязвимостей в вебе.

Разберём по шагам. Сервер при применении купона последовательно делает три вещи (иллюстративная модель, а не реальный код лабы PortSwigger): проверяет SELECT used FROM coupons WHERE code = 'PROMO20' — купон не использован; применяет скидку к заказу через apply_discount(order); помечает купон использованным через UPDATE coupons SET used = true WHERE code = 'PROMO20'. Race window открывается между первым и третьим шагом. Если второй запрос успевает выполнить SELECT до того, как первый дойдёт до UPDATE — оба видят used = false, оба применяют скидку. Вот и всё волшебство.

Окно может составлять миллисекунды или микросекунды — именно поэтому обычный threading в Python с requests.post() часто не срабатывает. Сетевой джиттер размазывает запросы по десяткам миллисекунд, и они не попадают в одно окно. Техника single-packet attack, о которой ниже, решает эту проблему.

По классификации MITRE ATT&CK эксплуатация race condition в веб-приложениях подпадает под технику Exploit Public-Facing Application (T1190, Initial Access). Результат атаки зависит от бизнес-логики: двойное списание купонов ведёт к Financial Theft (T1657, Impact), перезапись email — к захвату аккаунта, а race condition в загрузке файлов — к Stored Data Manipulation (T1565.001, Impact). Не нишевая бага для ценителей — полноценный вектор первоначального доступа.

Ключевое для CTF: race window — величина непостоянная. Зависит от нагрузки на сервер, СУБД, уровня изоляции транзакций. В лабораторных CTF-задачах окно обычно шире, чем в проде, но техника синхронизации запросов одна и та же.

Типы race condition в CTF-задачах

Limit Overrun — обход одноразовых ограничений

Самый частый тип на CTF и в bug bounty. Приложение ограничивает действие одним разом — купон, бонус, голос, вывод средств. Атакующий отправляет N параллельных запросов, и все проходят проверку до того, как хотя бы один обновит состояние. Классическая эксплуатация состояния гонки через TOCTOU.

Где искать limit overrun в CTF: - Применение промокодов и подарочных карт (многократное погашение) - Списание бонусов, внутренней валюты, кэшбэков - Перевод средств между счетами — типичный double spending - Обход rate-limit на brute-force аутентификации - Голосования и лайки с ограничением «один раз на пользователя» - Повторное использование решения CAPTCHA

По данным GuidePoint Security, обход account lockout через race condition — одна из самых частых находок на реальных пентестах. Псевдокод проверки блокировки: если check_account_failed_login_count(username) < 3 — пропустить попытку логина, иначе — заблокировать. При single-packet attack десять запросов начинают обрабатываться до того, как хоть один вызов increment_failed_login_count завершится. Все десять проходят проверку лимита. Красота.

Практический признак в CTF: любой эндпоинт с однократным действием — первый кандидат. Шлёте 20-30 параллельных запросов и считаете, сколько вернут HTTP 200.

Single-endpoint Collision — подмена данных между потоками

Два запроса к одному эндпоинту модифицируют одно и то же поле. Второй перезаписывает данные первого до того, как первый их использует — и сервер работает с подменённым значением.

Хрестоматийный пример — CVE-2022-4037 в GitLab. Уязвимость затрагивала все версии до 15.5.7, версии с 15.6 до 15.6.4 и с 15.7 до 15.7.2. Механизм: при смене email сервер записывает новый адрес в поле pending_email в базе, генерирует токен подтверждения на основании значения из базы, но отправляет письмо на адрес из HTTP-запроса. Два параллельных запроса создают рассинхронизацию: поток A записывает pending_email = attacker@evil.com, поток B перезаписывает pending_email = victim@target.com, затем поток A читает из базы уже victim@target.com, генерирует токен для жертвы — и отправляет этот токен на адрес атакующего из исходного запроса. Результат: верифицированный email жертвы на аккаунте атакующего, захват OAuth-сессии. Элегантно и страшно.

Обратите внимание на CVSS-вектор (CVE-2022-4037, CVSS 6.4 MEDIUM): AC:L — сложность атаки низкая, PR:L — нужна минимальная аутентификация, S:C — scope changed, уязвимость бьёт за пределами компонента. На PortSwigger Web Security Academy есть лаба с этим сценарием — рекомендую потренироваться.

В CTF single-endpoint collision встречается в задачах со сменой email, сбросом пароля, любыми операциями, где одно поле в базе перезаписывается между чтением и использованием.

Multi-endpoint Race — гонка между эндпоинтами

Самый хитрый тип для обнаружения. Два разных эндпоинта работают с общими данными, и гонка происходит между ними. Типичный сценарий: один эндпоинт добавляет товар в корзину, другой проверяет баланс и списывает средства. Если проверка баланса не синхронизирована с добавлением — можно добавить дорогой товар после проверки, но до списания.

Multi-endpoint race condition в CTF встречается реже, но приносит больше очков. Для обнаружения нужно понимать, какие эндпоинты разделяют общее состояние — а это уже анализ бизнес-логики, а не механическая отправка параллельных запросов.

Single-packet attack: нейтрализация сетевого джиттера

Главная проблема эксплуатации race condition — синхронизация запросов. Отправить 20 HTTP-запросов «одновременно» через обычные TCP-соединения не получится: каждый уходит в отдельном пакете, разница в доставке достигает десятков миллисекунд. Для race window в микросекунды этого слишком много.

Техника single-packet attack, представленная PortSwigger Research на Black Hat USA 2023 (whitepaper «Smashing the state machine: The true potential of web race conditions»), решает проблему через HTTP/2 мультиплексирование. HTTP/2 позволяет передавать несколько потоков (streams) по одному TCP-соединению. Атакующий формирует HTTP/2-фреймы всех запросов заранее, удерживает финальные байты — и досылает их в одном TCP-пакете. Сервер распаковывает пакет и видит 20-30 полных запросов практически одновременно. Бенчмарки PortSwigger показывают разброс около четырёх микросекунд. Для сравнения — обычный threading даёт разброс в 20-50 миллисекунд. Разница в четыре порядка.

Для HTTP/1.1 есть техника last-byte synchronization: открываете N TCP-соединений, по каждому отправляете весь запрос кроме последнего байта, затем последние байты всех запросов досылаете одновременно. Менее точно, чем single-packet, но работает с серверами без HTTP/2.

В Burp Repeater (версия 2023.9+) всё проще: добавляете нужные запросы в группу, Burp определяет HTTP-версию и применяет соответствующую технику. Для CTF-задач на платформах с HTTP/2 — самый быстрый путь к эксплуатации.

Burp Turbo Intruder: скрипт для race condition

Для сложных сценариев — сотни попыток, кастомная логика между запросами, перебор параметров — нужен Turbo Intruder. Расширение поддерживает single-packet attack через движок Engine.BURP2 и позволяет управлять синхронизацией через механизм gate.

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-соединение, обязательное условие для single-packet attack. gate='race1' удерживает каждый запрос: Turbo Intruder формирует фреймы, но не отправляет финальные байты. engine.openGate('race1') разом досылает все двадцать запросов в одном пакете. Если в результатах видите отрицательный timestamp — сервер ответил до завершения отправки, для этой техники это нормально.

Критический нюанс: Engine.BURP2 требует HTTP/2 на стороне target. Для HTTP/1.1 берите Engine.THREADED или Engine.BURP — они работают с last-byte sync, менее точны, но совместимы. На практике я чаще всего начинаю с BURP2, и только если target не поддерживает HTTP/2 — откатываюсь к THREADED.

Кастомный Python-скрипт для многопоточных атак

На CTF-соревнованиях не всегда удобен GUI Burp Suite. Для автоматизации race condition подходит Python с asyncio и aiohttp — все запросы ставятся в очередь и отправляются через asyncio.gather.

import asyncio, aiohttp

async def send(session, url, data):
    async with session.post(url, json=data) as r:
        return r.status, await r.text()

async def race(url, data, n=20):
    async with aiohttp.ClientSession() as s:
        tasks = [send(s, url, data) for _ in range(n)]
        return await asyncio.gather(*tasks)

# пример для демонстрации концепции
results = asyncio.run(race("https://target/apply", {"code": "PROMO20"}))
print([r[0] for r in results])  # статус-коды

Подход проще single-packet attack и работает с HTTP/1.1, но менее точен: asyncio.gather не гарантирует одновременную отправку. aiohttp открывает отдельное TCP-соединение на каждый запрос, event loop планирует отправку не строго одновременно — джиттер в миллисекунды сохраняется. Для race window шириной в миллисекунды хватает, для микросекунд — может потребоваться 50-100 итераций.

Альтернатива: threading.Barrier с библиотекой requests — все потоки ждут на барьере и стартуют синхронно. Но для серьёзной работы с таймингами single-packet attack через Turbo Intruder надёжнее на порядок. Я пользуюсь Python-скриптом, когда нужно быстро проверить гипотезу, а Turbo Intruder — когда нужно стабильное воспроизведение.

Реальные CVE с race condition: от RCE до path traversal

Race condition уязвимости — не CTF-абстракция. Реальные CVE показывают, как TOCTOU-паттерны приводят к удалённому выполнению кода в продакшене.

CVE-2021-32648 — RCE в October CMS (CWE-362, CVSS 9.1 CRITICAL). В версиях до 1.0.476, 1.1.12 и 2.2.15 (пакет october/system в Packagist) метод October\Rain\Database\Attach\File::fromData позволял указать произвольное имя файла. Race condition во временной директории: файл создавался, проверялся на вредоносность, затем удалялся — но между созданием и удалением оставалось окно, в которое PHP-shell успевал отработать. Неаутентифицированный атакующий (PR:N) загружал shell, а пока сервер «чистил» опасный файл — shell уже выполнился. EPSS-оценка: 0.0142, percentile 71.77% — выше медианы.

CVE-2024-2120 — RCE в Autoptimize (WordPress) (CWE-362). Плагин пытался удалять вредоносные файлы из загруженного через «Import Settings» архива, но race condition позволяла исполнить файл между извлечением на диск и удалением. Характерно: патч закрыл прямую загрузку, но race condition в процессе очистки осталась. Типичная ситуация — «починили входную дверь, забыли про окно».

CVE-2023-24042 — path traversal в LightFTP (CWE-362, CVSS 7.5 HIGH, CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H). Race condition в LightFTP до версии 2.2: handler-поток использовал перезаписанный context->FileName из-за гонки при обработке malformed FTP-запроса. CISA SSVC отмечает статус exploitation: poc, однако общедоступного PoC в Exploit-DB на момент написания не найдено — воспроизведение потребует самостоятельной разработки эксплойта по описанию CVE.

Общий паттерн во всех трёх CVE: взаимодействие с пользователем не требуется (UI:N), но остальные параметры различаются. В October CMS и Autoptimize привилегии не нужны (PR:N) при высокой сложности (AC:H); в LightFTP — PR:L/AC:H, в GitLab — PR:L/AC:L. Race condition — атака на тайминг, но требования к привилегиям зависят от конкретного приложения.

TOCTOU на уровне базы данных в CTF

В CTF-задачах среднего и высокого уровня race condition часто завязана на уровень изоляции транзакций БД. Стандартный READ COMMITTED (дефолт PostgreSQL) не защищает от TOCTOU: два запроса в разных транзакциях читают одно значение баланса, оба проверяют «достаточно средств», оба списывают — баланс уходит в минус.

Уязвимый паттерн: SELECT balance FROM accounts WHERE id = 1 → проверка в коде приложения → UPDATE accounts SET balance = balance - 800 WHERE id = 1. Между SELECT и UPDATE — race window. Защитный вариант: SELECT balance FROM accounts WHERE id = 1 FOR UPDATE блокирует строку до COMMIT. Второй запрос ждёт, пока первый завершится, видит обновлённый баланс и получает корректный отказ.

Другой защитный механизм — оптимистическая блокировка с version-полем: UPDATE accounts SET balance = 200, version = 6 WHERE id = 1 AND version = 5. Если affected_rows = 0 — строку изменили раньше. В CTF этот паттерн обычно отсутствует (иначе задача не решается через race condition). Ищите в исходном коде SELECT без FOR UPDATE перед критическим UPDATE — это почти гарантированно уязвимая точка.

Дополнительный нюанс: SERIALIZABLE уровень изоляции предотвращает большинство race condition на уровне БД, но требует retry-логики для обработки serialization failure (ошибка 40001 в PostgreSQL) и редко используется из-за влияния на производительность. В CTF-задачах с PostgreSQL или MySQL смотрите конфиг-файлы — если уровень изоляции не задан явно, действует дефолтный READ COMMITTED. Это ваш шанс.

Методология поиска race condition: predict, probe, prove

Методология PortSwigger (Tier-1 research) для систематического поиска race condition — три шага. Работает одинаково на CTF, в bug bounty и на пентесте.

Predict — предсказать потенциальные коллизии. Определите эндпоинты, которые пишут или читают общие данные. Для каждого — три вопроса. Первый: где хранится состояние? Серверная БД — идеальный кандидат для эксплуатации. JWT на клиенте — бесполезно, нет общего серверного состояния. Состояние в сессии — частично защищено session-locking (PHP блокирует файл сессии на время запроса). Второй: операция редактирует или добавляет? Редактирование (UPDATE) создаёт потенциал для коллизий, добавление (INSERT) уязвимо только к limit overrun. Третий: какой ключ использует операция? Нужны два запроса с одним ключом — username, token, coupon code.

Probe — искать подсказки. Отправьте группу из 10-20 параллельных запросов к потенциально уязвимому эндпоинту через Burp Repeater (Send group in parallel). Ищите любые аномалии: разные HTTP-коды в ответах, различное содержание email-уведомлений, изменения в данных сессии. Один ответ 200 среди девятнадцати 403 — подсказка. Два письма с разными токенами вместо одного — подсказка. Разный текст ошибки в одном из ответов — тоже. На этом этапе не нужна стабильность, нужен хотя бы один аномальный ответ.

Prove — доказать концепцию. Сократите запросы до минимума (обычно 2-3) для стабильного воспроизведения. Если при двух запросах PoC нестабилен — увеличьте количество попыток или вернитесь к двадцати. Для CTF нестабильность допустима: хватает одного успешного срабатывания из десяти итераций.

Отдельная история — Time Sensitive Attacks. Не TOCTOU в чистом виде, но техника синхронизации та же. Если токен сброса пароля генерируется на основе timestamp, а не криптографически случайной строки — два параллельных запроса сброса для разных пользователей могут создать одинаковый токен. Для CTF: если видите, что два reset-токена совпали — перед вами генерация на основе времени, и race condition позволяет перехватить чужой токен.

Race condition остаётся одной из самых недооценённых категорий уязвимостей, и причина конкретная: ни один DAST-сканер не умеет надёжно детектировать TOCTOU-паттерны — нужно понимание бизнес-логики и ручная работа с таймингами. До 2023 года эксплуатация race condition в вебе считалась ненадёжной: сетевой джиттер делал результат непредсказуемым. После публикации single-packet attack на Black Hat USA 2023 ситуация изменилась радикально — то, что раньше требовало идеальных сетевых условий, стабильно воспроизводится с четырёхмикросекундным разбросом на любом HTTP/2-сервере. Думаю, в ближайшие год-два race condition тестирование на проникновение станет стандартным чекпоинтом в методологии — наравне с IDOR и SSRF. Пока разработчики не думают о гонках (тесты запускаются в один поток, QA не проверяет параллельные запросы), это низковисящий фрукт для каждого, кто владеет техникой синхронизации. На WAPT эту цепочку проходят в течение двух модулей с лабами — для тех, кому writeup мало и нужна прогрессия с разбором от ментора.

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

Поделиться

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

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

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

Читайте также

Wireshark для начинающих: фильтры и анализ трафика CTF

12 мин.

4

Wireshark для начинающих: фильтры и анализ трафика CTF

14 000 пакетов, один фильтр, 10 секунд до флага. Разбор pcap в Wireshark: 12 display-фильтров, анализ HTTP/DNS/FTP, ARP-спуфинг и CyberChef для CTF

10 ОКТЯБРЬ, 2026

SQL-инъекции в CTF: от обнаружения до blind SQLi

12 мин.

6

SQL-инъекции в CTF: от обнаружения до blind SQLi

Пошаговый разбор SQL-инъекций в CTF: UNION, error-based, boolean и time-based blind техники, автоматизация на Python, обход WAF и sqlmap. Реальные payload'ы.

10 ОКТЯБРЬ, 2026

XXE уязвимость: как эксплуатировать в CTF

13 мин.

10

XXE уязвимость: как эксплуатировать в CTF

7 техник эксплуатации XXE: classic, blind OOB, error-based, SVG upload, XInclude, Content-Type switch. Пошаговые пейлоады с Burp Suite и таблица выбора.

9 ОКТЯБРЬ, 2026