
Всем привет! Меня зовут Сергей Бобров aka BlackFan. Я работаю в Лаборатории Касперского, занимаюсь анализом защищенности мобильных и веб-приложений, участвую в пентестах и редтимах. В свободное время я также разрабатываю собственные инструменты, занимаюсь исследованиями в области безопасности и ищу баги на платформе Standoff Bug Bounty. Сегодня мы разберем на практике мисконфигурации, связанные с объектным хранилищем S3, и углубимся в проблемы, возникающие при его интеграции в веб-приложения через прокси (например, nginx). В основе статьи — 10 лабораторных стендов, каждый из которых поясняет различные уязвимости интеграции: от классической хранимой XSS и подмены бакетов до обхода правила rewrite и отравления кэша.
Все сказанное ниже можно посмотреть и послушать в видео (также в VK видео).
Статья носит исключительно информационный характер и не является инструкцией или призывом к совершению противоправных действий. Ее цель — исключительно образовательная: рассказать о существующих уязвимостях, которыми могут воспользоваться злоумышленники, предостеречь пользователей и дать рекомендации по защите личной информации в интернете. Автор не несет ответственности за использование опубликованной информации. Помните, что нужно следить за защищенностью своих данных.
S3: что нужно знать багхантеру
S3 (Simple Storage Service) — это сервис, разработанный компанией Amazon для предоставления доступа к объектному хранилищу в облаке AWS. Поскольку Amazon был первопроходцем в сфере облачных сервисов, его решения стали отраслевым стандартом. Сегодня под «S3» подразумевается не просто конкретный продукт, а совместимое API и подход к хранению данных.
Базовая функциональность S3 описывается всего четырьмя HTTP-запросами:
Создание или редактирование объекта в бакете (контейнере) —
PUT;Получение объекта —
GET;Удаление объекта —
DELETE;Листинг (список объектов) бакета —
GETна корень бакета.
На практике разработчики часто воспринимают S3 как «просто папку в облаке», и именно в этом упрощении кроется огромное пространство для мисконфигураций и уязвимостей при интеграции S3 в работу веб-приложений.
Авторизация и подписи запросов
Для выполнения авторизованных запросов каждый пользователь получает пару: Access Key и Secret Key. Подпись формируется следующим образом:
все компоненты запроса (метод, Content-Type, дата и др.) приводятся к каноническому виду — строке, разделенной символами переноса;
вычисляется HMAC-SHA1 от этой строки;
результат кодируется в Base64 и передается в заголовке
Authorizationили в query-параметрах.

Существует два основных формата подписи: v2 (устаревшая, но еще используется во многих совместимых решениях) и v4 (более сложная). В своих примерах я использую v2 из-за ее простоты и наглядности. Для временного доступа к объектам применяются подписанные URL с параметром Expires.
Два способа интеграции S3 в веб-приложения
Через S3-клиент в коде приложения
Разработчики создают клиент, указывают endpoint, пару значений Access Key и Secret Key, а затем вызывают соответствующие методы S3. В этом случае атакующий ограничен лишь элементами, передаваемыми в метод клиента: содержимым файла, Content-Type, именем файла.
Через прокси (nginx)
Часто S3 подключается как хранилище статики через nginx с помощью директивы proxy_pass. При такой конфигурации все возможности S3-протокола становятся доступны злоумышленнику, потому что nginx не фильтрует методы и заголовки (кроме префикса пути). Это самый интересный для нас случай, поскольку здесь возникает большинство уязвимостей.
Контроль доступа: ACL и Bucket Policy
Управление доступом в S3 реализовано двумя способами:
S3 Bucket ACL (Access Control List) — список правил, который определяет разрешения для бакета или отдельных объектов. Самая опасная ошибка здесь в том, что разработчики часто неверно трактуют группу
AuthenticatedUsers. Это не пользователи одного аккаунта, а все авторизованные пользователи S3 (в рамках данного облака) во всем мире. Такая ошибка приводила к масштабным утечкам, а иногда и к шифрованию бакетов с требованием выкупа. Например, если вы пользуетесь облаком Яндекса, то любой другой пользователь при наличии такой мисконфигурации может получить доступ к вашему бакету.S3 Bucket Policy — JSON-документ, позволяющий задавать правила на уровне бакета: разрешения по префиксам, условиям, конкретным пользователям. В отличие от ACL, политика применяется ко всему бакету, что упрощает администрирование, но при ошибках также открывает широкие возможности для атак.
Идентификация S3-систем и разведка
Перед атакой важно понять, с какой именно S3-системой мы имеем дело. Я составил таблицу S3 Fingerprint, где собраны характерные заголовки и страницы ошибок для разных реализаций. Она позволяет по ответу на запрос идентифицировать S3. Второй репозиторий — это специальное расширение для Burp Suite и Caido.
Рекомендации:
Сделайте запрос к существующему и несуществующему объекту — сравните ответы. Это позволяет понять, как устроено проксирование, выявить, какие заголовки добавляет фронтенд, раскрывается ли служебная информация S3.
Обратите внимание на заголовок
X-Amz-Request-Id— его формат (длина, регистр, префиксы) часто указывает на конкретную S3-совместимую систему (MinIO, Ceph RGW, Yandex Cloud, AWS).Используйте расширение для Burp Suite (например, Hackvertor) для быстрой генерации подписанных запросов с некорректной сигнатурой. Это может раскрыть каноническое представление запроса, включая имя бакета.
Практические стенды
Перейдем к серии лабораторных работ. Все стенды внешне будут практически идентичны: это имиджборд с изображениями. Однако, несмотря на одинаковый визуал, разница всего в одном символе в конфигурации сайта приведет к разным ошибкам, каждая из которых эксплуатируется по-своему.
Ошибки в правилах проксирования
Прокси не знает семантики S3 и работает с декодированным и нормализованным значением пути. Ошибка в одном символе правила разделяет то, что видит nginx, и то, что уходит в хранилище, и именно на этом расхождении строятся атаки ниже.
Подмена бакета через отсутствие конечного слеша
В этом стенде ошибка в том, что разработчик забыл указать завершающий слеш в правиле /static. Если обратиться к /staticxxx, то при проксировании префикс static заменится на имя бакета, а все дополнительные данные склеятся с ним. В результате запрос уйдет к несуществующему бакету.

Таким образом, мы можем создать бакет с оригинальным префиксом, добавить к нему какие-то данные, залить любые файлы и обратиться к этим файлам через сайт, с которым мы взаимодействуем. Например, создав в облаке атакующего такой бакет, мы загрузили в него файл xss.html, содержащий типичный эксплойт. При обращении через уязвимый сайт у пользователей срабатывал XSS.

32:17–36:18 (демонстрация решения)
HTTP Request Splitting через $uri и кража cookies
В этом стенде правило прокси использует переменную $uri, которая содержит декодированный путь запроса. Это позволяет злоумышленнику внедрять в путь служебные символы (например, пробелы и переводы строк) и использовать HTTP Request Splitting для модификации запроса. Уязвимость детектируется так: добавление пробела и буквы H вызывает ошибку 400 (воспринимается как версия протокола). Используя эту технику, атакующий подменяет заголовок Host и перенаправляет запрос на произвольный бакет, загружая XSS-эксплойт. Более того, сформировав PUT-запрос с той же уязвимостью и отправив его через XSS, можно записать в бакет атакующего часть HTTP-запроса пользователя вместе с его сессионными cookies (включая HttpOnly). Для подписи таких запросов создается сервисный аккаунт с ролью storage.uploader.

Если кратко, то мы нашли возможность проводить XSS-атаки, а также заставляем браузер пользователя записывать чувствительные данные в файл на подконтрольном нам хранилище.
36:19–50:11 (демонстрация решения)
Обход rewrite через %0A
В этой лабораторной используется Ceph Rados Gateway, но здесь имя бакета заменяется с помощью регулярного выражения — правила rewrite. В такой конфигурации возникает следующая проблема: зачастую в этих правилах используются символы начала строки (^) и конца строки ($) в регулярном выражении. Но nginx взаимодействует с декодированным и нормализованным значением пути, и, если после папки static указать символ переноса строки (%0A), регулярное выражение не сработает. Корректное имя бакета не добавится в HTTP-запрос.

