Бесплатно Экспресс-аудит сайта:

24.03.2021

Как обойти WAF через некорректно настроенное правило (Часть 2)

Автор: Red Timmy Security Staff

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

Обход WAF при помощи «одиночного закодированного байта»

Некоторое время назад к нам обратились с просьбой повторно проверить веб-уязвимость, связанную с удаленным выполнением кода (RCE), которая может быть активирована после визита по адресу наподобие следующего:

https://victim.com/appname/MalPath/something?badparam=%rce_payload%  

Приложение находилось за устройством Big-IP с включенным модулем ASM (Application Security Manager; Менеджер безопасности приложений), представляющее собой фаервол веб-приложений (WAF), работающий на седьмом уровне модели OSI и доступный на платформах компании F5. Чтобы немедленно отреагировать на угрозу и не оставлять приложение доступным через интернет, сетевые инженеры добавили новое правило в WAF для защиты конечного узла перед тем, как будет выпущено обновление для приложения. Наша задача состояла в проверке эффективности и надежности этого правила, которое выглядело примерно так:


https://victim.com/appname/MalPath/something?badparam=%rce_payload%  

Рисунок 1: Правило для WAF, добавленное в качестве временной меры для защиты от уязвимости

В случае перехвата запроса HTTP GET (строка 2) если в URI, преобразованным в нижний регистр, есть строки «/malpath/» и «badparam=» (строки 3 и 4), запрос вначале будет занесен в журнал (строка 7), затем заблокирован, а клиент получит ответ в виде ошибки 404 (строка 8).

После добавления нового правила, если зайти по адресу

https://victim.com/appname/%4dalPath/something?badparam=%rce_payload%  

… то мы получим ошибку 404. Казалось бы, правило работает, как и предполагалось.

Однако в документации для платформы компании F5 мы обнаружили, что директива «HTTP::uri» (строки 3 и 4) возвращает URI запроса без полного декодирования. Соответственно, если мы посетим следующий URL:

https://victim.com/appname/%4dalPath/something?badparam=%rce_payload%  

… где «%4» представляет собой закодированный символ M, произойдет следующее:

  1. Вначале WAF перехватывает запрос GET. Затем URI в виде «/appname/%4dalPath/something?badparam=%rce_payload%» полностью конвертируется в нижний регистр, поскольку внутри правила используется функция «tolower». После преобразования получаем строку «/appname/%4dalpath/something?badparam=%rce_payload%».

  2. Затем внутри URI ищутся строки «/malpath/» и «badparam=». Строка «/%4dalpath/» не совпадает со строкой «/malpath/», указанной в правиле (строка 3). Соответственно, правило не срабатывает.

  3. Как только у нас получилось обойти правило, HTTP запрос достигает бэкэнда, где происходит канонизация и декодирование. Таким образом, запрос, содержащий URI, сконвертированный из «/appname/%4dalPath/something?badparam=%rce_payload%» в «/appname/MalPath/something?badparam=%rce_payload%» достигает целевого веб-приложения и отрабатывается.

  4. Полезная нагрузка «%rce_payload%» выполняет все заложенные функции (открытие реверсивного шелла, выполнение команд операционной системы и так далее).

По результатам вышесказанного нужно усвоить следующий урок: перед сравнением и проверкой URI всегда нужно выполнять декодирование. Отсутствие полного декодирования URI может привести к проблемам. Еще хуже – если внутри правила используется ключевое слово «contains».

Частичное декодирование URL = полноценный обход защиты

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

В этот раз при посещении обоих URL’ов:

https://victim.com/appname/MalPath/something?badparam=%rce_payload%  

и

https://victim.com/appname/%4dalPath/something?badparam=%rce_payload%  

возвращалась ошибка 404. Если по-простому, то WAF нас блокировал.

Затем мы попытались дважды закодировать символ «M» в строке «/Malpath/» при помощи последовательности «%254d». Однако при посещении следующего URL’а:

https://victim.com/appname/%254dalPath/something?badparam=%rce_payload%  

ничего не происходило, поскольку фаервол также блокировал этот запрос. Теперь обновленное правило выглядело примерно так:


Рисунок 2: Обновленное правило, учитывающее закодированную часть URL’а

На рисунке выше строки 15-24 эквивалентны строкам 1-10 предыдущей версии правила. Однако в этот раз строки «/malpath/» и «badparam=» проверяются с использованием переменной «$decodeUri» (строки 17-18), а не директивы «HTTP::uri», как в прошлый раз. Сразу же возникает вопрос, откуда появилась эта переменная?

В строках 1-14 происходит заполнение содержимым этой переменой. Если по-простому, переменная представляет собой URI из запроса («[HTTP::uri]» в строке 2). Декодирование происходит такое количество раз, которое указано в переменной «max» (строка 4) в цикле «while» (строки 9-13) с использованием счетчика (переменная «cnt», объявленная в строке 5). Цикл прерывается, когда больше не осталось закодированных символов, или счетчик стал равным значению переменной «max».

