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

11 мин.00

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

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

Куртка за €1 337, на балансе — €50. Двадцать параллельных запросов на применение одноразового промокода PROMO20 через single-packet attack в Turbo Intruder — и финальная цена падает до €37.62. Купон сработал не один раз, а столько, сколько запросов проскочило: сервер просто не успел пометить его использованным до того, как обработал всю пачку. Это лаба PortSwigger «Limit overrun race conditions», и такие же задачи регулярно всплывают на CTF. Race condition эксплуатация — навык, который превращает «непредсказуемый баг» в воспроизводимую атаку. Но только если понимаешь механику race window и не пытаешься ловить тайминг на глаз.

Три типа race condition в веб-задачах

PortSwigger на Black Hat USA 2023 показали (whitepaper «Smashing the state machine: The true potential of web race conditions»), что race condition в вебе — это не только «применить купон дважды». Три основных типа покрывают большинство CTF-сценариев и баунти-отчётов. Подробнее — в нашем обзоре пентест веб-приложений.

Limit Overrun — TOCTOU уязвимость

Самый частый тип 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 встречается в задачах на:

  • Погашение подарочных карт и бонусных баллов многократно
  • Обход rate-limit при brute-force OTP-кодов
  • Двойное списание или начисление баланса
  • Повторное использование CAPTCHA-решения
  • Многократное голосование или выставление оценок

В баунти-программах этот класс приносит стабильные выплаты. На HackerOne (report #759247) описан ровно такой сценарий: одна подарочная карта погашалась параллельными запросами и применялась многократно. По данным PortSwigger, limit overrun — подтип TOCTOU-уязвимостей, но есть и более хитрые разновидности.

Single-endpoint Collision

Более тонкий тип гонки потоков в веб-приложении. Два запроса к одному эндпоинту одновременно меняют одно и то же поле. Второй перезаписывает данные первого до того, как первый их использует.

Классический пример — 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. Механика:

  1. Поток A записывает pending_email = "attacker@evil.com"
  2. Поток B перезаписывает pending_email = "victim@target.com"
  3. Поток A читает pending_email для генерации токена — а там уже victim@target.com
  4. Поток A отправляет письмо на attacker@evil.com, но токен привязан к victim@target.com

Атакующий получает на свою почту токен подтверждения для чужого email. Это ведёт к подделке верифицированных email и захвату аккаунтов при использовании GitLab как OAuth-провайдера. По данным CISA (SSVC Decision: Track), эксплуатация не автоматизируема и технический импакт partial — но в CTF-задачах single-endpoint collision составители обожают. У PortSwigger есть отдельная лаба на этот вектор.

Multi-endpoint Race

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

Многопоточная атака на веб-приложение с multi-endpoint race усложняется тем, что разные эндпоинты обрабатываются с разной скоростью. Тут нужны дополнительные приёмы синхронизации:

  • Padding быстрого эндпоинта — добавляем «мусорные» параметры или заголовки, чтобы выровнять время обработки с медленным эндпоинтом
  • Connection warming — предварительные запросы прогревают серверный кеш и соединение, снижая разброс задержек
  • HTTP/2 мультиплексирование — отправка через одно соединение минимизирует разброс по времени доставки

Single-packet attack: как нейтрализовать сетевой джиттер

Главный враг эксплуатации состояния гонки на удалённом сервере — сетевой джиттер. Вариация задержки в сети — десятки миллисекунд, а race window часто измеряется единицами. Без минимизации джиттера гонка запросов HTTP превращается в лотерею с паршивыми шансами.

Last-byte synchronization для HTTP/1

Для серверов на HTTP/1.1 применяется техника last-byte synchronization:

  1. Открываем N параллельных TCP-соединений к серверу
  2. По каждому отправляем весь HTTP-запрос кроме последнего байта тела
  3. Сервер удерживает частично полученные запросы в буфере — обработка не начинается
  4. Одновременно отправляем последний байт по всем N соединениям
  5. Сервер получает N «завершённых» запросов практически в один момент

Джиттер влияет только на доставку одного байта через уже установленное TCP-соединение — на порядки меньше, чем доставка целого запроса. Burp Suite автоматически применяет эту технику при параллельной отправке на HTTP/1 серверы.

Ограничение: каждый запрос требует отдельного TCP-соединения. При 20-30 запросах это 20-30 параллельных соединений, и можно упереться в серверные лимиты MaxClients или firewall-правила. В CTF обычно проблем нет, в продакшене — как повезёт.

Single-packet техника для HTTP/2

HTTP/2 поддерживает мультиплексирование — несколько запросов через одно TCP-соединение. Single-packet attack race condition использует именно это:

  1. Формируем N полных HTTP/2-запросов
  2. Удерживаем финальные фрагменты каждого — сервер ждёт завершения
  3. Упаковываем все финальные фрагменты в один TCP-пакет
  4. Отправляем — сервер получает все запросы в рамках одной сетевой операции

Эту технику впервые показали исследователи 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 и Turbo Intruder для race condition

Burp Repeater — Send group in parallel

С версии Burp Suite 2023.9 в Repeater появилась встроенная поддержка параллельной отправки. Для базовой эксплуатации race condition в CTF этого хватает:

  1. Перехватываем целевой запрос через Proxy (например, POST /apply-coupon с телом code=PROMO20)
  2. Отправляем в Repeater, дублируем вкладку 20-25 раз через Ctrl+R
  3. Выделяем все вкладки, правый клик — «Send group in parallel»
  4. Смотрим ответы: если несколько запросов вернули HTTP 200 с подтверждением скидки — race condition подтверждена

Burp сам выбирает технику синхронизации по протоколу. HTTP/2 — single-packet attack. HTTP/1 — last-byte synchronization.

Момент, который часто упускают: если первая попытка не сработала, это не значит, что race condition нет. Серверный джиттер (GC-паузы, блокировки пула потоков, кеш-промахи) может сдвинуть обработку так, что запросы не попадут в одно race window. Повторяйте 5-10 раз, прежде чем отбрасывать гипотезу. В CTF обычно хватает 3-5 попыток.

Turbo Intruder: скрипт single-packet атаки

Для сценариев посложнее — 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.BURP
  • concurrentConnections=1 — одно TCP-соединение, все запросы через мультиплексирование. Для HTTP/1 ставьте 10-30
  • gate='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% задач.

Race condition уязвимость: примеры из реальных CVE

Чтобы понять, к чему приводит эксплуатация состояния гонки за пределами 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) — при двойном списании и обходе платёжных лимитов.

