- PVSM.RU - https://www.pvsm.ru -
TL;DR Большинство атакующих использовали ненадежные методы race condition, инструмент racepwn поможет это исправить
Вася хочет перевести 100 долларов, которые есть у него на счету, Пете. Он переходит на вкладку переводов, вбивает Петин ник и в поле с количеством средств, которые необходимо перевести — цифру 100. Далее, нажимает на кнопку перевода. Данные кому и сколько отправляются на веб-приложение. Что может происходить внутри? Что необходимо сделать программисту, чтобы все работало корректно?
Необходимо получить значение текущего баланса пользователя, если оно меньше чем сумма, которую он хочет перевести — сказать ему об этом. С учетом того, что на нашем сайте не предусмотрены кредиты и в минус баланс уйти не должен.
Необходимо записать в значение баланса текущего пользователя его баланс с вычетом переводимой суммы. Было 100, стало 100-100=0.
Пете наоборот, было 0, стало 0+100=100.
При написании программ человек берет простейшие алгоритмы, которые он объединяет в единый сюжет, что и будет являться сценарием программы. В нашем случае, задача программиста написать логику переводов денег (баллов, кредитов) от одного человека к другому в веб-приложении. Руководствуясь логикой можно составить алгоритм, состоящий из нескольких проверок. Представим, что мы просто убрали все лишнее и составили псевдокод.
Если (Вася.баланс >= сумма_перевода) То
Вася.Баланс=Вася.Баланс-сумма_перевода
Петя.Баланс=Петя.Баланс+сумма_перевода
Поздравление()
Иначе
Ошибка()
Но всё бы ничего, если бы все происходило в порядке очереди. Но сайт может обслуживать одновременно множество пользователей, а это происходит не в одном потоке, потому что современные веб-приложения используют многопроцессорность и многопоточность для параллельной обработки данных. C появлением многопоточности у программ появилась забавная архитектурная уязвимость — состояние гонки (или race condition).
А теперь представим, что наш алгоритм срабатывает одновременно 3 раза.
У Васи все так же 100 баллов на балансе, только вот каким-то образом он обратился к веб-приложению тремя потоками одновременно (с минимальным количеством времени между запросами). Все три потока проверяют, существует ли пользователь Петя, и проверяют, достаточно ли баланса у Васи для перевода. В тот момент времени, когда алгоритм проверяет баланс, он всё еще равен 100. Как только проверка пройдена, из текущего баланса 3 раза вычитается 100, и добавляется Пете.
Что мы имеем? У Васи на счету минусовой баланс (100 — 300 = -200 баллов). Тем временем, у Пети 300 баллов, хотя фактически, должно быть 100. Это и есть типичный пример эксплуатации состояния гонки. Сравнимо с тем, что по одному пропуску проходят сразу несколько человек. Ниже снимок экрана такой ситуации от 4lemon [1]
Состояние гонки может быть как в многопоточных приложениях, так и в базах данных, в которых они работают. Не обязательно в веб-приложениях, например, это частый критерий для повышения привилегий в операционных системах. Хотя веб-приложения имеют свои особенности для успешной эксплуатации, о которых я и хочу рассказать.
Заходит хакер в кальянную, квест и бар, а ему — у вас race condition! Омар Ганиев
В большинстве случаев для проверки/эксплуатации состояния гонки используют многопоточное программное обеспечение в качестве клиента. Например, Burp Suite и его инструмент Intruder. Ставят один HTTP-запрос на повторение, устанавливают много потоков и включают флуд. Как например, в этой статье [2]. Или в этой [3]. Это достаточно рабочий способ, если сервер допускает использование множества потоков на свой ресурс и как пишут в статьях выше — если не получилось, попробуйте ещё раз. Но дело в том, что в некоторых ситуациях, это может быть не эффективно. Особенно если вспомнить, как подобные приложения обращаются к серверу.
Каждый поток устанавливает TCP соединение, отправляет данные, ждет ответа, закрывает соединение, открывает снова, отправляет данные и так далее. На первый взгляд, все данные отправляются одновременно, но сами HTTP-запросы могут приходить не синхронно и в разнобой из-за особенностей транспортного уровня, необходимости устанавливать защищенное соединение (HTTPS) и резолвить DNS (не в случае с burp’ом) и множества слоёв абстракций, которые проходят данные до отправки в сетевое устройство. Когда речь идет о миллисекундах, это может сыграть ключевую роль.
Можно вспомнить о HTTP-Pipelining, в котором можно отправлять данные с помощью одного сокета. Ты можешь сам посмотреть как это работает, использовав утилиту netcat (у тебя же есть GNU/Linux, ведь так?).
На самом деле использовать linux необходимо по многим причинам, ведь там более современный стек TCP/IP, который поддерживается ядрами операционной системы. Сервер скорее всего тоже на нем.
Например, выполни команду nc google.com 80 и вставь туда строки
GET / HTTP/1.1
Host: google.com
GET / HTTP/1.1
Host: google.com
GET / HTTP/1.1
Host: google.com
Таким образом, в рамках одного соединения будет отправлено три HTTP-запроса, и ты получишь три HTTP ответа. Эту особенность можно использовать для минимизации времени между запросами.
Веб-сервер получит запросы последовательно (ключевое слово), и обработает ответы в порядке очереди. Эту особенность можно использовать для атаки в несколько шагов (когда необходимо последовательно выполнить два действия в минимальное количество времени) или, например, для замедления работы сервера в первом запросе, чтобы увеличить успешность атаки.
Трюк — ты можешь мешать серверу обработать твой запрос нагружая его СУБД, особенно эффективно если будет использован INSERT/UPDATE. Более тяжелые запросы могут “затормозить” твою нагрузку, тем самым, будет большая вероятность, что ты выиграешь эту гонку.
Для начала вспомни как формируется HTTP-запрос.
Ну как ты знаешь, первая строка это метод, путь и версия протокола:
GET / HTTP/1.1
Дальше идут заголовки до переноса строки:
Host: google.com
Cookie: a=1
Но как веб-сервер узнает, что HTTP-запрос закончился?
Давай рассмотрим на примере, введи nc google.com 80, а там
GET / HTTP/1.1
, после того, как нажмешь ENTER, ничего не произойдет. Нажмешь еще раз — увидишь ответ.
Host: google.com
То есть, чтобы веб-сервер принял HTTP-запрос, необходимо два перевода строки. А корректный запрос выглядит так:
GET / HTTP/1.1rnHost: google.comrnrn
Если бы это был метод POST (не забываем про Content-Length), то корректный HTTP-запрос был бы таким:
POST / HTTP/1.1
Host: google.com
Content-Length: 3
a=1
или
POST / HTTP/1.1rnHost: google.comrnContent-Length: 3rnrna=1
Попробуй отправить подобный запрос из командной строки:
echo -ne "GET / HTTP/1.1rnHost: google.comrnrn" | nc google.com 80
В итоге ты получишь ответ, так как наш HTTP-запрос полноценный. Но если ты уберешь последний символ n, то ответа не получишь.
На самом деле многим веб-серверам достаточно использовать в качестве переноса n, поэтому важно не менять местами r и n, иначе дальнейшие трюки могут не получиться.
Что это даёт? Ты можешь одновременно открыть множество соединений на ресурс, отправить 99% своего HTTP-запроса и оставив неотправленным последний байт. Сервер будет ждать пока ты не дошлёшь последний символ перевода строки. После того, как будет ясно, что основная часть данных отправлена — дослать последний байт (или несколько).
Это особенно важно, если речь идет о большом POST-запросе, например, когда необходима заливка файла. Но и даже в небольшом запросе это имеет смысл, так как доставить несколько байт намного быстрее, чем одновременно килобайты информации.
По результатам исследования Влада Роскова [4], нужно не только расщеплять запрос, но и имеет смысл делать задержку в несколько секунд между отправкой основной части данных и завершающей. А всё потому, что веб-сервера начинают парсить запросы еще до того, как получат его целиком.
Например nginx при получении заголовков HTTP-запроса начнет их парсить, складывая неполноценный запрос в кэш. Когда придет последний байт — веб-сервер возьмет частично обработанный запрос и отправит его уже непосредственно приложению, тем самым сокращается время обработки запросов, что повышает вероятность атаки.
В первую очередь это конечно же архитектурная проблема, если правильно спроектировать веб-приложение, можно избежать подобных гонок.
Обычно, применяют следующие методы борьбы с атакой:
Операция блокирует в СУБД обращения к заблокированному объекту, пока его не разблокируют. Другие стоят и ждут в сторонке. Необходимо правильно работать с блокировками, не блокировать ничего лишнего.
Упорядоченные транзакции (serializable) — гарантируют, что транзакции будут выполнены строго последовательно, однако, это может сказаться на производительности.
Берут какую-нибудь штуку (например etcd). В момент вызова функций создают запись с ключом, если не получилось создать запись, значит она уже есть и тогда запрос прерывается. По окончании обработки запроса запись удаляется.
И вообще мне понравилось видео выступления Ивана Работяги [8] про блокировки и транзакции, очень познавательно.
Одна из особенностей сессий может быть то, что она сама по-себе мешает эксплуатировать гонку. Например, в языке PHP после session_start() происходит блокировка сессионного файла, и его разблокировка наступит только по окончанию работы сценария (если не было вызова session_write_close [9]). Если в этот момент вызван другой сценарий который использует сессию, он будет ждать.
Для обхода этой особенности можно использовать простой трюк — выполнить аутентификацию нужное количество раз. Если веб-приложение разрешает создавать множество сессий для одного пользователя, просто собираем все PHPSESSID и делаем каждому запросу свой идентификатор.
Если сайт, на котором необходимо эксплуатировать race condition
Когда задача отправить запросы и минимизировать промежуток отправки между ними, непосредственная близость к веб-серверу несомненно будет плюсом.
Ведь есть разница, когда ping к серверу 200 и 10 мс. А если повезет, вы вообще можете оказаться на одном физическом сервере, тогда зарейсить будет немного проще :)
Для успешного race condition можно применять различные трюки для увеличения вероятности успеха. Отправлять несколько запросов (keep-alive) в одном, замедляя веб-сервер. Разбивать запрос на несколько частей и создавать задержку перед отправкой. Уменьшать расстояние до сервера и количество абстракций до сетевого интерфейса.
В результате этого анализа мы вместе с Michail Badin разработали инструмент RacePWN [11]
Он состоит из двух компонентов:
RacePWN можно интегрировать в другие утилиты (например в Burp Suite), или создать веб-интерфейс для управления рейсами (все никак руки не доходят). Enjoy!
Но на самом деле ещё есть куда расти и можно вспомнить о HTTP/2 и его перспективы для атаки. Но в данный момент HTTP/2 у большинство ресурсов лишь фронт, проксирующий запросы в старый-добрый HTTP/1.1.
Может ты знаешь еще какие-то тонкости?
Оригинал [12]
Автор: Bo0oM
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/informatsionnaya-bezopasnost/324176
Ссылки в тексте:
[1] 4lemon: https://habr.com/ru/users/4lemon/
[2] в этой статье: https://medium.com/@valeriyshevchenko/how-to-check-race-conditions-in-web-applications-338f73937992
[3] в этой: https://medium.com/@ciph3r7r0ll/race-condition-bug-in-web-app-a-use-case-21fd4df71f0e
[4] Влада Роскова: https://twitter.com/leetmore
[5] блокировки: https://ru.wikipedia.org/wiki/%D0%91%D0%BB%D0%BE%D0%BA%D0%B8%D1%80%D0%BE%D0%B2%D0%BA%D0%B0_%D0%B7%D0%B0%D0%BF%D0%B8%D1%81%D0%B8
[6] изоляциями транзакций: https://ru.wikipedia.org/wiki/%D0%A3%D1%80%D0%BE%D0%B2%D0%B5%D0%BD%D1%8C_%D0%B8%D0%B7%D0%BE%D0%BB%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8_%D1%82%D1%80%D0%B0%D0%BD%D0%B7%D0%B0%D0%BA%D1%86%D0%B8%D0%B9
[7] мьютексные семафоры: https://ru.wikipedia.org/wiki/%D0%9C%D1%8C%D1%8E%D1%82%D0%B5%D0%BA%D1%81
[8] видео выступления Ивана Работяги: https://www.youtube.com/watch?v=AYWiRVdJFTI
[9] session_write_close: https://www.php.net/manual/ru/function.session-write-close.php
[10] хостится: https://www.reg.ru/?rlink=reflink-717
[11] RacePWN: https://github.com/racepwn/racepwn
[12] Оригинал: https://bo0om.ru/race-condition-ru
[13] Источник: https://habr.com/ru/post/460339/?utm_campaign=460339&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.