1. 从一次“内网探测”引发的思考:SSRF漏洞的本质
几年前,我在一次内部安全演练中,遇到了一个非常典型的场景。一个看似无害的“头像上传”功能,允许用户通过URL地址设置头像。开发者的初衷是好的,希望用户能方便地引用其他网站的头像,省去下载再上传的麻烦。然而,就是这个功能,让我在几分钟内,从外网直接访问到了部署在内网测试环境的数据库管理后台。这个漏洞,就是今天我们要深入拆解的SSRF。
SSRF,全称 Server-Side Request Forgery,中文叫服务端请求伪造。这个名字听起来有点唬人,但它的核心逻辑异常简单:攻击者能够诱使服务器(后端应用)代替自己,向一个指定的、通常是服务器自身或内网中的地址发起HTTP请求。你可以把它想象成你控制了公司的前台电话(服务器),然后你让前台帮你拨打一个内部的分机号(内网服务),并把通话内容(响应)转述给你。前台(服务器)拥有访问内网的权限,而作为外部访客的你(攻击者)原本是没有的。
为什么SSRF在CTF和实战中如此重要且危险?原因有三点。第一,权限绕过:它利用了服务器通常拥有更高网络权限(如访问内网、本地环回地址127.0.0.1)的特性,实现了攻击者自身无法直接做到的访问。第二,攻击面广:一旦成功,它能攻击的目标不仅仅是Web应用本身,而是服务器能访问到的整个网络平面,包括数据库、缓存服务、管理接口、甚至云元数据服务。第三,危害升级:SSRF常常是通往更大漏洞的跳板。通过它,我们可以探测内网结构、攻击未授权访问的内网应用,或者与服务器本地的其他服务(如Redis、Memcached)进行交互,进而可能实现远程代码执行。
在CTFshow的Web入门系列中,SSRF题目设计得非常精妙,几乎涵盖了从基础到进阶的所有考点。它们不仅仅是让你“打出一个flag”,更是引导你理解漏洞的成因、利用的链条以及防御的思路。接下来,我们就以CTFshow的题目为线索,一层层剥开SSRF的外壳。
2. 漏洞的源头:那些“信任”用户输入的函数
SSRF漏洞的产生,几乎无一例外,都源于应用程序过度信任了用户可控的输入,并将其直接用于发起网络请求。在PHP、Python、Java等主流Web开发语言中,都有一些常见的“高危函数”。
2.1 PHP中的典型危险函数
在CTFshow的PHP题目里,以下几个函数是常客:
file_get_contents(): 这个函数本意是读取文件内容,但它也支持http://、ftp://等URL协议。如果开发者直接将$_GET[‘url’]这样的参数传递给它,SSRF就产生了。// 漏洞代码示例 $url = $_GET['file']; $content = file_get_contents($url); echo $content;攻击者可以传入
file=http://127.0.0.1:8080/admin,服务器就会去读取本机8080端口的管理页面。curl_exec(): cURL库功能强大,支持众多协议和复杂配置。也正是因为其强大,如果URL或配置项由用户完全控制,风险极高。$ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $_GET['url']); // 危险! curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); $output = curl_exec($ch); curl_close($ch);fsockopen()/fopen(): 这些更底层的函数也能用于发起网络连接,同样存在风险。
2.2 漏洞产生的核心场景
理解了危险函数,我们再来看看它们通常出现在哪些业务功能里,这能帮助我们在代码审计或黑盒测试时快速定位:
- 远程资源加载:就像开头的例子,头像、图片、文档的远程URL上传或预览功能。
- 数据采集/爬虫:一些网站提供“网页快照”、“URL转码”或“获取网页信息”的服务,后端需要去抓取用户提供的URL。
- Webhook或回调通知:应用需要根据用户配置的URL发起回调请求。
- 文件导入/导出:支持从互联网URL导入数据到应用内。
- 内网服务集成:某些功能可能需要请求同一个服务器上的其他内部API(比如生成PDF的服务),如果这个内部API地址是前端传递或可被篡改的,就存在问题。
注意:并非所有使用这些函数的地方都有SSRF。关键在于传入函数的参数(主要是URL)是否完全或部分由用户控制,且服务端没有对其进行严格的校验和过滤。一个固定的、写死在代码里的内网API调用,通常不是SSRF。
3. 利用技巧进阶:协议、绕过与信息探测
直接请求http://127.0.0.1/flag.php就能拿分的时代已经过去了。现代的CTF和实战中,处处是过滤和限制。下面我们梳理一下常见的利用技巧,这些技巧在ctfshow的题目中会反复出现。
3.1 利用各种URL协议
URL不只是http和https。许多编程语言的网络库支持多种协议,这为SSRF利用提供了更多可能性:
file://: 用于读取服务器本地文件。例如file:///etc/passwd可以读取系统用户列表。这是最基本的本地文件读取(LFI)利用。dict://: 字典协议,可以向服务(如Redis)发送简单的命令。虽然功能有限,但在探测端口和简单交互时有用。例如dict://127.0.0.1:6379/info可能泄露Redis信息。gopher://:这是一个“神器”。Gopher协议非常原始,但它允许我们构造几乎任意的TCP数据包。通过Gopher,我们可以与Redis、MySQL、FastCGI等许多文本协议的服务进行交互,从而可能实现更深入的攻击,比如向Redis写入Webshell。构造Gopher数据包需要一定的技巧,通常会借助脚本生成。ftp://: FTP协议,有时可用于进行端口扫描(根据连接成功或失败的时间差异判断端口状态)。
3.2 绕过常见的过滤与限制
题目不会让你轻易成功,常见的防御(或过滤)手段包括:
- 黑名单过滤:禁止
127.0.0.1、localhost、192.168.、10.、172.16.等内网IP或域名。- 绕过方法:
- IP地址进制转换:
127.0.0.1可以转为十进制2130706433,或者八进制0177.0.0.1,或者十六进制0x7f.0.0.1(取决于解析库)。http://2130706433等价于http://127.0.0.1。 - 域名指向:
localhost可以用127.0.0.1.nip.io(一个解析任意前缀到IP的公共服务)或localtest.me(指向127.0.0.1)来绕过。 - IPv6地址:使用
[::1]代表本地IPv6地址。 - URL编码:对点号、斜杠等进行二次编码,如
127%2E0%2E0%2E1。 - 利用重定向:如果服务器会跟随302跳转,可以先请求一个自己控制的、会返回302跳转到内网地址的页面。
- IP地址进制转换:
- 绕过方法:
- 白名单过滤:只允许访问特定域名,如
*.ctfshow.com。- 绕过方法:
- 利用
@符号:URL格式中,@之前是认证信息。http://expected.com@malicious.com,有些旧的解析库会连接到malicious.com,但校验域名时可能只看@前面的expected.com。不过现代库大多修正了此问题。 - 利用子域名解析漏洞:如果白名单是
*.ctfshow.com,可以尝试注册hack.ctfshow.com?这通常不现实。但可以尝试ctfshow.com.hacker.com,如果校验正则不严谨(如.*ctfshow.com$),可能会被放过。 - 结合重定向:同上,让白名单域名下的页面跳转到内网地址。
- 利用
- 绕过方法:
- 限制请求的端口:例如只允许80、443端口。
- 绕过方法:
- 利用非HTTP默认端口:很多内网Web服务可能跑在8080、8000、3000等端口。
- 利用非Web服务:如果后端用的是
file_get_contents(),它不关心端口,只关心协议。可以尝试用file://协议读取文件,这不需要端口。
- 绕过方法:
3.3 内网信息探测与端口扫描
SSRF的一个前置利用就是信息收集。我们如何通过一个SSRF点,摸清服务器所在内网的情况?
- 识别存活主机:通过遍历内网IP段(如
192.168.0.0/24)和常见端口,根据HTTP响应状态码、响应时间或错误信息差异来判断主机和端口是否开放。这通常需要编写脚本自动化进行。 - 识别服务指纹:对于开放的端口,尝试发送一些特定协议的探测包(如HTTP的GET /, Redis的
INFO命令),根据返回的Banner信息判断服务类型(Apache、Nginx、Redis、MySQL等)。 - 利用回显与报错:如果目标SSRF有回显(即服务器将请求的结果返回给页面),那么探测结果一目了然。如果是盲SSRF(没有回显),则需要利用时间延迟或DNS外带技术。
- 时间延迟:构造请求到一个不存在的IP或端口,连接会超时(耗时久);请求到一个开放端口但无响应的服务,可能快速返回连接拒绝。通过响应时间的显著差异来判断端口状态。这要求你对网络超时时间有较好的把握。
- DNS外带:这是盲SSRF探测的利器。如果目标服务器在请求你提供的URL时,会进行DNS解析,那么你可以使用一个你拥有日志记录的域名。例如,构造URL为
http://192-168-1-1.your-domain.com。服务器在请求时,会先解析192-168-1-1.your-domain.com这个域名,你可以在你的DNS服务器日志中看到这次解析记录,从而证明服务器尝试访问了192.168.1.1这个IP。Burp Suite的Collaborator功能就是为此而生。
4. 从SSRF到RCE:攻击链的构建
单纯的SSRF能读取文件、探测内网,危害已经不小。但攻击者的终极目标往往是远程代码执行。如何通过SSRF实现RCE?这需要将SSRF与其他漏洞或服务特性结合。
4.1 攻击Redis未授权访问
这是CTF中最经典的组合技。假设通过SSRF探测到内网172.18.0.2:6379运行着Redis,并且没有设置密码(默认配置)。
- 交互原理:Redis使用简单的文本协议,每条命令以
\r\n结尾。我们可以通过SSRF,直接向Redis发送符合其协议格式的TCP数据包。 - 利用Gopher协议攻击:Gopher协议可以承载我们精心构造的原始TCP数据。我们可以构造一个Gopher URL,其内容是一系列Redis命令。
- 写入Webshell:假设我们知道Web目录的绝对路径(如
/var/www/html),可以通过以下命令序列,将一句话PHP木马写入一个文件:
将上述命令按照Redis协议格式(每行以flushall set shell "<?php @eval($_POST['cmd']);?>" config set dir /var/www/html config set dbfilename shell.php save\r\n结尾,整体需要计算长度)进行编码,放入Gopher URL中。服务器通过SSRF请求这个Gopher URL,就等于向Redis发送了这些命令,从而在Web目录下生成shell.php。 - 利用CRLF注入攻击:如果SSRF点不支持Gopher协议,但支持HTTP且对输入过滤不严,可以尝试CRLF注入。在HTTP请求中插入
\r\n,可以在一个HTTP请求中“注入”额外的协议数据。例如,将Redis命令注入到HTTP请求头或体中,如果后端服务器与Redis在同一主机且存在某种奇怪的解析,可能导致命令执行。但这比Gopher直接攻击要困难得多。
实操心得:在实战或CTF中,用Gopher攻击Redis时,经常会因为空格、引号等字符被URL编码处理而导致命令失效。我的经验是,先在本地用Python脚本模拟Redis服务,打印出收到的原始字节,确保你构造的协议数据完全正确。也可以使用
redis-cli --pipe进行命令测试。
4.2 攻击FastCGI/PHP-FPM
如果通过SSRF发现内网开放了9000端口(PHP-FPM默认端口),并且服务器上运行着PHP,那么可能存在FastCGI RCE漏洞。
- 原理:PHP-FPM通过FastCGI协议与Web服务器通信。我们可以伪造一个FastCGI请求,通过设置
PHP_VALUE和PHP_ADMIN_VALUE环境变量,来修改PHP的配置,例如开启auto_prepend_file,让其自动包含一个我们可控的文件(可能是之前通过其他漏洞上传的),或者直接写入日志文件再包含。 - 利用方式:同样,我们可以使用Gopher协议来构造一个原始的FastCGI协议数据包,发送给
127.0.0.1:9000。这需要你对FastCGI协议格式有一定了解。网上有成熟的利用脚本(如gopherus工具),可以生成对应的攻击Payload。
4.3 攻击云元数据服务
在云服务器(AWS, Azure, GCP, 阿里云等)环境中,存在一个特殊的内部端点:元数据服务。例如,AWS的元数据地址是http://169.254.169.254。这个服务提供了关于当前云主机的信息,包括可能最敏感的临时访问凭证。
如果存在SSRF漏洞,攻击者就可以让服务器去请求这个元数据地址,从而窃取到云服务器的访问密钥。利用这些密钥,攻击者可以在云平台上进行横向移动、创建资源、窃取数据等,造成极其严重的后果。在CTFshow的一些题目中,可能会模拟这种场景,将flag放在元数据服务中。
4.4 结合其他文件包含漏洞
如果SSRF只能读取文件(如通过file://协议),而同时应用中存在本地文件包含漏洞,那么就可以形成一个攻击链:先用SSRF将恶意代码写入服务器本地的一个临时文件(或者利用已有的日志文件),再利用LFI漏洞去包含这个文件,最终达到代码执行的目的。
5. CTFshow SSRF题目实战精讲与踩坑记录
理论讲得再多,不如动手解一道题。我们以ctfshow web入门中一道经典的SSRF题目为例,来串联上面的知识点。请注意,这里不会给出具体flag,而是聚焦于解题思路和可能遇到的坑。
5.1 题目场景还原
假设题目提供一个功能:输入一个图片URL,后端会抓取这个图片并显示。代码逻辑简化如下:
<?php highlight_file(__FILE__); $url = $_GET['url']; if(preg_match('/^http:\/\/ctfshow\.com\//', $url)){ $content = file_get_contents($url); echo "<img src='data:image/png;base64,".base64_encode($content)."'/>"; }else{ echo "url must start with http://ctfshow.com/"; } ?>目标:读取服务器本地的/flag文件。
5.2 解题思路拆解
- 分析限制:白名单过滤,只允许
http://ctfshow.com/开头的URL。直接file:///flag或http://127.0.0.1会被拒绝。 - 寻找绕过方法:白名单校验通常检查URL的开头部分。我们可以尝试利用
@符号或者重定向。 - 尝试重定向绕过:
- 我们需要一个可控的、位于
http://ctfshow.com/域名下的页面,这个页面会返回一个302状态码,Location头指向我们真正想访问的内网地址,比如http://127.0.0.1/flag。 - 但题目通常不会给你一个
ctfshow.com的二级域名。这时需要思考:题目环境本身是否就在ctfshow.com域下?或者,是否存在一个可控的重定向参数?例如,题目可能有一个image.php文件,接受url参数并跳转:image.php?url=http://127.0.0.1/flag。那么我们的Payload可以是:http://ctfshow.com/image.php?url=file:///flag。这样,白名单校验通过,服务器会去请求ctfshow.com下的这个页面,该页面执行重定向到file:///flag,file_get_contents()在跟随重定向时,就会去读取本地文件。
- 我们需要一个可控的、位于
- 利用DNS重绑定(更高级的绕过):如果上述方法不行,可以考虑DNS重绑定攻击。我们需要控制一个域名(比如
hack.com),将其A记录先指向ctfshow.com的IP(通过白名单校验),然后在极短时间内(TTL很短)将其A记录改为127.0.0.1。服务器在第一次DNS解析时得到的是合法IP,通过校验并发起请求;但在实际建立TCP连接时,由于DNS缓存时间极短或实现问题,可能会重新解析域名得到127.0.0.1,从而访问到内网。这在CTF中实现较难,但属于一种经典的绕过思路。 - 尝试其他协议:题目校验的是
http://开头,但后面部分我们可控。能否在路径中嵌入其他协议呢?例如http://ctfshow.com/file:///flag?这通常不行,因为file://会被当作路径的一部分,而不是协议。但可以尝试利用#号(片段标识符)或?号(查询参数)来干扰解析,成功率取决于后端URL解析库的实现。
5.3 实战踩坑点
file_get_contents()与302跳转:默认情况下,file_get_contents()会跟随重定向。但它的context参数可以设置follow_location为0来禁用。如果题目禁用了跳转,重定向绕过法就会失效。- URL解析差异:PHP的
parse_url()函数和curl库、浏览器对URL的解析可能存在细微差异。例如,对于http://expected.com@malicious.com,parse_url()解析出的host可能是malicious.com,但一些简单的字符串匹配(如strpos($url, ‘ctfshow.com’) !== false)可能会被@前面的内容欺骗。需要仔细分析过滤代码的逻辑。 - IPv6与特殊地址:
[::1]、0.0.0.0、127.0.0.1.nip.io这些都是需要尝试的绕过点。 - 端口与协议:如果目标不是Web服务,而是Redis(6379),即使你能访问
127.0.0.1,用http协议也是不行的。这时就要考虑如何利用支持的协议(如dict,gopher)去与这些服务交互。
6. 防御之道:从开发层面根除SSRF
理解了攻击,才能更好地防御。作为开发者,应该如何避免引入SSRF漏洞?
原则:对用户输入做“白名单”校验,而非“黑名单”
- 最佳实践:如果业务确实需要从指定外部资源获取数据,那么应该维护一个允许的域名或IP白名单。只有完全匹配白名单的请求才被放行。
- 避免做法:试图过滤掉
127.、192.168.、localhost等关键词。绕过方法太多,防不胜防。
统一网络请求出口并实施过滤
- 在架构上,将所有需要出站的HTTP请求收敛到一个统一的网络中间件或服务中。在这个统一出口实施严格的安全策略:协议限制(只允许HTTP/HTTPS)、目标限制(禁止访问内网IP段、回环地址)、DNS解析限制(禁止解析到内网IP)等。
禁用不需要的URL协议
- 在应用程序或库的层面,明确禁用危险的协议,如
file://、gopher://、dict://、ftp://等。例如,在PHP的cURL中,可以使用CURLOPT_PROTOCOLS来限制允许的协议。
- 在应用程序或库的层面,明确禁用危险的协议,如
谨慎处理URL重定向
- 如果业务需要跟随重定向,必须对重定向的目标地址再次进行白名单校验,防止通过重定向跳转到内网。
为内网服务设置认证
- 即使存在SSRF,如果Redis、MySQL、管理后台等服务设置了强密码认证,攻击者也无法轻易利用。这是纵深防御的重要一环。
使用云服务商的安全组/防火墙
- 在云环境中,严格配置安全组规则,禁止Web服务器主动访问不必要的内网端口(如Redis的6379, Memcached的11211等)。从网络层面进行隔离。
对返回内容进行安全检查
- 如果请求外部资源并返回给用户,应对返回的内容进行安全检查(如图片是否真的是图片,防止SVG图片携带恶意脚本),避免造成二次漏洞。
7. 工具与资源:让测试事半功倍
工欲善其事,必先利其器。在审计和测试SSRF时,以下工具能极大提升效率:
- Burp Suite Professional + Collaborator:
- Collaborator是盲SSRF测试的神器。它为你生成一个临时域名,所有指向该域名的DNS查询和HTTP/HTTPS请求都会被Burp记录并通知你。在测试任何可能触发后端请求的功能时(如Webhook、URL预览),把Collaborator域名放进去,然后观察是否有交互产生,这是证明SSRF存在最直接的方法。
- Intruder:用于自动化进行内网端口扫描或IP段爆破。可以配合时间差或DNS外带进行盲注。
- SSRFmap / Gopherus:
- SSRFmap:一个自动化的SSRF测试框架,内置了很多Payload和绕过技巧,可以自动探测内网服务并尝试利用。
- Gopherus:专门用于生成攻击各种服务(Redis, MySQL, FastCGI等)的Gopher协议Payload的工具,非常方便。
- DNSLog平台:类似Burp Collaborator的免费替代品,如
dnslog.cn、ceye.io,提供临时域名用于接收DNS查询记录,验证盲SSRF。 - 编程脚本:很多时候需要自己写Python脚本来构造特殊的Payload、进行自动化探测或处理复杂的协议交互。掌握
requests、socket库的基本使用非常必要。
我个人在测试时的流程通常是:先用手工尝试几个基本Payload(如http://127.0.0.1,file:///etc/passwd),然后用Burp Collaborator探测盲SSRF,如果发现存在且有一定自由度,再上SSRFmap进行自动化内网探测,最后针对发现的服务(如Redis),用Gopherus生成Payload进行深入利用。
SSRF是一个看似简单却内涵丰富的漏洞,它考验的不仅是漏洞利用的技巧,更是对网络协议、应用架构和防御思路的深入理解。通过CTFshow的题目进行练习,是一个非常好的入门和进阶途径。记住,关键不在于记住每一个Payload,而在于掌握“信任边界突破”这一核心思想,以及“输入不可信,输出需过滤”的安全开发基本原则。在实战中,保持好奇心,多问一句“这个URL参数,服务器到底会怎么处理?”,也许就能发现一个潜在的SSRF漏洞。