В результате папка static остается в запросе, но proxy_pass с префиксом static заменяется на одинарный слеш, и все, что идет после static, становится именем бакета. Добавив %0A в середину названия объекта, получаем ошибку NoSuchBucket. Значит, правило подстановки не сработало, и мы можем обратиться к произвольному бакету (например, test) с нужным нам постфиксом.
Перебрав имена бакетов в Intruder, находим существующий бакет public и точно так же формируем PUT-запрос на создание файла. В результате мы опять получаем возможность выполнять XSS через проксирование на S3.
55:47–59:40 (демонстрация решения)
Отравление кэша через параметры запроса
В этом стенде нет основного приложения — весь трафик проксируется в S3 и кэшируется nginx на одну минуту. Ключ кэша формируется только по пути, без учета query-параметров. Здесь ошибка в том, что разработчики воспринимают S3 как статичную папку и не учитывают параметры, меняющие ответ.
Мы отправляем запрос к index.html с параметром ?response-content-type=image/png. S3 возвращает 200 OK с измененным Content-Type. nginx закэшировал этот ответ, и все последующие пользователи получали index.html с типом image/png. Таким образом, мы можем временно блокировать функциональность сайта.
А вот попытка использовать параметр ?acl, который часто доступен для неавторизованных пользователей, не удалась: у нашего бакета корректно заданы привилегии, поэтому возвращается ошибка 403 — такие ответы не попадают в кэш.
1:16:04–1:20:00 (демонстрация решения)
Особенности S3-совместимых систем
Здесь конфигурация проксирования выглядит вполне адекватно, но S3-совместимые системы ведут себя не так, как ожидает разработчик: по-разному нормализуют путь и по-разному раскрывают служебную информацию в ошибках.
Обход пути через .. в Ceph RGW
На этот раз используем Ceph Rados Gateway. Это более продвинутое приложение по сравнению с MinIO.

Здесь правила проксирования выглядят вполне адекватно, но проблема заключается в том, что у proxy_pass в nginx есть два вида проксирования: с указанием пути в правиле или без него. Для nginx оба запроса выглядят одинаково, но, если путь не задан, запрос улетает на upstream-сервер именно так, как его указал пользователь.

Это приводит к тому, что для S3-систем, которые не нормализуют путь (в отличие от файловой системы), символы ../ становятся корректной частью имени объекта. В результате мы можем обращаться к произвольным бакетам, но с неизменяемым постфиксом.
Наша цель — обратиться к внутреннему бакету public, который не предназначен для внешнего взаимодействия. При обращении к существующему файлу мы видим новый формат заголовка x-amz-request-id (содержится длинное значение с префиксом tx...), что соответствует Ceph RGW.
Сформировав ненормализованный путь с .., пытаемся обратиться к другому бакету, и получаем в ответ NoSuchBucket. Для перебора существующих внутренних бакетов используем Intruder. Однако у ошибок NoSuchBucket одинаковый код и размер ответа, поэтому нужно добавить правило извлечения текста из контента, и по регулярному выражению или разделителям извлекаем код ответа. В одном из значений мы находим ошибку NoSuchKey: это означает, что бакет public существует.
Мы формируем PUT-запрос, загружаем произвольный файл в некорректно настроенный бакет и обращаемся к нему через уязвимое веб-приложение. Поскольку браузер нормализует путь с .., нам пришлось закодировать один из слешей, чтобы запрос отправился корректно. В итоге мы снова получаем XSS.
50:10 – 55:41 (демонстрация решения)
Поиск бакетов через нотацию имен
Мы выяснили, что имя бакета передается в Host-заголовке (virtual-host-style). При запросе с некорректной подписью нам возвращается подробная ошибка с кодом 403, внутри которой раскрывается каноничное представление запроса и содержится имя бакета.
Так как речь идет о публичном объектном хранилище, разработчики стараются использовать единую нотацию именования бакетов. Зная корпоративную нотацию, мы можем перебирать возможные имена бакетов через Burp Intruder (тип атаки — Cluster bomb attack). Ожидаем три типа ответов:
404— бакет не существует;403— существует, но листинг запрещен;200— существует и некорректно настроен.
Таким образом мы нашли существующий бакет другого приложения, который раскрывает чувствительные данные.
24:41–32:17 (демонстрация решения)
Злоупотребление штатными возможностями протокола
Прокси настроено корректно, но сам протокол S3 остается доступен снаружи: листинг, подписанные ссылки, подмена Content-Type, все это штатные возможности, которые при неосторожной интеграции превращаются в вектор атаки.
Базовый листинг и загрузка файлов (XSS)
В этом стенде бакет был доступен только через прокси с префиксом /static. Мы выяснили, что:
Листинг бакета включен по умолчанию, и мы можем перебирать объекты.
Доступна загрузка (PUT) для неавторизованных пользователей.

