Перейти к содержимому
← Каталог php Race Condition

Promo Code Race

Состояние гонки при активации промокодов (PHP).

mediumphpPro
Задача
# Race Condition: TOCTOU при активации промокода ## Сценарий SecureShop — интернет-магазин на PHP, в котором реализована система промокодов: при активации действующего кода баланс пользователя пополняется на фиксированную сумму. По задумке каждый промокод должен активироваться **только один раз на одного пользователя**, однако в реализации обработчика `/cart/coupon` есть классическая уязвимость **Race Condition (TOCTOU)** — Time-of-Check to Time-of-Use. Между моментом проверки «использовал ли уже этот пользователь данный купон?» и моментом записи факта использования существует временной промежуток. В этот промежуток несколько параллельных запросов могут одновременно пройти проверку — каждый из них увидит, что купон ещё не был применён, — и независимо друг от друга начислят бонус. В результате одноразовый купон срабатывает многократно, а на счёте появляется сумма, кратная номиналу купона. ## Цели 1. Исследуйте логику обработчика применения промокода — как сервер проверяет, что купон ещё не использован, и как фиксирует факт его использования. 2. Найдите способ многократно активировать один и тот же промокод от лица одного пользователя — без подмены параметров, без обхода CSRF, исключительно за счёт свойств параллельной обработки запросов. 3. Накопите достаточно средств для покупки служебного товара **CTF Flag** ($9999), оформите заказ и получите флаг со страницы профиля. ## Теория **TOCTOU (Time-of-Check Time-of-Use)** — класс уязвимостей гонок состояний, при которых между моментом проверки условия и моментом фактического использования его результата существует временное окно, в которое состояние может измениться. Типичный паттерн — «прочитать → проверить → действовать», когда эти шаги выполняются как отдельные SQL-запросы без транзакции и без блокировки строки. При параллельном доступе несколько запросов успевают пройти шаг проверки до того, как любой из них завершит запись, и все они действуют на основании одного и того же устаревшего состояния. **Уязвимый PHP-паттерн** в обработчике применения купона: проверка факта использования и фактическая запись о применении вынесены в две отдельные SQL-операции. Внутри обработчика последовательность такая — сначала read-запрос проверяет, есть ли уже запись в таблице использованных купонов для этой пары `(user_id, code)`, и только потом отдельный write-запрос фиксирует использование и начисляет бонус. Между read и write нет атомарного контекста. CHECK и USE — два **независимых** SQL-запроса. Между ними нет ни транзакции с немедленным захватом write-lock, ни блокировки строки, ни UNIQUE-constraint'а на паре `(user_id, code)`. SQLite в WAL-режиме допускает множественные одновременные read-запросы, поэтому все параллельные клиенты успевают прочитать «не использовано» ещё до того, как любой из них выполнит INSERT в таблицу использованных купонов. При параллельной отправке N запросов на `/cart/coupon` сервер обрабатывает их в разных процессах PHP-FPM / потоках. Все они успевают выполнить шаг CHECK до того, как любой завершит USE; все видят «не использовано»; все проходят проверку; все вызывают INSERT + UPDATE баланса. Итог: одноразовый промокод срабатывает столько раз, сколько запросов попало в окно — баланс растёт кратно номиналу купона. ## Точка входа атаки | Параметр | Значение | |----------|----------| | Учётные данные | `demo` / `demo` (начальный баланс $500) | | Уязвимый endpoint | `POST /cart/coupon` (параметры `code`, `csrf_token`) | | Промокоды | `PROMO50` ($50), `PROMO100` ($100) | | Целевой товар | CTF Flag за $9999 | | Завершение | `POST /cart/add` → `POST /cart/checkout` → `GET /profile` | ## Подсказки к среде - Сессия хранится в cookie `session_token`; CSRF-токен находится в HTML-странице `/profile` в поле `name="csrf_token"`. - Внутри `redeemCoupon()` стоит `usleep(300000)` — искусственная задержка 300 мс перед записью использования, имитирующая обращение к внешнему сервису. Эта задержка расширяет окно гонки и упрощает эксплуатацию. - Эндпоинт возвращает HTTP 302 на `/profile` или `/cart` независимо от результата — судите об успехе атаки по изменению баланса.
🚧 Сайт в разработке. Полный функционал пока недоступен. Все вопросы — support@hackandfix.ru