Race Condition для самых маленьких
@fefuctf @collapszВступление
Всем привет!
Весной 2024 в ходе решения Tinkoff CTF мне довелось столкнуться с достаточно интересной задачкой на Race Condition, а не так давно мне захотелось провести небольшой ресерч на эту тему, разобраться, как эта штука работает под капотом и одновременно с тем познакомить с ней вас.
Немного определений
Прежде чем перейти к сути Race Condition, необходимо ознакомиться с разницей однопоточности и многопоточности.
Многопоточность – свойство платформы или приложения, заключающееся в том, что любой процесс внутри этого приложения может быть представлен несколькими потоками, которые будут выполняться одновременно.
Однопоточность же, напротив, поддерживает только поочередное выполнение процессов без какого-либо потенциала для их одновременного выполнения.

Важно понимать, что понятие многопоточности, как и понятие Race Condition, являются универсальными и не относятся только к сфере веб-приложений.
Наглядно: представим, что у нас есть два человека (поток 1 и поток 2) и две задачи – заварить чай и приготовить бутерброды. В случае однопоточного приложения алгоритм действий будет следующим:
- Поток 1 заваривает чай
- Поток 1 ждет, пока чай заварится
- Поток 1 готовит бутерброды
- Поток 2 ничем не занят, пока поток 1 последовательно выполняет всю работу

В случае с многопоточного приложения оба потока будут работать одновременно:
- Поток 1 заваривает чай
- Поток 2 одновременно с тем готовит бутерброды
- Оба потока работают независимо друг от друга, пока не завершат свои задачи, тем самым ускоряя процесс выполнения необходимой работы
На сегодняшний день подавляющее большинство приложений поддерживают работу в многопотоке, что необходимо для ускорения работы и повышения производительности приложения в целом.
Переходя к сути
Теперь, когда у нас есть понимание логики работы многопотока, мы можем перейти к Race Condition. Что это вообще за зверь такой?

Race Condition, оно же состояние гонки – это состояние, когда два и более потоков пытаются применить операции чтения или записи к одним тем же ресурсам одновременно. Рассмотрим на примере:
Есть счет в некоем банке, на балансе этого счета лежит 500 рублей. Два человека, Алиса и Боб, одновременно проверяют баланс счета и видят одну и ту же картину – 500 рублей. Затем эти же два человека одновременно снимают со счета 500 рублей, тем самым входя в состояние гонки, в результате чего и Алиса, и Боб становятся обладателями 500 рублей, в то время как счет в банке уходит в минус и становится -500 рублей.
Прежде чем читать далее, попробуйте проанализировать произошедшую ситуацию и предположить, почему именно она произошла? Как именно работал алгоритм со стороны сервера и в чем произошла ошибка?
Причиной этому – небезопасная многопоточность, приводящая к возникновению RC. Давайте попробуем разобраться с причинами чуть более детально.
Race Condition под капотом
В рамках статьи ограничимся сферой веб-приложений, но, опять же, важно помнить, что явление RC применимо и во многих других сферах!
Итак, рассмотрим следующую ситуацию:
У нас есть промокод онлайн магазина и мы хотим им воспользоваться. Мы вводим промокод, отправляем запрос и видим пополнение средств в личном кабинете.
Рассмотрим алгоритм обработки такого запроса приложением:
- Мы отправляем POST-запрос с промокодом
- Приложение сверяется с базой данных и проводит простую проверку – действителен ли введенный промокод?
- Если промокод является действительным, приложение начисляет средства на баланс личного кабинета, затем меняет статус промокода на "недействительный"
- В противном случае выдает ошибку, содержащую информацию о том, что введенный промокод недействителен.

Однако что произойдет, если мы отправим два запроса с одинаковым действительным промокодом одновременно? Все верно, мы войдем в состояние гонки. И в случае, если сервер не сможет справиться с многопоточным запросом, мы сможем применить один и тот же промокод дважды, прежде чем он перейдет в статус "недействительный"

Для понимания того, как вообще работает отправка параллельных запросов, достаточно взглянуть на любой HTTP-запрос:

Можно обратить внимание, что в конце запроса следует двойной перенос строки – зачем?
\r\n\r\n
Именно по этому набору байт веб-сервер понимает, что HTTP-запрос является законченным и можно приступать к его обработке. Логика эксплуатации Race Condition заключается в отправке бОльшей части запроса без заключительного байта \n, без которого запрос не будет являться законченным и не начнет обрабатываться веб-сервером.
Таким образом формируется ряд идентичных запросов, к которым потом посылается заключительный байт, ведь отправить один байт намного быстрее, чем сформировать и отправить полноценный запрос.
После того, как заключительный байт будет послан к каждому пакету, сервер начнет обрабатывать все поступившие на обработку пакеты в многопоточном режиме, тем самым позволяя нам реализовать Race Condition.

Виды Race Condition
Обратимся к порталу Hacktricks – швейцарскому ножу любого пентестера. У RС существует ряд вариаций, каждая из которых отличается подходом и методологией.
- Limit Overrun. Наиболее популярный вид состояния гонки, который проявляется в тех случаях, когда приложение ограничивает количество раз выполнения того или иного действия, например: использование промокода, установка нескольких оценок на товар за раз, вывод или перевод средств с банковского аккаунта, переиспользование одной и той же CAPTCHA несколько раз и обход анти-брутфорс рейт лимитов.
- Time-sensitive attacks. Точность времени в запросах может привести к уязвимостям, если, например, для генерации токенов используются предсказуемые методы, такие как временные метки. При одновременных запросах могут быть сгенерированы одинаковые токены.
- Hidden and unintended substates. Эксплуатация более сложных RC зачастую подразумевает краткосрочное взаимодействие со скрытыми или непреднамеренными состояниями машины. Например, попадание в тайминг обработки совершенного первым потоком действия, которое вынуждает систему перейти в уязвимое состояние будет является реализацией данного вида RC. Практические примеры приведены все в той же статье на Hacktrics, среди них можно выделить обход 2FA и подтверждение почты с её одновременным изменением на другую.
Инструментарий. Burp Suite.
Безусловно, первое что приходит на ум – Burp Suite. В нем имеется штатный инструментарий, позволяющий реализовывать Limit Overrun состояния гонки. Для этого необходимо отправить в Repeater необходимое количество пакетов

Затем создать группу

И добавить туда все пакеты, которые мы будем использовать для реализации Race Condition

Далее, нажав на стрелочку рядом с Send, выбрать "Send group in parallel" (тот самый last byte – \n)

После, как и оговаривалось ранее, будет отправлена бОльшая часть запросов, затем к каждому из них будет послан заключительный байт и сервер начнет обработку всех поступивших в это тайминговое окно пакетов.
Burp Suite Turbo Intruder
Говоря о состоянии гонки, грех не упомянуть мощнейшее расширение для Burp Suite – Turbo Intruder. Для его установки нужно включить отображение вкладки Extensions:

Далее переходим в BApp Store и в поиске вбиваем Turbo Intruder, выбираем, нажимаем Install.

Ставим галочку во вкладке Installed

Turbo Intruder готов к использованию

Теперь пришло время поговорить о том, зачем вообще нужен Turbo Intruder, если и штатный функционал Burp Suite позволяет работать с RC:
- Во-первых, Turbo Intruder поддерживает огромное количество вариаций состояния гонки (все из них покрыть в рамках такой статьи будет проблематично)

- Во-вторых, он использует самописный стек HTTP, что значительно ускоряет работу с отправкой пакетов.
- В-третьих, конфигурация атак проводится на Python, что позволяет работать с более комплексными вариациями состояния гонки, модификацией пакетов и прочими прелестями, которыми располагает Python.
Race Condition с промокодами. Точка невозврата
Предлагаю ознакомиться с эксплуатацией состояния гонки на примере задания с codeby.games – Точка невозврата.
Заходим по адресу http://62.173.140.174:16023/

Проходим регистрацию и авторизуемся, видим поле ввода кода ваучера, баланс и стоимость флага

В коде приложения находим три тестовых ваучера

Однако использование всех трех ваучеров даст нам максимум 1300$, поэтому не будем медлить и сразу перейдем к эксплуатации состояния гонки!
Вводим тестовый ваучер, перехватываем запрос и отправляем его в Turbo Intruder
Выбираем race-single-packet-attack, меняем BURP2 на BURP, т.к. работаем с HTTP и начинаем атаку

Однако после атаки можно увидеть, что промокод отработал только один раз, почему так?

Причина кроется в защитном механизме PHP – Session based blocking mechanism, который является механизмом защиты от DoS (Denial of Service) атак и работает на уровне сессии пользователя. Он ограничивает количество запросов, которые пользователь может отправить за определенный период времени, для предотвращения перегрузки сервера.
Для того, чтобы обойти данный механизм, достаточно удалить сессионный идентификатор пользователя, в данном случае – куку
Берем новый код, отправляем запрос в Turbo Intruder, ставим race-single-packet-attack, удаляем заголовок Cookie со всем содержимым и начинаем атаку

На этот раз атака прошла успешно и один промокод на 100$ увеличил наш баланс с 1100$ до 1400$, а значит состояние гонки было успешно проэксплуатировано!

Аналогичным образом задание решается и с использованием штатного функционала Burp Suite – Send in parallel, однако в таком случае будьте готовы создавать большое количество запросов в Repeater ;)
Race Condition - это сложная и многогранная тема, требующая глубокого понимания как теоретических, так и практических аспектов. Ее освоение требует времени и усилий, но наградой за это станет прочный фундамент для создания стабильных и надежных систем.
Продолжайте исследовать, экспериментировать и практиковаться.
До новых встреч!

Ресурсы для самостоятельного изучения: