Удаленное выполнение кода (RCE) через инъекцию шаблонов на стороне сервера (SSTI)
В этой статье мы увидим, как я определил уязвимость удаленного выполнения кода и обошел правила Akamai WAF. Пока я проводил проверку безопасности, я заметил конечную точку, которая включает контролируемые пользователем данные в строку и отражает их обратно в ответе. Заметив отражение текста, я попробовал некоторые полезные функции XSS, но не смог успешно выполнить JavaScript, поскольку ответом Content-Type был application / json. Тем не менее, при вводе полезной нагрузки вида ${191*7} оказалось, что арифметическое выражение было успешно вычислено в ответе как:
[SNIP]…getApprovalGroupByContext.contextType: 1337…[/SNIP].
Примечание: «RCE_» не является частью нагрузки, оно используется только для поиска отраженного текста.
Требование символа $ в синтаксисе для успешного выполнения выражения обычно указывает на использование некоторого шаблонизатора или серверной обработки выражения.
Шаблонизаторы широко используются веб-приложениями для отображения динамических данных на веб-страницах и в электронных письмах. Небезопасное внедрение пользовательских данных в шаблоны позволяет выполнить серверную инъекцию шаблона.
В данном случае пользователь управляет содержимым параметра context_type в запросе. После выявления инъекции шаблона следующим шагом было определение используемого шаблонизатора. Иногда это можно сделать, отправив некорректный синтаксис, так как шаблонизаторы могут идентифицировать себя в сообщениях об ошибках. Однако данный метод не работает, если сообщения об ошибках скрыты. После небольшого исследования было сделано несколько предположений о том, какой шаблонизатор используется в данном приложении.
Чтобы оценить влияние уязвимости, я провел дополнительные исследования, чтобы найти полезную нагрузку, которую я мог бы использовать для удаленного выполнения команд, и я обнаружил, что следующая полезная нагрузка может быть использована для выполнения команды ls на удаленном сервере:
${"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("ls")}.
После выполнения я не увидел результатов выполнения команды в ответе, потому что это была слепая инъекция, но я получил ссылку на процесс Unix (выделено в теле ответа выше). Это подтвердило, что команда была выполнена успешно.
Теперь стало интереснее, так как я хотел загрузить обратную оболочку (тип оболочки, в которой целевой компьютер взаимодействует с атакующим компьютером). Я выполнил ещё две команды, чтобы проверить, установлены ли уже модули python и wget, необходимые для загрузки и выполнения сценария обратной оболочки. В результате выполнения я получил ссылку на процесс, указывающую на то, что утилиты предустановлены. Это неудивительно, так как подобные утилиты часто поставляются в комплекте, в ОС Unix.
Далее с помощью Python был запущен HTTP-сервер на 80 порту, и на нем размещён скрипт обратной оболочки:
Сначала была загружена обратная оболочка на уязвимый сервер.
Ссылки на процесс в ответе было достаточно, чтобы понять, что сценарий обратныой оболочки был загружен на удаленный компьютер. Следующим шагом был запуск слушавателя Netcat на 443 порту (чтобы поймать входящее соединение) и выполнять команды. Как вы можете видеть на скриншоте ниже, я смог получить обратную оболочку от тестового сервера и выполнять команды.
Следующей задачей было обойти WAF Akamai для выполнения удаленного выполнения кода на рабочем сервере.
От рабочего сервера был получен тот же ответ на базовые математические операции, такие как умножение и деление, что подтвердило наличие ошибки и на рабочем сервере. Однако финальная нагрузка не сработала, вернулась страница с ошибкой 403 и сообщение об отказе в доступе.
Было решено разделить нагрузку на несколько частей, чтобы проверить, что является безопасным и небезопасным с точки зрения правил WAF. Такой метод проб и ошибок позволил выявить два ключевых слова, которые WAF считал небезопасными:
Нагрузка для выполнения выглядела так:
${"".getClass().forName("java.lang.Runtime").getMethods()[6].invoke("".getClass().forName("java.lang.Runtime")).exec("wget")}.
Для обхода первой проверки была изучена документация по JS, чтобы найти альтернативные способы возврата строки “java.lang.Runtime”. После анализа было найдено несколько способов, но в данном случае сработал только метод concat.
“java.lang.Runtime” === “java.lang”.concat(“.Runtime”).
Таким образом была пройдена первая проверка. Текущая нагрузка:
${"".getClass().forName("java.lang".concat("Runtime")).getMethods()[6].invoke("".getClass().forName("java.lang".concat("Runtime"))).exec("wget")}.
Следующей задачей было обойти проверку ключевого слова ().. Было замечено, что если вставить любой символ между () и ., то firewall не блокирует его, однако это нарушает выполнение метода. Пришлось задуматься о том, можно ли обойти это, не нарушив цепочку вызовов методов. Возникла идея использовать самовызванные функции, как в JavaScript. Хотя опыта программирования на JS не было, решено было попробовать.
В JavaScript конструкции console.log(“hello”), (console.log(“hello”)) и (console.log)(“hello”) эквивалентны.
Метод getClass() был заменён на (getClass()), и, так как не было проверки на ключевое слово (), удалось обойти и вторую проверку.
${("".getClass()).forName("java.lang".concat("Runtime")).getMethods()[6].invoke(("".getClass()).forName("java.lang".concat("Runtime"))).exec("wget")}.
После выполнения нагрузки был получен процесс, аналогичный тому, который был на тестовом сервере, что подтвердило возможность выполнения команд удалённо на рабочем сервере.
По очевидным причинам не была предпринята попытка загрузки каких-либо вредоносных файлов на целевой сервер, однако было проверено, можно ли инициировать исходящий запрос с сервера на Burp Collaborator.
Далее запрос был отправлен на сервер, для проверки выполнения DNS-запроса.
Через несколько секунд был получен DNS-запрос, что раскрыло реальный IP-адрес атакуемого сервера.
Лучший способ предотвратить серверную инъекцию шаблона — не позволять пользователям изменять или отправлять новые шаблоны. Однако это не всегда возможно из-за бизнес-требований.
Один из самых простых способов избежать уязвимости — использовать шаблонизаторы без логики, такие как Mustache, если необходимо. Максимальное разделение логики и представления может значительно снизить риск атак.
Еще одной мерой является выполнение пользовательского кода в песочнице, где потенциально опасные модули и функции полностью удалены. К сожалению, создание защищённых песочниц для недоверенного кода крайне сложно и подвержено обходам.
Ещё один подход — принять неизбежность произвольного выполнения кода и развернуть шаблонизатор в закрытом контейнере, таком как Docker.
— Следует применять белые списки URL для предотвращения загрузки вредоносного кода или утечки данных.
— Ни один WAF не может быть на 100% точным, и ни один WAF не является абсолютной защитой.
— Если обнаружена ошибка, её следует исправить на уровне кода, не полагаясь только на WAF.
— Всегда следует стремиться максимизировать влияние выявленных уязвимостей.
— Знание зависимостей бекенда и фронтенда, а также используемых фреймворков заранее является преимуществом.
https://portswigger.net/web-security/server-side-template-injection
https://portswigger.net/research/server-side-template-injection
https://medium.com/server-side-template-injection/server-side-template-injection-faf88d0c7f34
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection#expression-language-el---code-execution