Как работает уязвимость в ModSecurity WAF

Файрвол

В ModSecurity — известном WAF для Apache, IIS и nginx — нашли критическую уязвимость, которая приводит к отказу в обслуживании. Причем завершает работу не только сама библиотека, но и приложения, которые ее вызывают. Давай разберемся, в чем ошиблись разработчики ModSecurity и как эксплуатировать эту лазейку при пентестах.

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

Уязвимость в ModSecurity WAF

Уязвимость обнаружили Эрвин Хегедюш (Ervin Hegedüs) и Андреа Менин (Andrea Menin), разработчики OWASP Core Rule Set, когда изучали работу парсера запросов, в частности обработку хидера cookie.

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

Уязвимость получила идентификатор CVE-2019-19886 и затрагивает все версии Trustwave ModSecurity ветки 3.х, начиная с 3.0.0 и заканчивая 3.0.3 включительно.

Стенд

Сначала нужно поднять тестовое окружение. Здесь есть два варианта.

Если не хочешь возиться с отладкой и копанием в сорцах, то можешь просто запустить докер-контейнер с уязвимой версией ModSecurity и потестить эксплоит.

Второй вариант — скомпилить все из сорцов с возможностью дебага.

Начнем с запуска контейнера с Debian на борту и установки всех необходимых пакетов.

Теперь скачиваем библиотеку ModSecurity последней уязвимой версии 3.0.3.

Далее подгрузим все необходимые модули.

А потом скомпилируем ее и установим.

Установка уязвимой версии библиотеки ModSecurity

Установка уязвимой версии библиотеки ModSecurity

Пришел черед коннектора для nginx.

Ну и сам веб-сервер, разумеется.

Теперь сконфигурируем его для использования библиотеки ModSecurity.

Дальше дело за компиляцией и установкой.

Компиляция и установка nginx с поддержкой ModSecurity

Компиляция и установка nginx с поддержкой ModSecurity

Для более наглядных тестов я также установлю и запущу PHP-FPM.

По дефолту nginx ставится в директорию /usr/local/nginx/. Перейдем туда, чтобы немного подправить конфиги под наши реалии. Сначала включим поддержку PHP. У меня PHP-FPM работает через сокет, поэтому пропишем путь до него в раздел server файла nginx.conf.

Теперь переходим к настройке ModSecurity. Добавим необходимую директиву во все тот же nginx.conf, только на этот раз в раздел http.

Так как у нас будет несколько конфигурационных файлов, сделаем один общий modsec_includes.conf, в который будем добавлять список необходимых конфигов. Подгрузим его при помощи modsecurity_rules_file.

Далее нужно скопировать дефолтный конфиг ( modsecurity.conf-recommended) из дистрибутива ModSecurity.

Переключим библиотеку из пассивного режима в режим блокировки.

Пришел черед фильтров. Я рекомендую использовать набор правил OWASP Core Rule Set. Для наших тестов достаточно будет только нескольких файлов. В первую очередь загрузим основной конфиг.

Затем мой выбор пал на файл с правилами для предотвращения XSS-уязвимостей.

И наконец, два вспомогательных конфига — для инициализации правил и блокировки запроса на основе системы скоринга.

Теперь нужно использовать все эти правила в нашей конфигурации nginx. Для этого я и создавал файл /usr/local/nginx/conf/modsec_includes.conf.

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

РЕКОМЕНДУЕМ:
Настройка безопасности MikroTik

На этом этап подготовки стенда закончен, осталось его проверить. Запустим веб-сервер.

Я добавил несколько параметров, чтобы отключить запуск nginx как демона и вывести логи в консоль. Если теперь отправить потенциально опасный запрос на сервер — http://nginxdos.vh/?xss=<script>alert()</script>, то ModSecurity заблокирует запрос и вернет 403.

Готовый стенд. ModSecurity заблокировала потенциально опасный запрос к серверу

Готовый стенд. ModSecurity заблокировала потенциально опасный запрос к серверу

Вот и готовый стенд. Переходим к препарированию уязвимости.

Детали

Начнем с того, что посмотрим на патч, который исправляет уязвимость. Изменения коснулись файла transaction.cc в разделе парсинга куков. Запустим веб-сервер через отладчик и поставим бряк на строку 558, чтобы потрейсить процесс обработки.

Отладка сервера nginx с библиотекой ModSecurity. Ставим брейк-пойнт на парсере заголовка cookie

Отладка сервера nginx с библиотекой ModSecurity. Ставим брейк-пойнт на парсере заголовка cookie

Отправляем запрос с куками и попадаем в точку останова.

Сработал брейк-пойнт в процессе парсинга куков

Сработал брейк-пойнт в процессе парсинга куков

Как ты, скорее всего, знаешь, в протоколе HTTP строка cookie представляет собой последовательность пар имя=значение, разделенных символом ;. Поэтому изначально вся строка разбивается на части, где разделителем служит ;. Результат записывается в вектор ( std::vector).

