SSRF攻击与防御
约 1630 字大约 5 分钟
ssrfsecurity
2025-08-16
概述
服务端请求伪造(Server-Side Request Forgery, SSRF)是攻击者利用服务端发起网络请求的功能,让服务器访问攻击者指定的内部或外部资源。SSRF 在云原生环境中尤其危险,因为它可以直接访问云平台的元数据服务获取临时凭证。
攻击原理
常见 SSRF 漏洞场景
1. URL 获取功能
# 漏洞示例:用户提供 URL,服务端获取内容
import requests
from flask import Flask, request
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
# 直接使用用户输入的 URL 发起请求(SSRF 漏洞)
response = requests.get(url)
return response.text
# 攻击者请求:
# /fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# /fetch?url=http://192.168.1.100:6379/ (访问内部 Redis)
# /fetch?url=http://127.0.0.1:8080/admin (访问本地管理接口)2. Webhook 回调
# 漏洞示例:用户配置 Webhook URL
@app.route('/webhook/config', methods=['POST'])
def set_webhook():
webhook_url = request.json['url']
# 保存 URL,后续事件触发时调用
db.save_webhook(user_id, webhook_url)
# 验证 URL 可达性(SSRF 漏洞)
requests.head(webhook_url, timeout=5)
return {"status": "configured"}3. 文件导入/图片处理
# 漏洞示例:根据 URL 下载图片
@app.route('/avatar/import')
def import_avatar():
image_url = request.args.get('url')
response = requests.get(image_url) # SSRF
# 保存图片...云元数据服务攻击
这是 SSRF 最具破坏力的利用方式。
# AWS EC2 元数据服务(IMDSv1,无需认证)
curl http://169.254.169.254/latest/meta-data/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name
# 返回 AccessKeyId, SecretAccessKey, Token → 接管 AWS 资源
# GCP 元数据服务
curl -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/
# Azure 元数据服务
curl -H "Metadata: true" http://169.254.169.254/metadata/instance?api-version=2021-02-01
# 阿里云元数据服务
curl http://100.100.100.200/latest/meta-data/DNS 重绑定攻击
DNS 重绑定可以绕过基于 IP 地址的检查。
防御措施
1. URL 解析与白名单验证
from urllib.parse import urlparse
import ipaddress
import socket
ALLOWED_SCHEMES = {'http', 'https'}
BLOCKED_HOSTS = {'localhost', 'metadata.google.internal'}
def validate_url(url: str) -> bool:
"""验证 URL 是否安全"""
try:
parsed = urlparse(url)
except Exception:
return False
# 检查协议
if parsed.scheme not in ALLOWED_SCHEMES:
return False
# 禁止用户名密码(如 http://user:pass@host)
if parsed.username or parsed.password:
return False
hostname = parsed.hostname
if not hostname:
return False
# 检查黑名单主机
if hostname.lower() in BLOCKED_HOSTS:
return False
# 解析 IP 并检查是否为私有地址
try:
resolved_ips = socket.getaddrinfo(hostname, parsed.port or 443)
for family, type_, proto, canonname, sockaddr in resolved_ips:
ip = ipaddress.ip_address(sockaddr[0])
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved:
return False
# 阻止云元数据地址
if str(ip) in ('169.254.169.254', '100.100.100.200'):
return False
except (socket.gaierror, ValueError):
return False
return True2. 防止 DNS 重绑定
import socket
import ipaddress
def safe_request(url: str, **kwargs) -> requests.Response:
"""发起安全的 HTTP 请求,防止 DNS 重绑定"""
parsed = urlparse(url)
hostname = parsed.hostname
# 步骤 1: 解析 DNS
resolved_ip = socket.gethostbyname(hostname)
# 步骤 2: 验证 IP
ip = ipaddress.ip_address(resolved_ip)
if ip.is_private or ip.is_loopback or ip.is_link_local:
raise ValueError(f"Blocked private IP: {resolved_ip}")
# 步骤 3: 直接使用解析后的 IP 发起请求(绕过第二次 DNS 查询)
# 通过 Host 头保证虚拟主机正确路由
ip_url = url.replace(hostname, resolved_ip)
headers = kwargs.pop('headers', {})
headers['Host'] = hostname
return requests.get(ip_url, headers=headers, **kwargs)3. 网络层隔离
# Kubernetes NetworkPolicy: 阻止 Pod 访问元数据服务
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: block-metadata
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 169.254.169.254/32 # 阻止元数据服务
- 10.0.0.0/8 # 阻止内网
- 172.16.0.0/12
- 192.168.0.0/16# iptables 规则:阻止应用用户访问内网
iptables -A OUTPUT -m owner --uid-owner www-data \
-d 169.254.169.254 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data \
-d 10.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data \
-d 172.16.0.0/12 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data \
-d 192.168.0.0/16 -j DROP4. AWS IMDSv2
# IMDSv2 要求先获取 token(需要 PUT 请求 + 自定义头)
# SSRF 通常无法发送 PUT 请求和自定义头
# 获取 token(需要 PUT + TTL 头)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
# 使用 token 访问元数据
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
http://169.254.169.254/latest/meta-data/
# 强制使用 IMDSv2(禁用 v1)
aws ec2 modify-instance-metadata-options \
--instance-id i-1234567890abcdef0 \
--http-tokens required \
--http-endpoint enabled5. URL 解析陷阱防御
# 攻击者可能利用 URL 解析差异绕过检查
# 绕过示例
urls_to_block = [
"http://127.0.0.1",
"http://0x7f000001", # 十六进制 IP
"http://2130706433", # 十进制 IP
"http://017700000001", # 八进制 IP
"http://127.0.0.1.nip.io", # DNS 解析到 127.0.0.1
"http://[::1]", # IPv6 回环
"http://0:0:0:0:0:ffff:127.0.0.1", # IPv6 映射
"http://127.1", # 简写 IP
"http://①②⑦.0.0.①", # Unicode 数字
]
# 防御:解析后统一转换为标准 IP 格式再检查
def normalize_and_check(hostname: str) -> bool:
try:
resolved = socket.gethostbyname(hostname)
ip = ipaddress.ip_address(resolved)
return not (ip.is_private or ip.is_loopback or ip.is_link_local)
except Exception:
return False防御体系
最佳实践
- 使用 URL 白名单而非黑名单,只允许必要的域名和 IP 范围
- 解析 DNS 后验证 IP,不要仅验证域名(DNS 重绑定绕过)
- 直接使用解析后的 IP 发起请求,避免二次 DNS 查询
- 启用 IMDSv2(AWS),强制要求 token 访问元数据
- 网络层隔离:防火墙规则限制应用进程的出站连接
- 禁止非必要的 URL scheme(如
file://、gopher://、dict://) - 设置请求超时和重定向限制,防止探测和延迟攻击
- 最小权限 IAM 角色,即使元数据被获取,也限制凭证的权限范围
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于