Удаленное выполнение PHP-кода: приемы обхода фильтров и правил WAF

Удаленное выполнение PHP-кода: приемы обхода фильтров и правил WAF

В этой заметке мы рассмотрим различные возможности в PHP, позволяющие удаленно обходить фильтры, проверки входных данных и правила в WAF.

Автор: Andrea Menin

В этой заметке мы рассмотрим различные возможности в PHP, позволяющие удаленно обходить фильтры, проверки входных данных и правила в WAF.

Обычно, когда я пишу подобные статьи, всегда кто-нибудь спросит «а реально такой код пишут люди?». Сразу понятно, что спрашивающий не является пентестером. Прежде, чем вы спросите меня снова, отвечу загодя: ДА и ДА.

Ниже показан один из уязвимых скриптов, который я буду использовать в своих экспериментах. Скрипт очень простой, но я хочу лишь продемонстрировать уязвимость, связанную с удаленным выполнением кода (возможно, в реальном случае придется сделать чуть больше телодвижений, чтобы оказаться в такой ситуации).

Рисунок 1: Первый PHP-скрипт

Очевидно, что шестая строка - явное недоразумение. В третьей строчке стоит нечто похожее на фильтр функций system, exec и passthru (в PHP есть много других функций, при помощи которых можно выполнять системные команды, но давайте остановимся на вышеуказанных трех). Этот скрипт работает на веб-сервере за фаерволом CloudFlare. Как обычно, я всегда использую CloudFlare, не потому, что этот фаервол плохо защищен, а потому что хорошо известен. У других подобных приложений те же самые проблемы (плюс минус). Второй скрипт будет находиться за связкой ModSecurity + OWASP CRS3.

Чтение файла /etc/passwd

Во время первого теста я попробую прочесть файл /etc/passwd при помощи функции system() внутри запроса /cfwaf.php?code=system(“cat /etc/passwd”);

Рисунок 2: CloudFlare заблокировал первую попытку

Как видно на рисунке выше, CloudFlare заблокировал первый запрос (возможно потому, что там была строка «/etc/passwd»), однако подобная проверка обходится при помощи замены на cat /etc$u/passwd.

Рисунок 3: CloudFlare удалось обойти, но теперь запрос оказался заблокированным фильтром внутри скрипта

Нам удалось обойти CloudFlare, но теперь возникла проблема с фильтром, поскольку мы использовали функцию system. Сразу же возникает вопрос, можно ли выполнить функцию system без использования строки «system». Обратимся к соответствующему разделудокументации PHP.

Escape-последовательности в PHP

\[0–7]{1,3} – последовательность символов в восьмеричной нотации, которая по тихому переполняется, чтобы уместиться в байте (например, «\400» === «\000»).

\x[0–9A-Fa-f]{1,2} – последовательность символов в шестнадцатеричной нотации (например, «\x41»).

\u{[0–9A-Fa-f]+} – последовательность кодовых точек кодировки Unicode, которая будет отображена в виде строки, где каждая кодовая точка представлена в формате UTF-8 (добавлено в PHP 7.0.0).

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

Функции в виде переменных

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

То есть синтаксис $var(args); и “string”(args); эквивалентны function(args); Если мне доступен вызов функции через переменную или строку, то я также могу использовать escape-последовательность вместо имени функции. Пример:

Рисунок 4: Вызов функции различными способами

Последний способ представляет собой escape-последовательность символов в шестнадцатеричном формате, которую PHP конвертирует вначале в строку «system», а затем в функцию system с аргументом «ls». Пробуем выполнить обновленный запрос:

Рисунок 5: Проверку внутри скрипта удалось обойти

Приведенная техника не работает с функциями, определяющими конструкцию языка: echo, print, unset(), isset(), empty(), include, require и тому подобные. Если вы хотите воспользоваться подобными методами, понадобится обертка.

Улучшение фильтра входных данных

Что произойдет, если я добавлю в фильтр проверку на двойные и одинарные кавычки? Смогу ли я обойти новый фильтр без использования двойных кавычек? Пробуем:

Рисунок 6: Добавляем в условие двойные и одинарные кавычки

После внесенных изменений в переменной $_GET[code] нельзя использовать двойные и одинарные кавычки. Теперь полезная нагрузка должна быть заблокирована:

Рисунок 7: Полезная нагрузка с кавычками стала попадать под фильтр

Однако в PHP нам не всегда нужны кавычки при использовании строк. Можно объявить тип элемента, например, так $a = (string)foo; В этом случае переменная $a будет содержать строку «foo». Более того, все, что находится в круглых скобках без специального объявления, интерпретируется как строка:

Рисунок 8: Различные варианты использования строк

В результате мы можем обойти новый фильтр двумя способами. Первый способ – использовать конструкцию (system)(ls);. Однако, поскольку мы не можем вставить слово system внутрь параметра, нужно соединить несколько строк, например, так (sy.(st).em)(ls);. Второй способ – использовать переменную $_GET. Если я отправлю запрос ?a=system&b=ls&code=$_GET[a]($_GET[b]);, переменная $_GET[a] будет заменена на строку «system», $_GET[b] – на строку «ls», и в итоге я смогу обойти новый фильтр!

Рисунок 9: Методы обхода нового фильтра

Пробуем первую полезную нагрузку (sy.(st).em)(whoami);

Рисунок 10: Первая полезная нагрузка успешно прошла через WAF и проверку в скрипте

Пробуем вторую полезную нагрузку ?a=system&b=cat+/etc&c=/passwd&code=$_GET[a]($_GET[b].$_GET[c]);

Рисунок 11: Вторая полезная нагрузка успешно прошла через WAF и проверку в скрипте

Вы даже можете добавить комментарии внутрь имени функции и аргументов, что может быть полезно при обходе правил в WAF, которые блокируют определенные имена методов. Все нижеуказанные синтаксисы корректны:

Рисунок 12: Варианты использования комментариев

Функция get_defined_functions

Функция возвращает многомерный массив, содержащий перечень всех объявленных функций, как встроенных (внутренних), так и пользовательских. Внутренние функции доступны через массив $arr[“internal”], пользовательские – через $arr[“user”]. Например:

Рисунок 13: Перечень внутренних функций

Этим методом также можно добраться до функции system без указания имени. Если я поищу среди элементов массива по слову «system», то смогу найти индекс элемента, где хранится эта функция, который затем можно использовать в запросе.

Рисунок 14: Использование функции через элемент массива

Естественно, приведенный метод должен сработать и при обходе правил в WAF и во время проверки в самом скрипте:

Рисунок 15: Обход проверок при помощи функции get_defined_functions

Массив символов

Каждая строка в PHP может быть представлена в виде массива символов (примерно так же как в Python), и вы можете ссылаться на отдельные символы, используя синтаксис $string[2] или $string[-3] . Этот способ также можно использовать для обхода правил, блокирующих имена функций. Например, при помощи строки $a=”elmsty/ “; я могу соорудить следующую строку system(“ls /tmp”);

Рисунок 16: Сборка строки из отдельных символов, представляющих собой элементы массива

Если вы окажетесь особо удачливы, то сможете найти все нужные символы внутри имени файла скрипта. Используя ту же самую технику, можно достать все необходимые символы через конструкцию (__FILE__)[2] :

Рисунок 17: Сборка строки из символов имени файла

OWASP CRS3

Скажу сразу, что если используются наборы правил OWASP CRS3, то ваша жизнь значительно усложняется. Во-первых, используя техники, упомянутые выше, мне удалось обойти только первый уровень, что в целом хороший результат. Поскольку первый уровеньParanoia Level 1 представляет собой небольшой набор правил, которые можно найти в CRS3. Level 1 уровень реализован так, чтобы исключить любые ложные срабатывания. На втором уровне Paranoia Level 2 жизнь становится еще сложнее из-за правила 942430, которое именуется так «Restricted SQL Character Anomaly Detection (args): # of special characters exceeded». Мне удалось только запустить команду без аргументов: «ls», «whoami» и так далее, но не удалось запустить команды, подобные следующей «cat /etc/passwd», как в случае с приложением CloudFlare WAF:

Рисунок 18: Результаты экспериментов с правилами OWASP CRS3

Мои аккаунты в социальных сетях:

Twitter: @Menin_TheMiddle

GitHub: theMiddleBlue

LinkedIn: Andrea Menin

Ваша приватность умирает красиво, но мы можем спасти её.

Присоединяйтесь к нам!