modsec/v3.0.3/src/transaction.cc

Вектор в C++ — это замена стандартному динамическому массиву, который может управлять выделенной для него памятью. С его помощью можно создавать массивы, длина которых задается во время выполнения, без использования операторов new и delete. Все элементы вектора должны принадлежать одному типу. В дополнение к функциям прямого доступа элементы вектора можно получить посредством итераторов. Что и происходит дальше по коду.

modsec/v3.0.3/src/transaction.cc

Перебираем все переданные в запросе куки

Перебираем все переданные в запросе куки

Так как я передал одну куку, то и элемент всего один.
Теперь он разбивается по разделителю =. Таким образом получаем имя и значение cookie.

Дальше проверяется размер полученного вектора, и, если он больше единицы, выполнение продолжается.

modsec/v3.0.3/src/transaction.cc

Разбиваем содержимое cookie на пару имя-значение

Разбиваем содержимое cookie на пару имя-значение

Пока все идет хорошо. Если обратиться к разделу 5.2 спецификации RFC 6265 о механизмах хранения состояния в HTTP, то там под пунктом 2 увидим, что если в паре имя-значение отсутствует символ = (%x3D), то его нужно игнорировать.

Спецификация RFC 6265. Раздел 5.2

Спецификация RFC 6265. Раздел 5.2

Именно так и происходит. А также частично выполняется и пункт 4 — удалять все пробелы перед именем и после него и значения куки. В нашем случае только один пробел перед именем.

modsec/v3.0.3/src/transaction.cc

А вот с третьим пунктом возникли проблемы.

Спецификация RFC 6265. О том, как парсить пару имя-значение

Спецификация RFC 6265. О том, как парсить пару имя-значение

Часть строки до первого появления символа = — это имя куки, а все, что после, — ее значение. То есть из хидера вида hello=cruel=world должна получиться кука hello, значение которой cruel=world. Однако в случае с ModSecurity 3.0.3 получим hello со значением cruel. Виной всему следующий участок кода.

modsec/v3.0.3/src/transaction.c

В качестве значения передается только второй элемент массива, а в данном случае у нас получается их три: hello, cruel и world

Некорректный парсинг куки в ModSecurity 3.0.3 больше чем с одним символом =

Некорректный парсинг куки в ModSecurity 3.0.3 больше чем с одним символом =

Чем это может грозить? Если у тебя есть какие-то правила, которые фильтруют разные виды атак в куках, например XSS или SQL-инъекции, то, используя трюк с двойным символом =, можно обходить эти фильтры.

Добавляем XSS в cookie

Добавляем XSS в cookie

Для демонстрации я создам простенький, уязвимый к XSS скрипт.

index.php

Если я напрямую передаю в куке XSS, то защита срабатывает, если использую второй символ =, то XSS триггерится.

Обход защиты от XSS в ModSecurity

Обход защиты от XSS в ModSecurity

Разумеется, нельзя отфильтровать то, что не удалось правильно прочитать.

Обход фильтров WAF — это, конечно, интересно, но давай посмотрим, что еще можно сделать.
Вернемся к пункту 5 спецификации RFC 6265.

Спецификация RFC 6265. Игнорируем cookie с пустым именем

Спецификация RFC 6265. Игнорируем cookie с пустым именем

То есть если имя куки пустое, то она должна игнорироваться. В ModSecurity 3.0.3 такой логики не предусмотрено. В связи с этим предлагаю еще раз внимательно посмотреть на следующий участок кода.

modsec/v3.0.3/src/transaction.cc

Здесь проверяется, какой символ стоит на нулевой позиции в имени куки. А что произойдет, если передать cookie с пустым названием?

Программа попытается считать значение из пустого диапазона, что вызовет исключение вида out_of_range.

Ошибка при обработке пустого имени cookie приводит к завершению работы ModSecurity

Ошибка при обработке пустого имени cookie приводит к завершению работы ModSecurity

Теперь программа завершит свою работу, а вместе с ней остановится и воркер nginx. Таким образом, постоянно отправляя запросы с вредоносной кукой, можно вызвать отказ в обслуживании веб-сервера, и он перестанет обрабатывать запросы легитимных пользователей. Для этого можно воспользоваться простейшим PoC.

Отказ в обслуживании веб-сервера nginx из-за уязвимости в библиотеке ModSecurity

Отказ в обслуживании веб-сервера nginx из-за уязвимости в библиотеке ModSecurity

Заключение

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

После того как разработчики были уведомлены об уязвимости, исследователь Эрвин Хегедюш тоже отправил два pull-реквеста с изменениями, которые позволяют исправить уязвимость (2023, 2201). В результате разработчики пересмотрели всю логику парсинга cookie и приблизили ее к спецификации RFC 6265.

РЕКОМЕНДУЕМ:
Сетевые атаки и защита от них

И конечно, если ты используешь ModSecurity версий от 3.0.0 до 3.0.3, то поспеши обновиться до версии 3.0.4, где эта уязвимость исправлена.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий