ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSRF漏洞攻防实战:从内网探测到端口扫描与防御加固

SSRF漏洞攻防实战:从内网探测到端口扫描与防御加固 还是那次被授权的渗透测试让我彻底记住了SSRF服务端请求伪造这个名字。当时目标系统里藏着一个看起来很普通的“远程图片抓取”功能前端传一个图片链接后端抓下来展示。结果就是这样一个不起眼的功能让我一路从公网摸进了内网最后碰到了一台没设密码的Redis服务器差一点就直接打到内网核心网段。整个过程没有0day没有复杂绕过完全靠的就是服务端“不加限制地帮用户发起请求”这一个缺陷。这篇文章我想把SSRF的攻防细节彻底拆开聊透漏洞到底怎么产生的、攻击者用它探测内网时脑子里在想什么、怎么通过SSRF做端口扫描来定位内网服务以及防御方该怎么一层层把口子堵死。如果你做Web开发、搞安全测试或者正在帮公司做SDL安全评审花十分钟看完这篇你脑子里应该能建立一张完整的SSRF攻防地图。1. SSRF漏洞的本质与成因分析1.1 先搞清楚SSRF到底是什么SSRF全称Server-Side Request Forgery服务端请求伪造。说人话就是服务器替你去请求了一个资源但这个资源地址是你说了算的。很多业务为了完成某个功能会让后端去主动访问一个URL。举个例子你在一个笔记软件里粘贴一个图片链接后端不是直接把链接返回给浏览器而是自己先把图片下载下来存到自己的服务器上再展示给你。这个“后端自己下载”的过程就是一次由服务端发起的请求。问题来了这个请求目标地址如果完全由用户控制而后端又没有做校验那攻击者就可以让服务器去请求任意地址。什么地址呢可以是外网的钓鱼站点也可以是内网地址比如http://10.0.0.8/、http://192.168.1.1/admin甚至是file:///etc/passwd这种本地文件协议。你想想服务器自己访问内网是不是相当于一个持有内网通行证的人正常的用户请求只能走大门但SSRF给了攻击者一个“让保安替你去敲门”的机会。真正的杀伤力就在这里——服务器本身就是内网的一台主机它能看到、能访问到内网里很多普通用户根本够不着的东西。1.2 漏洞产生的三类典型场景我平时做代码审计最怕的就是下面这几种功能。它们天然存在“后端主动请求外部URL”的需求也是SSRF的高发区。场景一远程资源获取功能这是最常见的入口。典型功能有头像远程加载、图片抓取、PDF生成时加载远程图片、网页截图服务、RSS订阅导入。这类功能必然要接收一个URL参数比如?urlhttp://example.com/logo.png然后后端用HTTP客户端去请求。// 典型的危险代码示例 ?php $url $_GET[url]; $content file_get_contents($url); // 用户直接控制请求地址 echo $content; ?场景二Webhook与回调通知不少平台允许用户填一个回调地址说“订单状态变化了通知我”。这个回调地址若不校验攻击者就能填内网地址。想象一下如果平台把一个内网管理接口的URL配成了回调地址每次订单触发就等于替攻击者访问一次内网接口。场景三代理与导入导出类API一些业务为了聚合内容提供了“URL翻译”“链接解析”“OAuth授权回调”等接口。这些接口的特点是后端拿到URL后会去请求并处理返回结果。其中OAuth的回调地址校验不严也常被用来打内网。把这些场景汇总一下共同特征很明显业务需要“由服务端主动发起请求”而请求目标没有严格的边界限制。所以判断一个功能有没有SSRF风险就看两点第一用户能不能控制目标URL第二后端发起请求前有没有验证目标地址是否允许访问。1.3 为什么说SSRF的杀伤力被低估很多开发同学觉得SSRF不就是让服务器访问一下内网IP嘛又不能直接拿shell有什么好怕的。这个想法很危险。SSRF的真正价值在于它是“内网渗透的跳板”。首先大多数内网服务对“来自内网IP的请求”是默认信任的。很多内网系统压根没有认证或者口令很弱。你从公网直接访问内网网段网络层就过不去但服务端发起请求时源IP就是内网IP内网系统的访问控制直接被绕过了。其次SSRF可以读取云环境的元数据服务。主流云厂商都会给虚拟机提供一个内部元数据地址通常是http://169.254.169.254/。云盘、临时密钥、实例ID不少东西都在这个地址上。如果SSRF没有任何协议限制攻击者构造http://169.254.169.254/latest/meta-data/就能把这些信息捞出来。拿到临时密钥就能以这台云服务器的身份调用云API这个危害级别直接拉满。再有SSRF配合gopher://、file://等伪协议还能攻击内网的Redis、MySQL这类非HTTP服务甚至构造数据包直接写入SSH公钥或者计划任务实现远程命令执行。所以把SSRF定义成一个中危漏洞严重低估了它的实际杀伤力。在我心里SSRF算得上高危漏洞的第一梯队。2. 内网探测攻击者视角的侦察思路理解攻击者怎么打防御者才知道该护哪里。下面这段讲的是“攻击者思路分析”目的是帮你在做检测和防护时知道该监控哪些异常行为。2.1 探测内网信息的基本逻辑当攻击者确认一个SSRF点可以控制后端请求地址后接下来要做的第一件事不是急着打Redis而是先“踩点”搞清楚这台服务器在哪个内网网段里内网里有哪些机器活着这些机器上跑了什么服务。探测的基本逻辑很简单把SSRF参数里的URL目标替换成内网地址然后观察响应差异。请求http://10.0.0.1/如果页面返回了正常内容或者特定报错说明这个IP是存在的请求http://10.0.0.8/如果请求超时或者连接被拒绝说明这个IP可能不存在或者被防火墙拦了请求http://192.168.1.1/如果返回了某种后台登录页的标题不仅说明IP存活还能直接确认对面是个Web管理后台。你可以把SSRF当成一个小型“手工扫描器”。正常情况下你从公网是打不到内网的但SSRF让你借了服务端的力这个力就是最值钱的东西。2.2 常见的内网IP识别与存活判断方法内网IP段一般集中在这三个范围里网段主要用途10.0.0.0/8大型企业内网用的最多172.16.0.0/12中大型企业内网AWS等云环境常见192.168.0.0/16中小型办公网、家用路由、实验室攻击者拿到一个SSRF点后通常会先用这些网段去试。比如原请求是GET /fetch?urlhttp://example.com/avatar.png改成GET /fetch?urlhttp://10.0.0.1/ GET /fetch?urlhttp://10.0.0.2/ GET /fetch?urlhttp://172.16.0.1/ GET /fetch?urlhttp://192.168.1.1/这时关键来了怎么判断这个IP到底存活不存活我总结了一套实际测试中最常用的判断依据第一个依据是响应内容。如果对方返回了HTML页面、JSON数据、图片或者包含特定关键词的错误信息基本可以断定IP存活且有服务监听。第二个依据是响应时间。连接一个不存在的IP通常会迅速返回“Connection refused”或等待超时连接一个存活的IP但端口未开放也会快速拒绝但如果IP存在且端口有服务往往会有一定延迟因为服务需要处理请求。用超时时间差来猜端口状态是SSRF探测的经典技巧。第三个依据是错误信息差异。同一个后端框架在“连接失败”和“连接成功但资源不存在”时的报错往往不同。比如有的后端会返回“URL connection error”有的会返回“404 Not Found”。这些细节都能透露出目标端口开放情况。2.3 从响应差异看结果判断这里多说一句内网和公网最大的区别在于网络环境更简单、更可控。所以攻击者在利用SSRF探测时通常会做一次“基准测试”先请求一个已知不存在的IP记录返回时间再请求一个已知存在的公网IP记录返回时间。把这两个基线时间记住之后探测内网IP时拿响应时间跟基线比对。比如基线是不存在IP耗时2000ms超时存在IP耗时200ms。那么当你探测http://10.0.0.5/耗时180ms时基本可以判断这个IP是活的。这个方法看起来笨但在真实测试中非常稳。防御方需要记住的是如果你们系统里有SSRF类功能日志里突然出现大量对内网网段、不同IP、不同端口的请求而且请求源IP集中在同一台服务器这大概率是有人在用SSRF做内网扫描。检测规则可以直接记录并告警这种特征。3. 端口扫描利用从探测到精准定位3.1 为什么攻击者要扫端口确认一个内网IP存活后下一步就是扫端口。端口扫出来服务就出来了服务出来漏洞面就出来了。举个例子你探测到内网有台10.0.0.8的机器但它可能同时开着80Web服务、3306MySQL、6379Redis、22SSH。哪个服务最有价值这取决于接下来想干什么。如果不小心把内网Redis暴露给了攻击者而且没设密码那攻击者可以尝试通过SSRF构造Redis数据包最终写入文件实现命令执行。如果是3306的MySQL弱口令那整个库都可能被拖走。端口常见服务攻击者关注的潜在利用点22SSH弱口令、密钥泄露80/8080Web管理后台后台弱口令、Web漏洞3306MySQL弱口令、SQL注入6379Redis未授权访问、写入计划任务或SSH公钥9200Elasticsearch未授权访问、数据泄露11211Memcached未授权访问27017MongoDB未授权访问8088/8081各类API服务业务逻辑漏洞、越权所以从攻击者的视角看端口扫描的目的不是“把所有端口都跑一遍”而是“找到最容易打进来的那扇门”。3.2 基于SSRF的端口扫描原理与技巧用SSRF做端口扫描原理其实比传统扫描更简单在目标URL后面拼上端口观察响应差异。比如探测10.0.0.8这台机器的6379端口和80端口请求1: /fetch?urlhttp://10.0.0.8:6379/ 请求2: /fetch?urlhttp://10.0.0.8:80/如果6379端口开着Redis不会返回标准的HTTP响应但SSRF请求仍然能建立起TCP连接后端可能会返回“超时”“连接成功但协议不是HTTP”之类的提示。而80端口开着的话返回的就是标准的HTTP页面内容。这里有个非常实用的技巧用公开的端口清单缩小范围。攻击者不会对65535个端口全量扫描那样太慢且容易触发告警。他们通常会先跑这份常用端口Top10022, 21, 23, 25, 53, 80, 110, 135, 139, 143, 443, 445, 993, 995, 1433, 1521, 3306, 3389, 5432, 5900, 6379, 7001, 8000, 8080, 8088, 8888, 9000, 9090, 9200, 10000, 11211, 27017等。为什么选这些端口因为它们是数据库、缓存、远程桌面、Web容器等最常用的服务端口打进去的收益最大。第二个技巧是分析响应时间阈值。开放端口响应时间较短关闭端口瞬间拒绝防火墙丢弃则超时较长。攻击者可以设置一个时间阈值比如超过1500ms视为“无法连接”低于800ms视为“端口开放”从而快速扫描一个C段内的多个IP。第三个技巧是利用协议伪协议扩展攻击面。如果SSRF没有限制协议攻击者可以把http://换成file://来读取服务器本地文件换成gopher://来构造任意TCP数据包攻击内网的Redis、MySQL等非HTTP服务。这也是我在实际测试中最担心的场景因为一旦能打Redis离命令执行就不远了。3.3 关键端口与服务指纹速查光扫出端口还不够攻击者还要确认“这个端口是不是我要找的那个服务”。这就涉及服务指纹识别。如果目标HTTP服务给出的是Server: nginx/1.18.0说明是Nginx如果Redis端口返回的是-ERR wrong number of arguments说明Redis存在。对防御方来说了解这些指纹特征同样重要因为你需要知道自己的服务暴露了什么特征才能有针对性地做隐藏和加固。我在实际项目中总结了一个极简判断法如果目标端口返回的是一段HTML基本是Web服务直接看title和Server头如果返回的是纯文本报错比如ERR、Connection refused多半是数据库或中间件如果返回的是二进制数据大概率是Redis、Memcached等自研协议的缓存服务。扫描到这一步攻击者已经完成了“信息收集阶段”接下来会针对具体服务发起爆破或者漏洞利用。而防御方的任务就是在攻击者扫到这一步时让他的每一步都变得困难且被记录。4. 漏洞复现与检测的实操记录4.1 为什么我强烈建议你搭一个靶场纸上谈兵没意思SSRF这东西最好自己亲手测一遍。但我要强调所有测试必须放在自己搭的靶场环境里或者在有明确授权的测试目标上做。我平时最常用的方式是本地搭建pikachu漏洞靶场或者基于CTFHub的SSRF靶题来练习既可以观察完整的攻击流量又不必担心法律和道德风险。如果你不想装重型环境Python的http.server模块就能起一个简单的测试点# 在本地起一个模拟目标服务监听8001端口 python3 -m http.server 8001 # 在另一个终端起一个模拟SSRF漏洞点的服务 # 代码见下面的示例靶场的目的不是教你打人而是教你“识别”。当你亲眼看到一次SSRF请求从发出到响应你会对日志特征、响应差异、检测绕过这些概念有非常直观的理解。以后再碰到真实代码评审你一眼就能嗅出同样的味道。4.2 完整复现流程与响应分析下面我用一段简化代码来模拟一个存在SSRF漏洞的文件下载功能然后演示完整的请求链路。# ssrf_lab.py from flask import Flask, request import requests app Flask(__name__) app.route(/fetch) def fetch(): url request.args.get(url) # 漏洞点没有校验目标地址直接请求 resp requests.get(url, timeout5) return resp.content if __name__ __main__: app.run(host0.0.0.0, port9000)启动后正常请求是curl http://127.0.0.1:9000/fetch?urlhttp://example.com/这时后端会返回example.com的首页内容。看起来一切正常但如果我们把参数值改成内网地址curl http://127.0.0.1:9000/fetch?urlhttp://192.168.1.10/ curl http://127.0.0.1:9000/fetch?urlhttp://10.0.0.1/后端一旦能返回内网IP对应的响应或者明显超时就说明SSRF存在。更关键的是测试file://协议能否读取本地文件curl http://127.0.0.1:9000/fetch?urlfile:///etc/passwd如果后端返回了root:x:0:0:root:/root:/bin/bash这类内容说明这个SSRF不仅存在而且没有做协议限制可以直接读文件。这种情况下漏洞级别要往高危甚至严重调。我在靶场上复现时习惯抓三份数据请求包、响应包、后端日志。请求包用来观察攻击者提交了哪些参数响应包用来判断是否存在内容回显后端日志用来确认SSRF请求的真实目的地。这三份数据对比着看很多隐蔽的SSRF点都能暴露出来。4.3 自动化检测脚本思路手工测SSRF效率太低我一般会写一个简单的脚本做初步筛查。核心逻辑是拿到一个疑似接收URL的参数自动请求外部可控地址通过返回内容判断是否存在回显。# ssrf_check.py import requests TARGET http://127.0.0.1:9000/fetch params [url, target, uri, path, dest, redirect, location] # 测试内网存活情况 payloads [ http://127.0.0.1:9000/, http://10.0.0.1/, http://192.168.1.1/, file:///etc/passwd, http://169.254.169.254/latest/meta-data/, ] for p in payloads: try: r requests.get(TARGET, params{url: p}, timeout5) print(f[*] {p} - status: {r.status_code}, len: {len(r.text)}) print(r.text[:200]) print(- * 50) except Exception as e: print(f[!] {p} - error: {e})再次强调这个脚本只能在你的靶场或者授权目标上使用。安全测试的第一原则就是授权没有授权的一切测试行为都有可能给自己惹上麻烦。脚本跑完你可以根据响应长度和内容快速判断出哪些地址可达、哪些协议可用、哪些内网网段存在存活主机。这就是你之后做修复验证的基线数据。5. 修复方案与防御加固的完整落地5.1 最核心的修复手段白名单与协议限制讲完攻击重点讲防御。SSRF的修复我的首要建议永远是白名单机制。不要试图用黑名单去拦黑名单永远拦不干净。你封了127.0.0.1攻击者可以用127.0.0.2、0.0.0.0、localhost、十六进制IP甚至DNS解析指向内网的域名来绕过。正确的做法是在业务层面明确“这个功能允许访问哪些地址”然后只放行这些地址。import requests from urllib.parse import urlparse ALLOWED_HOSTS [img.example.com, static.example.com] def safe_fetch(url): host urlparse(url).hostname if host not in ALLOWED_HOSTS: raise ValueError(url not allowed) # 只允许 http/https if not url.startswith((http://, https://)): raise ValueError(protocol not allowed) resp requests.get(url, timeout5) return resp.content如果业务确实需要允许用户输入任意URL那就必须走更严格的安全评审流程并且加上请求方的身份认证、频率限制和内容审计。绝大多数情况下真正的业务需求都能被白名单覆盖。5.2 DNS解析层面的防护细节白名单校验做得再好也不能忽略一个经典绕过手法DNS重绑定。什么是DNS重绑定攻击者先注册一个域名这个域名第一次解析到公网IP比如他自己的服务器通过校验。但紧接着他把解析记录改到内网IP比如192.168.0.1。后端如果只做一次“域名-IP”解析校验就会出现一个时间差窗口校验时是公网IP实际请求时已经变成了内网IP。这个窗口可以用来绕过白名单。应对方案是“二次校验”拿到URL后先解析一次IP确认是白名单内的然后在真正发起连接时再解析一次IP确保两次解析结果一致。更稳妥的做法是直接用IP地址发起请求不让系统再一次解析域名。import socket import requests def fetch_with_ip_pinning(url): host urlparse(url).hostname ip socket.gethostbyname(host) # 校验解析出的IP是否在白名单网段内例如只允许 203.0.113.0/24 if not is_ip_in_whitelist(ip): raise ValueError(ip not allowed) # 直接用IP拼接请求防止第二次DNS解析 final_url url.replace(host, ip) resp requests.get(final_url, timeout5, headers{Host: host}) return resp.content这个技巧叫“IP Pinning”能很大程度上规避DNS重绑定攻击。我在给团队做代码评审时这一条是必查项。5.3 深度防御的补充措施白名单是第一道门但光有白名单还不够。SSRF防御需要纵深每一层都做好攻击者才会彻底放弃。网络层控制在防火墙和云安全组上限制服务器出网方向只放行必要的目标地址和端口。比如一台Web服务器它完全不需要主动访问内网数据库端口那出方向到3306、6379这些端口就应该默认阻断。即使应用层白名单被绕过网络层还能兜底。服务层加固内网服务不能裸奔。Redis必须设密码MySQL不能用弱口令Elasticsearch要加认证。很多内网服务觉得“反正外面访问不到”就不设防结果SSRF恰好成了那个“外到内”的通道。我在授权测试里见过太多内网Redis未授权、MongoDB未授权的案例大部分都是因为这种侥幸心理。检测层监控在应用日志里记录请求的完整URL、目标IP、目标端口和来源IP。配置告警规则当单个会话短时间内出现大量不同的内网IP请求时触发实时告警。在我维护的告警规则里下面几条命中率特别高告警特征说明请求目标为内网网段且URL参数包含IP:端口疑似SSRF端口扫描请求协议不是http/https或参数里含file://、gopher://疑似SSRF协议绕过短时间内同一来源请求超过20个不同目标IP疑似内网探测请求地址包含云元数据地址特征疑似元数据攻击需立即处置另外如果你们用的是云主机建议在云安全策略里禁止实例通过HTTP头等绕过方式访问元数据服务并给元数据服务加上IAM角色约束最小化临时密钥的权限范围。6. 从实战角度总结SSRF防护优先级6.1 修复优先级排序如果你们团队现在有一堆存量代码不可能一口气把所有SSRF点修完。我建议按下面的优先级排工性价比最高优先级修复措施成本效果P0所有接收URL参数的功能全部加白名单和协议限制中直接解决90%的SSRF问题P0禁止file://、gopher://等非HTTP协议低阻断文件读取和Redis类攻击P1禁用请求自动跳转或跳转后重新做地址校验低堵住跳转绕过P1对域名解析做二次校验落地IP Pinning中防DNS重绑定P2网络层防火墙和云安全组收缩出向规则低纵深防御兜底P2内网服务全部开启认证并禁用默认口令中即使被打到内网也难以上手6.2 最后分享一点个人体会我从第一次被SSRF打穿到现在已经过去好几年了。这些年里我最大的感触是SSRF不是一个“单点漏洞”它更像是一把钥匙能不能打开大门取决于内网里的服务有多脆弱。所以做安全不能只盯着Web层内网服务的基线检查、口令策略、网络边界管控同样重要。如果你是开发者修SSRF时不要自我安慰说“我们内网很安全”。在攻防演练里真正让人汗毛竖起来的往往是那些你以为“绝对碰不到”的小功能。如果你是安全从业者建议把SSRF这部分知识吃透因为它既是Web安全的基础功又是未来做云安全、内网渗透时必须掌握的底层能力。希望这篇关于服务端请求伪造的拆解能帮你在攻与防两个方向上建立更清晰的认识。下次代码评审记得多看一眼那些会主动请求外部URL的地方。
返回列表