Чтобы лучше разобраться в происходящем, зайдите на страницу сервиса кодирования/декодирования и выполните четырехкратное декодирование символа «M» (закодированного при помощи алгоритма percent-encoding), добавив последовательность «%252525254d» в текстовое поле. Теперь нажмите кнопку «Decode» (см. рисунок ниже) четыре раза и во время каждой паузы посмотрите, что происходит. Та же самая логика заложена в правиле WAF, выполняемым обработку нашего URL’а.


Рисунок 3: Сервис по кодированию/декодированию строк

Соответственно, для обхода мы должны посетить URL

https://victim.com/appname/%252525254dalPath/something?badparam=%rce_payload%  

который WAF будет декодировать исходя из следующей логики:

  • (первая итерация цикла): /appname/%2525254dalPath/something?badparam=%rce_payload%

  • (вторая итерация цикла): /appname/%25254dalPath/something?badparam=%rce_payload%

  • (третья итерация цикла): /appname/%254dalPath/something?badparam=%rce_payload%

  • (четвертая итерация цикла): /appname/%4dalPath/something?badparam=%rce_payload%

Когда цикл внутри правила завершится, запрос с URI будет выглядеть так: «/appname/%4dalPath/something?badparam=%rce_payload%». И снова, как и в предыдущем случае, строка «/malpath/» (строка 17 в правиле WAF) не соответствует строке «/%4dalpath/» в URI, WAF разрешит запросу пройти дальше, и в итоге наша полезная нагрузка «%rce_payload%» выполнится. Как говорится, GAME OVER (второй раз подряд).

Можно было бы подумать: «Окей, но это же простейшая ошибка! Просто увольте этих ребят, притворяющихся специалистами по безопасности!». На самом деле это утверждение не совсем верно, поскольку опция «-normalized», которая несомненно упрощает жизнь сетевым инженерам, может быть использована только в самых последних версиях iRules , оставляя в качестве уникальной возможности создание и внедрение собственного правила, что, очевидно, может приводить к ошибкам. Однако всё равно удивительно, сколько источников публично приводят ошибочные правила в качестве примеров, даже в официальной документации компании F5 (см. рисунок ниже).


Рисунок 4: Выдержка из документации с примером по созданию уязвимого правила

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

Но вернемся к нашей главной истории! После обнаружения новой проблемы исправление появилось быстро (добавленный фрагмент показан на рисунке ниже).


Рисунок 5: Добавленный фрагмент для исправления новой проблемы

По сути, после цикла «while» (строки 9-13 на рисунке в начале этого параграфа) в новой версии правила появилось условие «if», которое по окончании цикла проверяет, равен ли счетчик «cnt» значению переменной «max». В случае равенства цикл «while» был прерван, но символы в URI для декодирования остались. Поскольку нам не нужно обрабатывать запрос с закодированными символами, правило возвращает ошибку 404, и полезная нагрузка не выполняется.

Еще одна проблема из-за неправильной гипотезы

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

Кажется, что на данный момент наша история со счастливым концом. Две проверки выявили недостатки в конфигурации фаервола, и в итоге было создано строгое правило, когда происходит полное декодирование URI. Однако в определенный момент времени возникает мысль, что та же самая уязвимость могла быть использована через POST запрос. Поскольку текущее правило обрабатывает только GET запросы, потребовалось еще одно изменение, после чего нас опять попросили проверить, что всё работает, как полагается.

С целью защититься от обоих сценариев (GET и POST) сетевые инженеры удалили строку 16:

if ([HTTP::method] equals “GET”) [...]

Без этого условия проверка URI выполнялась бы для любого HTTP запроса, вне зависимости от используемого метода. Если это решение было бы рабочим, всё бы выглядело красиво… однако сразу же возникает проблема. Потратьте пару секунд на размышления, прежде чем прочитать ответ.

При посещении следующего URL’а:

https://victim.com/appname/MalPath/something?badparam=%rce_payload%”  

был бы сформирован следующий HTTP-запрос:

GET /appname/MalPath/something?badparam=%rce_payload% HTTP/1.1  [...]  

Если после обнаружения, что метод GET блокируется на уровне WAF, злоумышленник предпринял бы попытку реализовать атаку через POST запрос:

POST /appname/MalPath/something HTTP/1.1  [other HTTPheaders]    badparam=%rce_payload%  

Сработала ли бы в этом случае полезная нагрузка, учитывая новую логику правила? Да, сработала бы. Почему? Ответ кроется в строках 17 и 18, как показано на рисунке ниже:


Рисунок 6: Проверка присутствия в запросе строк «/mathpath/» и «badparam=»

Сравним содержимое POST запроса злоумышленника с логикой нашего правила. В строке 17 происходит поиск строки «/malpath/» в полностью декодированном и преобразованном в нижний регистр URI. Проверка завершается успешно. Однако строка 18 проистекает из неверного предположения. Параметры POST запроса располагаются в теле HTTP, но не строке запроса! Строка 18 правила проверяет на предмет присутствия строки «badparam=» в переменной «$decodedUri», которая в случае использования POST запроса содержала бы строку «/appname/MalPath/something». Короче говоря, правило не проверяет строку «badparam=» там, где нужно, и, соответственно, отрабатывается некорректно.

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

Заключение

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

На сегодня всё. В будущем будут опубликованы другие похожие истории.

До связи.