Как я и люблю - мы начнем с условных основ и будем двигаться постепенно всё глубже и глубже. Ближе к концу разберём, как эксплуатировать. При написании статьи было использовано много разных источников. К чему это? А к тому, что я начну со слов, которые нашел в статье у Бума - про него будет дальше. Что ж, приятного чтения.
Помните, что использование полученных знаний и навыков должно быть ограничено законными и этическими рамками, и вмешательство в чужие сети без разрешения является неприемлемым и незаконным действием.
Оглавление
Как это устроено
Заходит хакер в кальянную, квест и бар, а ему — у вас race condition!
Омар Ганиев
Для начала, как и везде - определение.
Race condition (состояние гонки) - это ситуация, при которой несколько потоков (или процессов) одновременно пытаются выполнить операции чтения или записи к общим ресурсам без должной синхронизации. Представить можно в формате очереди, один за одним.
![Фотография не несёт негативный характер Фотография не несёт негативный характер](https://habrastorage.org/r/w1560/getpro/habr/upload_files/dea/f5b/e75/deaf5be75c7e3200796447a74f552533.jpg)
Сложно? Я Вас понимаю, при разборе тем всегда так. Попробуем разобрать пример из жизни:
Представьте площадку, аналогичную Я.маркет, где присутствуют купоны на скидку в 10% «best10». Два потока могут одновременно запросить базу данных и подтвердить, что код скидки «best10» не был применен к корзине, затем оба попытаются применить скидку, в результате чего, она будет применена дважды. Обратите внимание на то, что «гоночные» условия не ограничиваются конкретной архитектурой веб‑приложений. Проще всего рассуждать о многопоточном приложении с одной базой данных, но в более сложных системах состояние обычно хранится в еще большем количестве мест.
Однопоточные системы, такие как NodeJS, чуть менее уязвимы, но все равно есть вероятность возникновения подобных проблем. В прошлой статье, где я разбирал концепцию race condition в WS, меня поправили, что там не зависит именно от NodeJS, и меня это заинтересовало. И я стал искать то, где можно изучить подобное на примере, а лучше увидеть куском кода... И нашел. Статья, хоть и 2021, но показывает, как происходит состояние гонки в NodeJS. Самое интересное там происходит в комментариях, как и всегда на habr‑е.
Типичное состояния гонки
![](https://habrastorage.org/getpro/habr/upload_files/40b/1b2/f4c/40b1b2f4cd5709d74f8f1014a1224b3a.jpg)
Почти все программы и веб-приложения сегодня используют так называемую «многопоточную» обработку, при которой они способны выполнять несколько действий одновременно. Хоть это и позволяет приложениям работать значительно быстрее - это приводит к возникновению потенциальных ошибок, если более одного процесса (или «потока») пытаются одновременно получить доступ к одним и тем же данным.
Как правило, состояние гонки протекают по определенной схеме:
Критическая секция: Это участок кода, в котором происходит обращение к общим ресурсам и их модификация.
Синхронизация: Отсутствие надлежащих механизмов синхронизации может позволить нескольким процессам одновременно войти в критическую секцию.
Непредсказуемый результат: Из-за одновременного выполнения конечное состояние общего ресурса становится неопределенным.
Например, когда система SQL выполняет обновление базы данных, во время процесса обновления она создает временный файл. Именно этот временный файл со временем заменяет данные в базе данных. При своевременной атаке злоумышленники могут подменить временный файл SQL-обновления таблицы административного доступа на свой собственный, предоставив тем самым себе права администратора в системе.
Типы Race Condition
Данная часть была написана с использованием материала с самого важного сайта для каждого пентестера hacktricks
Limit-overrun / TOCTOU
Это наиболее простой тип Race Condition, когда уязвимости ограничивают количество раз выполнения какого-либо действия. Например, использование одного и того же купона скидки в интернет-магазине несколько раз. Очень простой пример можно найти в этой статье (требуется vpn) или в этом отчете h1.
Существует множество разновидностей этого вида атак, в том числе:
Погашение подарочной карты несколько раз
Многократное выставление оценок продукту
Снятие или перевод денежных средств, превышающих остаток на счете
Повторное использование одного решения CAPTCHA
Обход anti-brute-force
Hidden substates
Другие, более сложные RC, будут использовать подсистемы в состоянии машины, что может позволить злоумышленнику злоупотреблять состояниями, к которым он не должен был иметь доступа, но для злоумышленника есть небольшое окно для доступа к ним.
Прогнозирование потенциальных скрытых и интересных подсистем.
Первым шагом является определение всех конечных точек, которые либо записывают в подсистему, либо считывают из нее данные, а затем используют их для каких-то важных целей. Например, пользователи могут храниться в таблице базы данных, которая изменяется при регистрации, редактировании профиля, инициировании сброса пароля и завершении сброса пароля. Мы можем использовать три ключевых вопроса, чтобы исключить конечные точки, которые вряд ли могут стать причиной столкновений. Для каждого объекта и связанных с ним конечных точек задайте следующие вопросы:
Как хранится состояние?
Данные, хранящиеся в постоянной структуре данных на стороне сервера, идеально подходят для эксплуатации. Некоторые конечные точки хранят свое состояние полностью на стороне клиента, например, сброс пароля происходит путем отправки JWT по электронной почте — их можно смело пропустить.
Приложения часто хранят некоторые состояния в пользовательской сессии. Часто они в некоторой степени защищены от вложенных состояний — подробнее об этом будет расказано позже.Мы редактируем или добавляем?
Операции, редактирующие существующие данные (например, изменение основного адреса электронной почты учетной записи), обладают большим потенциалом «столкновений», в то время как действия, просто добавляющие существующие данные (например, добавление дополнительного адреса электронной почты), вряд ли будут уязвимы для чего‑либо, кроме limit‑overrun attacks.На чем основана операция?
Большинство конечных точек работают с определенной записью, которая ищется с помощью «ключа» - например, имени пользователя, маркера сброса пароля или имени файла. Для успешной атаки нам необходимы две операции, использующие один и тот же ключ. Например, представим две правдоподобные реализации сброса пароля:
Поиск подсказок.
На этом этапе самое время провести несколько RC‑атак на потенциально интересные конечные точки, чтобы попытаться найти неожиданные результаты по сравнению с обычными. Любое отклонение от ожидаемого ответа, например, изменение одного или нескольких ответов, или эффект второго порядка, например, различное содержимое писем или видимое изменение сеанса может быть подсказкой, указывающей, что что‑то не так.
Prove the concept (Доказательство концепции).
Последний шаг - подтверждение концепции и превращение ее в жизнеспособную атаку.
При отправке пакета запросов можно обнаружить, что ранняя пара запросов вызывает уязвимое конечное состояние, но последующие запросы перезаписывают/делают его недостоверным, и конечное состояние оказывается не эксплуатируемым. В этом случае необходимо исключить все лишние запросы — для эксплуатации большинства уязвимостей достаточно двух. Однако, если сократить количество запросов до двух, атака станет более чувствительной ко времени, поэтому может потребоваться многократное повторение атаки или ее автоматизация.
Time Sensitive Attacks
Иногда можно не обнаружить условий гонки, но техника доставки запросов с точным соблюдением временных рамок все равно может выявить наличие других уязвимостей.
Одним из таких примеров является использование временных меток высокого разрешения вместо криптографически защищенных случайных строк для генерации маркеров безопасности.
Рассмотрим маркер сброса пароля, который рандомизируется только по метке времени. В этом случае можно инициировать два сброса пароля для двух разных пользователей, которые используют один и тот же маркер. Все, что нужно сделать - это задать время для запросов, чтобы они генерировали одну и ту же временную метку.
Для подтверждения, например, предыдущей ситуации можно просто запросить 2 токена сброса пароля одновременно (используя атаку одним пакетом) и проверить, одинаковы ли они.
Обнаружение
Процесс обнаружения достаточно прост. В общих чертах - все, что вам нужно сделать, это:
Определить одноразовую или ограниченную по скорости конечную точку, которая имеет какое-то влияние на безопасность или другое полезное назначение.
Выдать несколько запросов к этой конечной точке в быстрой последовательности, чтобы проверить, сможете ли вы превысить этот лимит.
Основная сложность заключается в выборе времени для запросов таким образом, чтобы по крайней мере два окна гонки совпали
Даже если вы посылаете все запросы в одно и то же время, на практике существуют различные неконтролируемые и непредсказуемые внешние факторы, которые влияют на то, когда сервер обрабатывает каждый запрос и в каком порядке он это делает.
Мне очень понравились схемы от portswigger, я, правда, не знаю, будет ли это нарушать их правила, но я переведу их на русский язык для лучшего понимания аудиторией.
![](https://habrastorage.org/getpro/habr/upload_files/91e/1b3/b95/91e1b3b958f2f9697b20fe986779440f.jpg)
jitter - буквально "дрожание". То есть непрогнозируемое и нерегулируемое изменение некоторого параметра.
Эта часть была взята с того же portswigger, а сам burp вы можете приобрести или использовать community edition или использовать версию избавленную от жадности (внимательно выбирайте крякера, но советую 0daylab
Burp Suite 2023.9 добавляет в Burp Repeater новые мощные возможности, позволяющие легко отправлять группу параллельных запросов таким образом, чтобы значительно снизить влияние одного из этих факторов, а именно сетевого джиттера. Burp автоматически настраивает используемую технику в зависимости от версии HTTP, поддерживаемой сервером:
Для
HTTP/1
используется классическая техника синхронизации по последнему байту.Для
HTTP/2
используется техника однопакетной атаки, впервые продемонстрированная компанией PortSwigger Research на конференции Black Hat USA 2023.
Однопакетная атака позволяет полностью нейтрализовать помехи от сетевого джиттера, используя один TCP-пакет для одновременного выполнения 20-30 запросов.
![](https://habrastorage.org/getpro/habr/upload_files/45b/d32/e09/45bd32e092273d182ce35f5f1f920a69.jpg)
Хотя часто для запуска эксплойта можно использовать всего два запроса, отправка большого количества запросов помогает уменьшить внутреннюю задержку, известную также как jitter на стороне сервера. Это особенно полезно на начальном этапе обнаружения.
Особенно хочется отметить что тот самый jitter - не единственная проблема. Многие из Вас сталкивались с высоким пингом в играх. Тут схожий принцип работы. Чем ближе Вы будете находиться, тем наиболее вероятно получится проэксплуатировать RC.
Эксплуатация Race condition
Тут можно сказать простую фразу - "А давайте просто включим intruder или turbo intruder в много потоков и всё у нас получится. Где наши $300 за бб?". Да, в ней есть доля правды, но это не всегда эффективно. Понятно, что это остается основным способом, но есть и другие. Для объяснения я возьму статью Бума aka лучшие усики комьюнити.
Оригинальная статья, настоятельно рекомендую ознакомиться.
Расщепление HTTP-запроса на две част
Для начала вспомним как формируется HTTP-запрос.
Ну, как Вы знаете, первая строка это метод, путь и версия протокола:
GET / HTTP/1.1
Дальше идут заголовки до переноса строки:
Host: google.com
Cookie:
a=1
Но как веб-сервер узнает, что HTTP-запрос закончился?
Давайте рассмотрим на примере, введи nc google.com 80, а там
GET / HTTP/1.1
Host: google.com
После того, как нажмешь ENTER, ничего не произойдет. Нажмешь еще раз — увидишь ответ.
То есть, чтобы веб-сервер принял HTTP-запрос, необходимо два перевода строки. А корректный запрос выглядит так:
GET / HTTP/1.1\r\nHost: google.com\r\n\r\n
Если бы это был метод POST (не забываем про Content-Length), то корректный HTTP-запрос был бы таким:
POST / HTTP/1.1
Host: google.com
Content-Length: 3
a=1
или
POST / HTTP/1.1\r\nHost: google.com\r\nContent-Length: 3\r\n\r\na=1
Попробуй отправить подобный запрос из командной строки:
echo -ne "GET / HTTP/1.1\r\nHost: google.com\r\n\r\n" | nc google.com 80
В итоге Вы получите ответ, так как наш HTTP-запрос полноценный. Но если уберете последний символ \n
, то ответа не получишь.
На самом деле многим веб-серверам достаточно использовать в качестве переноса \n
, поэтому важно не менять местами \r
и \n
, иначе дальнейшие трюки могут не получиться.
Что это даёт? Вы можете одновременно открыть множество соединений на ресурс, отправить 99% своего HTTP-запроса и оставив неотправленным последний байт. Сервер будет ждать пока ты не дошлёшь последний символ перевода строки. После того, как будет ясно, что основная часть данных отправлена — дослать последний байт (или несколько).
Это особенно важно, если речь идет о большом POST-запросе, например, когда необходима заливка файла. Но даже в небольшом запросе это имеет смысл, так как доставить несколько байтов намного быстрее, чем одновременно килобайты информации.
Опять же настоятельно рекомендую к прочтению, там есть великолепные видосики с поездами
How to hack
В общих чертах все, что вам нужно сделать, это:
Определить одноразовую или ограниченную по скорости конечную точку, которая имеет какое-то влияние на безопасность или другое полезное назначение.
Выдать несколько запросов к этой конечной точке в быстрой последовательности, чтобы проверить, сможете ли вы превысить этот лимит.
Основная сложность заключается в выборе времени для запросов таким образом, чтобы по крайней мере два окна гонки совпали, вызвав "столкновение". Часто это окно составляет всего миллисекунды, но может быть и короче, как мы это разбирали выше.
Как говорилось раньше: даже если вы посылаете все запросы в одно и то же время, на практике существуют различные неконтролируемые и непредсказуемые внешние факторы, которые влияют на то, когда сервер обрабатывает каждый запрос и в каком порядке.
Инструменты
Обычно, для эксплуатации используют turbo intruder бурпа, но также мы посмотрим на racepwn.
Burp Turbo Intruder
![](https://habrastorage.org/getpro/habr/upload_files/17a/2a7/26c/17a2a726c7e9c6b8f8c3d93a4ecf3891.jpg)
Мы можем попробовать проэксплуатировать Race condition через Repeater, с помощью отправки сгруппированных HTTP-запросов, но зачем это делать, если есть Turbo intruder?
И тут встаёт вопрос - "Зачем нам использовать Turbo Intruder, если есть стандартный Intruder?". Если максимально коротко, то Turbo Intruder - это Intruder на максималках, с анаболиками (22 встроенных) и возможностью кастомизации атак при помощи Python (если вашего варианта нет в списке, то вы должны обладать навыками в написании скриптов на этом языке).
![](https://habrastorage.org/getpro/habr/upload_files/3c0/ee1/258/3c0ee1258a237867ab00b67dbc6078e0.png)
Как и в обычном интрудере, если выделить часть запроса перед отправкой в расширение, то она в окне запроса заменится на %s
. Это является аналогом символов § §
, и Вы можете перемещать их в любую точку запроса.
Теперь немного про его особенности (как в рекламе):
Быстрота: Turbo Intruder использует стек HTTP, созданный вручную с нуля с учетом скорости. В результате на многих целях он может серьезно обогнать даже модные асинхронные скрипты на Go (на самом деле стек можно выбрать, и большинство из них будут Вам знакомы).
Удобство: скучные результаты могут быть автоматически отфильтрованы с помощью усовершенствованного алгоритма дифов, адаптированного из Backslash Powered Scanner. Это означает, что Вы можете запустить атаку и получить полезные результаты в два клика.
Масштабируемость: Turbo Intruder может достичь плоского использования памяти, что позволяет проводить надежные многодневные атаки. Его также можно запускать в headless-окружении через командную строку.
Гибкость: атаки конфигурируются с помощью Python. Это позволяет выполнять сложные требования: например, подписанные запросы и многоступенчатые последовательности атак. Кроме того, пользовательский стек HTTP позволяет обрабатывать неправильно сформированные запросы, которые ломают другие библиотеки.
Для детального изучения - Ссылка
racepwn
RacePWN - утилита, написанная на golang, которая реализует интерфейс librace путем установки параметров через конфигурацию, написанную на json.
[
{
"race": {
// Установка параметров гонки
"type": "paralell", // режим гонки
"delay_time_usec": 10000, // временная задержка между двумя частями запроса
"last_chunk_size": 10 // размер куска по последнему запросу
},
"raw": {
"host": "tcp://localhost:8080", // имя хоста и порт
"ssl": false, // использовать флаг ssl
"race_param": [
{
// параметры гонки
"data": "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n", // raw HTTP запрос
"count": 100 // количество пакетов
}
]
}
}
]
Поддерживаются два режима работы. Работает как консольная утилита, и как веб-сервис.
console util:
В этом режиме работы на вход приложения подается конфигурационный файл. Приложение выполняет запросы к серверу.
racepwn < config.json
web service:
В этом режиме утилита начинает работать как веб-сервис. Для работы необходимо выполнить POST-запрос по пути /race с содержимым конфигурационного файла. Имя хоста и порт должны быть указаны с помощью флага -hostname.
racepwn -host "localhost:8080"
Пример:
curl -i -X POST localhost:8080/race --data @<(cat config.json)
Как защищать веб-приложение
Одним из наиболее эффективных способов предотвращения уязвимостей состояния гонки является использование механизмов блокировки. Механизмы блокировки обеспечивают одновременный доступ к общему ресурсу только одного процесса, не позволяя другим процессам вмешиваться в работу ресурса.
Семафоры - это один из видов механизмов блокировки, который часто используется для предотвращения условий гонки. Семафоры работают путем присвоения ресурсу значения, указывающего доступен он или нет. Когда процесс пытается получить доступ к ресурсу, он проверяет значение семафора. Если ресурс недоступен, процесс ждет, пока он не станет доступен.
Мьютексы - это тип механизма блокировки, аналогичный семафорам. Мьютексы работают, позволяя только одному процессу одновременно получать доступ к общему ресурсу. Когда процесс пытается получить доступ к ресурсу, он проверяет состояние мьютекса. Если к ресурсу уже обращается другой процесс, то он ждет, пока ресурс не станет доступным. Большинство языков программирования имеют встроенную функциональность блокировки данных; например, в Python есть "threading.Lock", а в Go - "sync.Mutex".
Атомарные операции являются разновидностью низкоуровневого механизма синхронизации. Он обеспечивает выполнение операций процесса за один, неделимый шаг. Это не позволяет другим процессам вмешиваться в работу с ресурсом во время выполнения операции.
Race Condition в BugBounty
В этой части статьи я приложу список 15 репортов на h1 (надеюсь, что в скором времени появятся дисклоузы и от ру площадок).
№ | Название | Компания | Вознаграждение | Ссылка |
1 | Race Conditions in OAuth 2 API implementations | The Internet | $2,500 | |
2 | Race condition in Flash workers may cause an exploitable double free | Flash (IBB) | $10,000 | |
3 | Race condition in performing retest allows duplicated payments | HackerOne | $2,100 | |
4 | Adobe Flash Player Race Condition Vulnerability | Flash (IBB) | $2,000 | |
5 | Race condition in activating email resulting in infinite amount of diamonds received | InnoGames | $2,000 | |
6 | Race Condition allows to redeem multiple times gift cards which leads to free “money” | Reverb.com | $1,500 | |
7 | Client-Side Race Condition using Marketo, allows sending user to data-protocol in Safari when form without onSuccess is submitted on www.hackerone.com | HackerOne | $1,250 | |
8 | Race condition на market.games.mail.ru | Mail.ru | $1,000 | |
9 | Race condition leads to duplicate payouts | HackerOne | $750 | |
10 | Race Condition Vulnerability On Pornhubpremium.com | PornHub | $520 | |
11 | Race Condition leads to undeletable group member | HackerOne | $500 | |
12 | Race condition in claiming program credentials | HackerOne | $500 | |
13 | race condition in adding team members | Shopify | $500 | |
14 | Race condition (TOCTOU) in NordVPN can result in local privilege escalation | NordVPN | $500 | |
15 | Register multiple users using one invitation (race condition) | Keybase | $350 |
Где можно попрактиковаться
Заключение
В завершении хотелось бы выразить искреннюю благодарность всем тем, кто внес свой вклад в создание данной статьи. Множество авторов, имя которых, возможно, останется незамеченным, но их влияние несомненно ощущается. Пусть этот текст не становится непосильным бременем для читателей, а остается доступным и понятным.
P.S. Больше подобной информации и хороших мемов Вы сможете найти тут.