SSRF Redirect: Обход валидации через HTTP-редирект
SSRF-уязвимость: валидация URL обходится через HTTP-редирект на внутренний адрес.
mediumgolangPro
Задача
# SSRF: обход валидации IP через цепочку HTTP-редиректов
## Сценарий
Вы аудируете внутренний сервис **SecurePreview**, в котором есть эндпоинт «предпросмотра ссылки»: пользователь передаёт URL, сервер скачивает страницу и возвращает её содержимое. Команда уверена, что эндпоинт безопасен — реализована проверка IP назначения по приватным диапазонам, перед запросом резолвится DNS и блокируются loopback/private адреса. По данным внутреннего аудита, в приложении сохранился административный API с конфиденциальными данными, доступный только при обращении с loopback-интерфейса; в его ответе раскрывается флаг.
В фильтре также есть «удобство для разработки» — список доверенных внутренних hostname'ов, которые пропускаются без IP-проверки. Типичный enterprise-паттерн: внутренние сервисы (`prometheus.internal`, `auth.local`, `lab-redirector.lab-infra.svc.cluster.local`) в allowlist'е, чтобы не плодить DNS-исключений. Если такой trusted-сервис умеет редиректить на произвольный URL (хоть как partner-tracking endpoint, хоть как helper для тестов), вся защита от SSRF разваливается.
## Цель
1. Подтвердите, что прямое обращение к внутреннему API через `url-preview` корректно блокируется проверкой IP.
2. Найдите в инфраструктуре сервис, чей hostname добавлен в allowlist фильтра, и который умеет менять адрес назначения **после** первоначальной валидации.
3. Скомбинируйте обнаруженные эндпоинты так, чтобы серверный HTTP-клиент в итоге обратился к внутреннему API и вернул его ответ. Извлеките флаг.
## Теория
Защита от SSRF, основанная на однократной валидации URL **до** запроса, имеет фундаментальный TOCTOU-разрыв (Time-Of-Check vs Time-Of-Use): между моментом проверки и фактическим завершением HTTP-обмена существуют события, способные сменить адрес назначения. Самая частая причина — поведение HTTP-клиента по умолчанию: при получении ответа `3xx` с заголовком `Location` клиент автоматически следует по новому адресу, не повторяя валидацию.
**Уязвимый паттерн:**
```go
// Проверка IP только для начального URL — клиент следует редиректам без переvalidation
checkPrivateIP(initialURL) // ok, public IP
client := &http.Client{} // дефолтные настройки следуют 3xx-редиректам
resp, _ := client.Get(initialURL)
```
Атака усиливается, если у фильтра есть allowlist «доверенных» сервисов, чья IP-проверка вообще пропускается, и хотя бы один из них поддерживает open-redirect (`/r?target=`, `/go?url=`, `/redirect?to=`). Тогда атакующему достаточно: подать в защищённый эндпоинт URL такого trusted-сервиса с параметром редиректа на внутренний адрес → внутренний редирект перенаправит клиент на loopback → серверный HTTP-клиент молча перейдёт по редиректу и вернёт ответ внутреннего API, к которому валидация изначально не пускала.
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Учётные данные | `demo` / `demo` |
| Защищённый эндпоинт предпросмотра | `GET /url-preview?url=<URL>` (валидирует IP до запроса; trusted hostnames пропускаются без проверки) |
| Кандидат для редирект-цепочки | внутренний trusted-сервис, чей hostname в allowlist'е и который умеет отвечать 302 на параметр `target` |
| Цель | внутренний административный API с конфиденциальной меткой |