Template Injection in Frontend - CSTI
CSTI, то есть Client Side Template Injection, относится к внедрению шаблона во фронтенде. Поскольку есть версия для фронтенда, существует и соответствующая версия для бэкенда, называемая SSTI, что означает "Внедрение шаблона на стороне сервера".
Прежде чем рассматривать версию для фронтенда, давайте посмотрим на версию для бэкенда.
Server Side Template Injection
При написании бэкенд-кода, который должен выводить HTML, вы можете выбрать его прямой вывод, как в чистом PHP:
<?php echo '<h1>hello</h1>';?>
Однако, когда в HTML есть динамические части, код становится все более сложным. Поэтому в реальной разработке обычно используют что-то, что называется движком шаблонов, о котором мы кратко упоминали, говоря о санитизации.
Например, на странице некоего блога часть шаблона выглядит так:
<article class="article content gallery" itemscope itemprop="blogPost">
<h1 class="article-title is-size-3 is-size-4-mobile" itemprop="name">
<%= post.title %>
</h1>
<div class="article-meta columns is-variable is-1 is-multiline is-mobile is-size-7-mobile">
<span class="column is-narrow">
<time datetime="<%= date_xml(post.date) %>" itemprop="datePublished">
<%= format_date_full(post.date) %>
</time>
</span>
<% if (post.categories && post.categories.length) { %>
<span class="column is-narrow article-category">
<i class="far fa-folder"></i>
<%- (post._categories || post.categories).map(category =>
`<a class="article-category-link" href="${url_for(category.path)}">${category.name}</a>`
).join('<span>></span>') %>
</span>
<% } %>
</div>
<div class="article-entry is-size-6-mobile" itemprop="articleBody">
<%- post.content %>
</div>
</article>
При рендеринге мне нужно просто передать объект post и объединить его с шаблоном, чтобы создать полноценную страницу статьи.
Внедрение шаблона не означает, что "злоумышленники могут манипулировать такими данными, как post," а скорее "злоумышленники могут манипулировать самим шаблоном".
Например, предположим, что у нас есть служба маркетинговых электронных писем. Обычно компании загружают в нее данные пользователей и устанавливают собственные шаблоны, такие как:
Hi, {{name}}Do you find our product fits for your needs?If not, feel free to schedule a brief 10-minute online meeting with me at your convenience.You can make a reservation <a href="{{link}}q={{email}}">here</a>
Когда шаблон напрямую используется бэкендом, используя Python с Jinja2 в качестве примера, он выглядит так:
from jinja2 import Template
data = {
"name": "Peter",
"link": "https://example.com",
"email": "test@example.com"
}
template_str = """Hi, {{ name }}
Do you find our product fits for your needs?
If not, feel free to schedule a brief 10-minute online meeting with me at your convenience.
You can make a reservation <a href="{{ link }}?q={{ email }}">here</a> wr3d"""
template = Template(template_str)
rendered_template = template.render(
name=data['name'],
link=data['link'],
email=data['email']
)
print(rendered_template)
Финальный вывод:
Hi, PeterDo you find our product fits for your needs?If not, feel free to schedule a brief 10-minute online meeting with me at your convenience.You can make a reservation <a href="https://example.com?q=test@example.com">here</a>
Выглядит нормально, но что, если мы модифицируем шаблон? Например так:
from jinja2 import Template
data = {
"name": "Peter",
"link": "https://example.com",
"email": "test@example.com"
}
template_str = """Output: {{ self.__init__.__globals__.__builtins__.__import__('os').popen('uname').read() }}"""
template = Template(template_str)
rendered_template = template.render(
name=data['name'],
link=data['link'],
email=data['email']
)
print(rendered_template)
Вывод станет: Output: Darwin, а Darwin - это результат выполнения команды uname.
Простыми словами, вы можете думать о содержимом в {{}} как о коде, который движок шаблонов выполнит за вас.
Хотя мы обычно писали просто {{name}}, на самом деле можно выполнить больше операций, например, {{ name + email }}. В вышеуказанном примере он начинается с self и использует Python, чтобы прочитать __import__, позволяя импортировать другие модули и достигать выполнения команды.
Уязвимости, которые позволяют злоумышленникам контролировать шаблон, называются внедрением шаблона. Когда это происходит на бэкенде, это называется SSTI, а когда это происходит на фронтенде, это называется CSTI.
Метод защиты прост: не рассматривайте ввод пользователя как часть шаблона. Если вам это все же необходимо, убедитесь, что движок шаблонов предоставляет функцию песочницы, которая позволяет выполнять ненадежный код в безопасной среде.
Примеры SSTI в реальности
Первый пример - это уязвимость, обнаруженная Orange в Uber в 2016 году. Однажды Orange заметил 2 в письме, отправленном Uber, и вспомнил, что он ввел {{ 1+1 }} в поле имени. Это обычная техника при поиске уязвимостей SSTI, когда в поля для ввода вводятся множество полезных нагрузок, чтобы проверить, есть ли какие-либо проблемы SSTI на основе результатов.
Затем они использовали упомянутую выше технику, чтобы найти, какие переменные можно использовать и объединить. Так как Uber также использует Jinja2, окончательный полезная нагрузка очень похожа на то, что мы только что написали, и они успешно достигли RCE с помощью SSTI.
Для более подробного процесса вы можете обратиться к: Uber Remote Code Execution via Flask Jinja2 Template Injection
Второй пример - это внедрение шаблонов Handlebars SSTI в Shopify, о котором сообщил Mahmoud Gamal в 2019 году.
Бэкенд продавцов Shopify имеет функцию, которая позволяет продавцам настраивать письма, отправляемые пользователям (аналогично приведенному ранее примеру). Они могут использовать синтаксис типа {{ order.number }} для настройки содержимого. Бэкенд использует Node.js с движком шаблонов Handlebars.
Поскольку Handlebars имеет некоторую защиту и более сложен, хакеры потратили много времени, чтобы разобраться, как атаковать его.
В конце концов, наличие SSTI - это одно, а вот не каждый шаблонный движок можно эксплуатировать для RCE.
Окончательная полезная нагрузка, которую они придумали, была очень длинной:
{{#with this as |obj|}}
{{#with (obj.constructor.keys "1") as |arr|}}
{{#with obj.constructor.name as |str|}}
{{#blockHelperMissing str.toString}}
{{#with (arr.constructor (str.toString.bind "return JSON.stringify(process.env);"))}}
{{#with (obj.constructor.getOwnPropertyDescriptor this 0)}}
{{#with (obj.constructor.defineProperty obj.constructor.prototype "toString" this)}}
{{#with (obj.constructor.constructor "test")}}
{{this}}
{{/with}}
{{/with}}
{{/with}}
{{/with}}
{{/blockHelperMissing}}
{{/with}}
{{/with}}
{{/with}}
Подробности можно найти в оригинальной статье автора: Handlebars template injection and RCE in a Shopify app
Client Side Template Injection
Понимание CSTI становится проще после понимания SSTI, поскольку принципы схожи, единственное отличие в том, что этот шаблон - это шаблон на стороне клиента.
Подождите, есть ли шаблоны на фронтенде? Безусловно!
Например, одним из них является Angular. Вот пример с официального сайта Angular:
// import required packages
import 'zone.js';
import { Component } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
// describe component
@Component({
selector: 'add-one-button', // component name used in markup
standalone: true, // component is self-contained
template: // the component's markup
`
<button (click)="count = count + 1">Add one</button> {{ count }}
`,
})
// export component
export class AddOneButtonComponent {
count = 0;
}
bootstrapApplication(AddOneButtonComponent);
Ключевое здесь - параметр под названием template. Если вы измените {{ count }} на {{ constructor.constructor('alert(1)')() }}, вы увидите всплывающее окно с предупреждением.
Необходимо использовать constructor.constructor('alert(1)')() потому что шаблон не может напрямую получить доступ к window, поэтому создается новая функция через конструктор функции.
В документации Angular упоминается Angular's cross-site scripting security model:
Шаблоны следует рассматривать как исполняемый код, и пользователю никогда не следует разрешать управлять шаблонами.
Кстати, знаете ли вы разницу между AngularJS и Angular?
Когда он был впервые выпущен в 2010 году, он назывался AngularJS, и номера версий были 0.x.x или 1.x.x. Но после версии 2 его переименовали в Angular, схожее использование, но полностью переписанный дизайн. Мы в основном будем ссылаться на старую версию, AngularJS, потому что она имеет больше проблем из-за своего возраста, и это библиотека, подходящая для помощи в атаках.
Когда AngularJS был впервые выпущен, выполнение произвольного кода также было возможно с использованием {{ constructor.constructor('alert(1)')() }}. Однако, начиная с версии 1.2.0, был добавлен механизм песочницы, чтобы предотвратить доступ к window любым возможным способом. Но когда дело доходит до атаки и защиты, исследователи безопасности не отстают, и они нашли способы обхода песочницы.
Этот цикл обхода, улучшения песочницы и снова обхода продолжался. Наконец, AngularJS объявил о полном удалении песочницы после версии 1.6. Причина в том, что песочница на самом деле не является функцией безопасности. Если ваш шаблон может быть контролирован, то это проблема которую необходимо решить, а не надеяться на песочницу. Детали можно найти в оригинальной статье объявления: Обход песочницы выражения AngularJS. Больше истории обхода можно найти в DOM-based AngularJS sandbox escapes.
В версиях AngularJS 1.x этот процесс был более удобным и простым, требуя только элемент с ng-app:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div ng-app>
{{ 'hello world'.toUpperCase() }}
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
</body>
</html>
Идеально было бы, если бы весь фронтенд контролировался AngularJS, с общением на бэкенд через API, а бэкенд не должен был бы заниматься рендерингом представления, однако в то время концепция SPA еще не была популярна, и многие веб-сайты все еще имели бэкенд, отвечающий за рендеринг представления. Поэтому было весьма вероятно написать следующий код:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
</head>
<body>
<div ng-app>
Hello, <?php echo htmlspecialchars($_GET['name']); ?>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
</body>
</html>
Хотя в коде выше закодирован ввод, в {{ alert(1) }} нет опасных символов, поэтому это все равно может привести к XSS.
Метод защиты такой же, как у SSTI. Никогда не рассматривайте ввод пользователя как часть содержимого шаблона, или это легко может вызвать проблемы.
Практический пример CSTI
Давайте приведём интересный случай в качестве примера. Масато Кинугава, исследователь в области кибербезопасности из Японии, продемонстрировал уязвимость удаленного выполнения кода (RCE) в программном обеспечении для связи Microsoft Teams во время Pwn2Own 2022. Отправив сообщение целевой стороне, можно выполнить код на их компьютере! Эта уязвимость принесла приз в размере $150,000 на Pwn2Own.
Программное обеспечение Teams для настольных компьютеров создано с использованием Electron, по сути, это веб-страница. Чтобы добиться RCE, обычно первым шагом является поиск XSS, который позволяет выполнить код JavaScript на веб-странице.
Teams также обрабатывает ввод пользователя. И на клиентской, и на серверной стороне есть санитайзеры, которые удаляют странные элементы и гарантируют, что окончательное отрендеренное содержимое безопасно. Хотя некоторый HTML можно контролировать, многие атрибуты и содержимое фильтруются.
Например, даже для имен классов разрешены только определенные имена классов. Масато обнаружил, что у санитайзера есть некоторый простор для манипуляций с именами классов. Например, есть правило типа swift-*, поэтому и swift-abc, и swift-; [] ()'% допускается в качестве имен классов.
Но что пользы только от манипуляций с именами классов?
Вот ключ: веб-страница Teams написана на AngularJS, у которого есть много магических функций. Одна из них – атрибут ng-init, используемый для инициализации, вот так:
<!DOCTYPE html>
<html lang="en">
<body>
<div ng-app>
<div ng-init="name='test'">
{{ name }}
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
</body>
</html>
Это отобразит test на странице, указывая на то, что код внутри ng-init выполнен.
Так что если вы измените его на ng-init = "constructor.constructor ('alert (1)') ()", появится предупреждающее окно.
Какое это имеет отношение к ранее упомянутому названию класса? Оказывается, что этот ng-init также можно использовать внутри имени класса:
<div class="ng-init:constructor.constructor('alert(1)')()"></div>
Поэтому, объединив ранее упомянутые правила проверки имен классов, можно составить имя класса, содержащее ранее упомянутую полезную нагрузку, и успешно выполнить XSS.
Оригинальная статья также включает раздел о том, как AngularJS анализирует имена классов и обходит песочницу AngularJS для этой версии. Превращение XSS в RCE потребует дополнительный средств, но поскольку они не связаны с рассмотренной в этой статье CSTI, они пропускаются. Я настоятельно рекомендую прочитать оригинальную презентацию: How I Hacked Microsoft Teams and got $150,000 in Pwn2Own.
AngularJS и CSP Bypass
На практике AngularJS наиболее часто используется для обхода CSP. Если вы можете найти AngularJS в разрешенных путях CSP, существует большая вероятность его обхода. Например:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src https://cdnjs.cloudflare.com">
</head>
<body>
<div ng-app ng-csp>
<input id="x" autofocus ng-focus="$event.composedPath() | orderBy:'(z=alert)(1)'">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.3/angular.min.js"></script>
</body>
</html>
Настройка CSP выглядит строгой и разрешает только https://cdnjs.cloudflare.com, но это позволяет нам подключить AngularJS, что приводит к XSS.
Хотя это может показаться простым, при более тщательном рассмотрении это не так просто. Подумайте, CSP не имеет unsafe-eval, поэтому ни одна строка не может быть выполнена как код. Но тогда как все эти строки внутри ng-focus выполняются? Разве это не выполнение строк как кода?
Именно здесь включается AngularJS. В стандартном режиме AngularJS использует eval или подобные методы для анализа вводимых вами строк. Однако, если вы добавите ng-csp, это скажет AngularJS переключиться в другой режим. Он будет использовать свой собственный интерпретатор для анализа строк и выполнения соответствующих действий.
Таким образом, можно считать, что AngularJS реализует свой собственный eval для выполнения строк как кода, не используя эти стандартные функции.
Когда мы обсуждали обход CSP ранее, я упоминал, что сделав конфигурацию путей более строгой, можно "снизить риск", а не "полностью устранить опасность". Примером являлось установка его на https://www.google.com/recaptcha/ вместо https://www.google.com.
На самом деле, в GoogleCTF 2023 было задание обойти CSP https://www.google.com/recaptcha/, и решение использовало AngularJS. Вот почему я сказал, что строгий путь может снизить риск, но не может полностью избежать его:
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Security-Policy" content="script-src https://www.google.com/recaptcha/">
</head>
<body>
<div ng-controller="CarouselController as c" ng-init="c.init()">
[[c.element.ownerDocument.defaultView.alert(1)]]
<div carousel>
<div slides></div>
</div>
</div>
<script src="https://www.google.com/recaptcha/about/js/main.min.js"></script>
</body>
</html>
Если вас интересует обход CSP в AngularJS, вы можете обратиться к статье: Automatically Finding Alternatives to prototype.js in AngularJS CSP Bypass, где представлен другой метод обхода.
Заключение
Рассмотренная на этот раз CSTI также является типом атаки "непрямого выполнения JavaScript".
Когда вы кодируете все выводы и думаете, что это безопасно, забывая, что на вашем фронтенде есть AngularJS, злоумышленники могут достичь XSS через казалось бы безопасные {{}}, используя CSTI.
Хотя сейчас сайтов, использующих AngularJS, становится все меньше и меньше, и все меньше людей рассматривают ввод пользователя как часть шаблона. Многие уязвимости попросту еще не были обнаружены.
Если ваш сервис использует AngularJS, убедитесь, что нет проблем с CSTI.
Last updated