Race conditions
Race conditions
Состояния гонки — тип уязвимостей, тесно связанный с изъянами бизнес-логики. Они возникают, когда сайты обрабатывают запросы конкурентно друг другу без достаточных мер защиты. В результате несколько независимых потоков одновременно взаимодействуют с одними и теми же данными, что приводит к «столкновению» и непреднамеренному поведению приложения. Атака состояния гонки использует точно выверенное по времени отправление запросов, чтобы вызвать такие преднамеренные столкновения и эксплуатировать это непреднамеренное поведение для атак.

Промежуток времени, в течение которого возможно столкновение, называется «окном гонки» (race window). Это может быть доля секунды между двумя взаимодействиями с базой данных, например.
Как и в случае других логических изъянов, влияние состояния гонки сильно зависит от приложения и конкретной функциональности, в которой оно возникает.
Состояния гонки для превышения лимитов (Limit overrun)
Наиболее известный тип состояния гонки позволяет превысить некоторый лимит, установленный бизнес-логикой приложения.
Например, рассмотрим интернет-магазин, который позволяет ввести промокод на этапе оформления заказа, чтобы получить разовую скидку. Для применения скидки приложение может выполнять следующие шаги:
Проверить, что вы ещё не использовали этот код.
Применить скидку к итоговой сумме заказа.
Обновить запись в базе данных, чтобы отразить факт использования кода.
Если вы позже попытаетесь использовать код повторно, начальные проверки в начале процесса должны помешать этому:

Теперь представьте, что пользователь, который ещё ни разу не применял этот промокод, попытался применить его дважды почти одновременно:

Как видно, приложение проходит через временное подсостояние (sub-state); то есть состояние, в которое оно входит и из которого выходит до завершения обработки запроса. В этом случае подсостояние начинается, когда сервер начинает обрабатывать первый запрос, и заканчивается, когда он обновляет базу данных, чтобы указать, что вы уже использовали код. Это создаёт небольшое окно гонки, в течение которого можно повторно получать скидку столько раз, сколько угодно.
Существуют многие вариации такого рода атаки, включая:
Многократное погашение подарочной карты
Многократную оценку товара
Снятие или перевод средств сверх баланса счёта
Повторное использование одного решения CAPTCHA
Обход ограничения скорости (rate limit) при защите от перебора
Превышение лимитов — это подтип так называемых изъянов «от момента проверки до момента использования» (time-of-check to time-of-use, TOCTOU).
Выявление и эксплуатация состояний гонки для превышения лимитов с помощью Burp Repeater
Процесс выявления и эксплуатации состояний гонки для превышения лимитов относительно прост. В общих чертах вам нужно:
Найти одноразовую или ограниченную по скорости конечную точку, которая влияет на безопасность или полезна иным образом.
Отправить несколько запросов к этой конечной точке в быстром темпе, чтобы проверить, удастся ли превысить лимит.
Главная сложность — синхронизировать запросы так, чтобы хотя бы два окна гонки совпали, вызвав столкновение. Это окно часто исчисляется миллисекундами и может быть ещё меньше.
Даже если вы отправите все запросы в точности одновременно, на практике есть различные неконтролируемые и непредсказуемые внешние факторы, влияющие на то, когда сервер обработает каждый запрос и в каком порядке.

Burp Suite 2023.9 добавляет мощные возможности в Burp Repeater, позволяющие легко отправлять группу параллельных запросов способом. Burp автоматически подбирает технику с учётом версии HTTP, поддерживаемой сервером:
Для HTTP/1 используется классическая синхронизация «по последнему байту» (last-byte synchronization).
Для HTTP/2 используется техника «атаки одним пакетом» (single-packet attack), впервые показанная PortSwigger Research на Black Hat USA 2023.
Атака одним пакетом позволяет полностью нейтрализовать влияние network jitter, используя один TCP-пакет для завершения 20–30 запросов одновременно.