Методология поиска race condition в CTF-задачах

Методология PortSwigger строится в три этапа: предсказание, зондирование, подтверждение.

Этап 1 — предсказываем коллизии. Ищем эндпоинты с конкурентным доступом к ресурсам. В CTF-задачах это:

  • Формы с одноразовыми действиями: применение купона, активация инвайта, отправка решения
  • Эндпоинты с паттерном «прочитать → проверить → записать» (баланс, лимиты, счётчики)
  • Смена email/пароля с токенами подтверждения
  • Любые операции с пометкой «один раз» или rate-limit

Для каждого кандидата — три вопроса. Где хранится состояние? Серверная БД — идеальна для эксплуатации. Клиентский JWT — бесполезно. Редактирование или добавление? Операции, меняющие существующие данные (смена email), дают больше потенциала коллизий. Общий ключ? Для успешной гонки нужны две операции, обращающиеся к одному ключу — одному user_id, одному coupon_code.

Этап 2 — ищем подсказки. Отправляем пачку из 20-30 параллельных запросов и сравниваем ответы с нормальным (последовательным) поведением:

  • Разные HTTP-коды в ответах на идентичные запросы (часть 200, часть 409 или 403)
  • Различия в содержимом: разные значения баланса, разные redirect-URL
  • Побочные эффекты: два письма вместо одного, удвоенная запись в профиле

Любое отклонение от ожидаемого — подсказка. Авторы 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 комментариев

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

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

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

Уязвимости JWT токенов в CTF: от alg none до RCE

13 мин.

5

Уязвимости JWT токенов в CTF: от alg none до RCE

Пошаговые PoC для 7 атак на JWT: alg none, brute force, algorithm confusion, kid injection. Команды jwt_tool и hashcat для решения CTF-задач.

16 СЕНТЯБРЬ, 2026

Format string уязвимость: от %p до shell

15 мин.

5

Format string уязвимость: от %p до shell

Разбор format string в CTF: поиск offset, утечка libc через %p, побайтовая запись через %hhn, GOT overwrite с pwntools — полная цепочка эксплуатации

15 СЕНТЯБРЬ, 2026

Command Injection в CTF: поиск и эксплуатация

11 мин.

9

Command Injection в CTF: поиск и эксплуатация

Пошаговый разбор OS command injection в CTF-задачах: от обнаружения уязвимости до reverse shell. Обход фильтров, blind injection, ANSI-C нотация, payload'ы.

15 СЕНТЯБРЬ, 2026