Загрузив test.html с Content-Type text/html, мы получили возможность выполнять произвольный HTML/JS в контексте сайта. Это классическая хранимая XSS.
Если мы видим такую мисконфигурацию, а помимо статических файлов здесь хранится и JavaScript, подключаемый на главной странице, мы можем перезаписать его и внедрить вредоносный скрипт для всех посетителей. В продакш-системах так делать не рекомендуется, потому что можно легко нарушить функциональность сайта.
12:04-21:10 (демонстрация решения)
Подписанные ссылки и листинг бакета
В этом стенде была реализована функциональность скачивания через подписанные URL. В этом случае на эндпойнт /download передается путь к файлу, который мы скачиваем. И если мы вместо пути к объекту укажем, например, слеш (/), то сервер нам вернет подписанную ссылку, указывающую на корень бакета. В случае с S3-системой такая ссылка предоставляет список всех объектов в бакете, для которых мы затем можем сгенерировать подписанные ссылки и получить доступ к их содержимому.

21:12 - 24:03 (демонстрация решения)
Service Worker и подмена контента
В этом сценарии приложение использовало два отдельных хоста: один взаимодействует с S3-бакетом, а второй — с основным приложением (flask). Такое разделение часто используется для того, чтобы контент пользователя изолировать от основного приложения. Наша цель — заставить легитимного пользователя при попытке скачать свой файл получить файл атакующего.

Создав аккаунт атакующего в отдельном браузере, чтобы эксплуатация была корректной, загружаем туда, например, XSS. В данном приложении обязательное использование постфикса .webp. И если мы попытаемся загрузить HTML-файл с расширением .webp, то при скачивании получим некорректное изображение, потому что приложение автоматически подставляет соответствующий Content-Type.
Чтобы изменить Content-Type на text/html и сделать загруженный нами HTML-файл исполняемым, мы используем параметр response-content-type в GET-запросе. Однако из-за изоляции поддомена это не дает доступа к cookie основного приложения.
Здесь нам поможет Service Worker: загружаем на поддомен JavaScript-файл с правильным Content-Type и с помощью отдельного HTML-файла регистрируем его как Worker. Теперь он перехватывает все fetch-запросы пользователя на этом поддомене, отправляет информацию (пути к файлам) на наш коллаборатор и подменяет ответы на ссылку, ведущую на наш вредоносный файл (например, злую картинку).
1:06:20–1:16:03 (демонстрация решения)
Когда классы уязвимостей сшиваются в цепочку
Реальная атака редко упирается в одну ошибку. Последний стенд показывает, как утечка на стороне приложения и штатная возможность протокола складываются в единый вектор.
Раскрытие ключей через debug-режим Django
В этой лабораторной приложение основано на Django, которое работает в debug-режиме. Наша цель здесь — обратиться к бакету secret и к его файлу secret.txt, который не должен быть доступен извне.
При искусственном вызове ошибки нам вывелась полная трассировка со значениями переменных окружения. Здесь есть автоматическое маскирование, однако у нас учетная запись S3 задана в виде коротких обозначений S3_SK и S3_AK, и таким образом мы смогли раскрыть учетную запись, которая используется для взаимодействия с S3 на этом сайте. Имея этот ключ, мы можем подписывать любые запросы к S3.
Несмотря на то, что прокси было настроено корректно, мы использовали специальный заголовок X-Amz-Copy-Source для копирования объекта из внутреннего бакета secret в публично доступный. Таким образом, мы обошли ограничения прокси и вытащили данные, к которым не имели прямого доступа.
59:41–1:06:14
https://youtu.be/SwEpvGKsidc?t=3580 (демонстрация решения)
Вывод
Исследование уязвимостей S3-интеграций важно, потому что даже один пропущенный символ в конфигурации nginx может обернуться компрометацией данных и сессий пользователей. Наш материал показывает, что большинство проблем возникает не в S3 как таковом, а на этапе его интеграции — а это слепая зона, которую разработчики часто упускают из виду. Для багхантера знание этих сценариев — это готовый набор векторов для реальных атак — от простого листинга бакетов до кражи HttpOnly-cookies в обход ограничений.