Хотя зачастую достаточно двух запросов, отправка большого количества запросов помогает компенсировать внутренние задержки, известные как «server-side jitter». Это особенно полезно на этапе первичного обнаружения. Эту методологию мы рассмотрим подробнее.
Выявление и эксплуатация состояний гонки для превышения лимитов с Turbo Intruder
Помимо нативной поддержки атаки одним пакетом в Burp Repeater, также улучшили расширение Turbo Intruder для поддержки этой техники. Вы можете скачать последнюю версию из BApp Store.
Turbo Intruder требует некоторого владения Python, но подходит для более сложных атак, например, тех, что требуют множественных повторов, ступенчатого тайминга или крайне большого числа запросов.
Чтобы использовать атаку одним пакетом в Turbo Intruder:
Убедитесь, что целевой хост поддерживает HTTP/2. Атака одним пакетом несовместима с HTTP/1.
Установите параметры конфигурации движка запросов
engine=Engine.BURP2иconcurrentConnections=1.При постановке запросов в очередь сгруппируйте их, назначив им именованные «ворота» с помощью аргумента
gateметодаengine.queue().Чтобы отправить все запросы в группе, откройте соответствующую возможность методом
engine.openGate().
Подробнее см. шаблон race-single-packet-attack.py в каталоге примеров по умолчанию Turbo Intruder.
Скрытые многошаговые последовательности
На практике один запрос может запускать целую многошаговую последовательность «за кулисами», переводя приложение через несколько скрытых состояний, в которые оно входит и из которых выходит до завершения обработки запроса. Мы будем называть их «подсостояниями» (sub-states).
Если вы можете определить один или несколько HTTP-запросов, которые взаимодействуют с одними и теми же данными, вы потенциально можете воспользоваться этими подсостояниями, чтобы обнаружить чувствительные ко времени вариации логических изъянов, распространённых в многошаговых процессах. Это позволяет эксплуатировать состояния гонки, выходящие далеко за пределы превышений лимитов.
Например, вам может быть знакома проблемная реализация многофакторной аутентификации — MFA (многофакторная аутентификация), которая позволяет выполнить первую часть входа с известными учётными данными, а затем пройти непосредственно в приложение с помощью принудительного перехода (forced browsing), фактически обходя MFA полностью.
Следующий псевдокод демонстрирует, как сайт может быть уязвим Race Conditions:
Как видно, это на самом деле многошаговая последовательность в рамках одного запроса. Важно то, что она проходит через подсостояние, в котором пользователь временно имеет валидную сессию входа, но MFA ещё не применяется. Атакующий потенциально может эксплуатировать это, отправляя запрос на вход вместе с запросом к чувствительной, требующей аутентификации конечной точке.
Методология
Чтобы выявлять и эксплуатировать скрытые многошаговые последовательности, рекомендуется использовать следующую методологию, кратко изложенную в докладе Smashing the state machine: The true potential of web race conditionsот PortSwigger Research.

1 — Predict potential collisions
Тестировать каждую конечную точку непрактично. После обычного покрытия целевого сайта вы можете сократить число конечных точек для тестирования, задавая себе следующие вопросы:
Является ли конечная точка критичной для безопасности? Многие конечные точки не затрагивают критичные функции, поэтому их тестирование нецелесообразно.
Есть ли потенциал столкновения? Для успешного столкновения обычно нужны два и более запроса, запускающих операции над одной и той же записью. Например, рассмотрим следующие варианты реализации сброса пароля:

В первом примере параллельные запросы на сброс пароля для двух разных пользователей вряд ли приведут к столкновению, так как изменяются две разные записи. Однако во второй реализации вы можете редактировать одну и ту же запись запросами для двух разных пользователей.
2 — Probe for clues
Чтобы определить точку для атаки, сначала нужно установить базовый уровень поведения конечной точки в нормальных условиях. Это можно сделать в Burp Repeater, сгруппировав все запросы и используя опцию Send group in sequence (separate connections). Подробнее см.: Sending requests in sequence.
Затем отправьте ту же группу запросов одновременно, используя атаку одним пакетом (или синхронизацию по последнему байту, если HTTP/2 не поддерживается), чтобы минимизировать сетевой джиттер. В Burp Repeater это делается опцией Send group in parallel. Подробнее см.: Sending requests in parallel. Либо используйте расширение Turbo Intruder из BApp Store.
Любые отклонения могут быть зацепкой. Просто ищите любую форму отличную от того, что вы наблюдали при корректном тестировании. Это включает изменения в одном или нескольких ответах, а также вторичные эффекты, такие как иное содержимое писем или видимое изменение поведения приложения после теста.
3 - Prove the concept
Постарайтесь понять, что происходит, уберите лишние запросы и убедитесь, что эффект воспроизводим.
Продвинутые Race Conditions могут приводить к необычным и уникальным примитивам, поэтому путь к максимальному воздействию не всегда очевиден. Может помочь представление каждого состояния гонки как структурной слабости, а не изолированной уязвимости.
Состояния гонки между несколькими конечными точками
Возможно, самая интуитивная форма таких уязвимостей — когда вы отправляете запросы к нескольким конечным точкам одновременно.
Вспомните классический изъян логики в интернет-магазинах: вы добавляете товар в корзину, оплачиваете, затем добавляете ещё товары и через принудительный переход попадаете на страницу подтверждения заказа.
Вариация этой уязвимости может возникать, когда проверка платежа и подтверждение заказа выполняются в рамках обработки одного запроса. State Machine статуса заказа может выглядеть примерно так:

В этом случае вы потенциально можете добавить больше товаров в корзину в окно гонки между моментом, когда платеж проверен, и моментом окончательного подтверждения заказа.
Выравнивание окон гонки между несколькими конечными точками
При тестировании таких состояний гонки вы можете столкнуться с проблемами синхронизации окон гонки для каждого запроса, даже если отправляете их строго одновременно с помощью техники одного пакета.

Эта распространённая проблема обусловлена в основном двумя факторами:
Задержки, вызванные сетевой архитектурой. Например, задержка при установлении фронтендом нового соединения с бэкендом. Протокол также может существенно влиять.
Задержки, вызванные обработкой конкретных конечных точек. Разные конечные точки по природе различаются по времени обработки, иногда значительно, в зависимости от выполняемых операций.
К счастью, есть потенциальные обходные пути для обеих проблем.
Connection Warming
Задержки соединений с бэкендом обычно не мешают атакам состояния гонки, потому что они задерживают параллельные запросы примерно одинаково, и те остаются синхронными. Важно уметь отличать такие задержки от задержек, вызванных логикой конкретных конечных точек. Один из способов — Подготовить соединение одним или несколькими несущественными запросами и посмотреть, выровняются ли оставшиеся времена обработки. В Burp Repeater попробуйте добавить GET-запрос к главной странице в начало группы вкладок, затем использовать Send group in sequence (single connection).
Если первый запрос всё ещё обрабатывается дольше, но остальные теперь обрабатываются в небольшой временной вилке, можно игнорировать кажущуюся задержку и продолжать тест как обычно.
Если вы всё ещё видите нестабильные времена ответа на одной конечной точке, даже при использовании техники одного пакета, это указывает на то, что задержка бэкенда мешает атаке. Можно попробовать обойти это, отправив запросы через Turbo Intruder перед основными атаками.
Использование лимитов скорости или ресурсов
Если Connection Warming соединения не помогает, есть разные решения. В Turbo Intruder вы можете ввести небольшую задержку на стороне клиента. Однако это подразумевает разделение ваших атакующих запросов на несколько TCP-пакетов, что исключает использование атаки одним пакетом.

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

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

Обратите внимание на конечное состояние после завершения всех операций:
session['reset-user'] = victimsession['reset-token'] = 1234
Сессия теперь содержит идентификатор жертвы, но валидный токен сброса отправлен атакующему.
Подтверждения email-адресов и любые операции по email обычно являются хорошей целью для состояний гонки на одной конечной точке. Письма часто отправляются в фоновом потоке после того, как сервер отдает HTTP-ответ клиенту, что повышает вероятность состояния гонки.
Механизмы блокировки на основе сессии
Некоторые фреймворки пытаются предотвратить случайную порчу данных, используя блокировку запросов. Например, встроенный модуль обработчика сессий PHP обрабатывает только один запрос на сессию одновременно.
Крайне важно замечать такое поведение, так как иначе оно может скрывать тривиально эксплуатируемые уязвимости. Если вы видите, что все ваши запросы обрабатываются последовательно, попробуйте отправлять каждый из них с разным сессионным токеном.
Состояния гонки из-за частичной конструкции объектов
Многие приложения создают объекты в несколько шагов, что может вводить временное промежуточное состояние, в котором объект уязвим.
Например, при регистрации нового пользователя приложение может создавать пользователя в базе и устанавливать его API-ключ двумя отдельными SQL-запросами. Это оставляет крошечное окно, в течение которого пользователь существует, но его API-ключ не инициализирован.
Такое поведение открывает путь к эксплойтам, когда вы подсовываете входное значение, возвращающее что-то, совпадающее с неинициализированным значением в базе (например, пустую строку или null в JSON), и это значение участвует в проверке безопасности.
Фреймворки часто позволяют передавать массивы и другие нестроковые структуры с помощью нестандартного синтаксиса. Например, в PHP:
param[]=fooэквивалентноparam = ['foo']param[]=foo¶m[]=barэквивалентноparam = ['foo', 'bar']param[]эквивалентноparam = []
Ruby on Rails позволяет сделать похожее, передав параметр запроса или POST с ключом без значения. Иными словами, param[key] приводит к следующему объекту на стороне сервера:
В примере выше это значит, что в окно гонки вы потенциально можете выполнить аутентифицированные API-запросы следующим образом:
Чувствительные ко времени атаки
Иногда вам может не удастся найти состояния гонки, но техники точной по времени доставки запросов всё же позволят выявить другие уязвимости.
Один из таких примеров — использование высокоточных меток времени вместо криптографически стойких случайных строк для генерации токенов безопасности. Рассмотрим токен сброса пароля, рандомизируемый только с помощью метки времени. В этом случае может быть возможно инициировать два сброса пароля для двух разных пользователей, которые оба используют один и тот же токен. Всё, что нужно — синхронизировать запросы так, чтобы они сгенерировали одинаковую метку времени.
Как предотвратить уязвимости состояний гонки
Когда один запрос может переводить приложение через невидимые подсостояния, понимать и предсказывать его поведение крайне сложно. Делать защиту на таком уровне — непрактично. Чтобы корректно защитить приложение, рекомендуется устранить подсостояния на всех чувствительных конечных точках, применяя следующие стратегии:
Избегайте смешивания данных из разных уровней хранения.
Убедитесь, что чувствительные конечные точки делают изменения состояния атомарными, используя возможности конкурентного доступа хранилища данных. Например, используйте одну транзакцию базы данных, чтобы проверить соответствие платежа сумме корзины и подтвердить заказ.
В качестве защиты в глубину (defense-in-depth) используйте возможности целостности и согласованности хранилища, например уникальные ограничения столбцов.
Не пытайтесь использовать один слой хранения данных для защиты другого. Например, сессии не подходят для предотвращения атак превышения лимитов в базах данных.
Убедитесь, что ваш фреймворк обработки сессий сохраняет внутреннюю согласованность сессий. Обновление переменных сессии по отдельности, а не батчем, может показаться оптимизацией, но это крайне опасно. Это относится и к ORM: скрывая такие концепции, как транзакции, они берут на себя полную ответственность за них.
В некоторых архитектурах может быть уместно вовсе избегать серверного состояния. Вместо этого можно зашифровать и перенести состояние на клиентскую сторону, например, используя JWT (JSON Web Token). Помните, что это несёт свои риски, которые подробно рассмотрены в нашей теме по Атакам на JWT.
Last updated