SSRF服务端请求伪造这个漏洞很多刚入门的朋友都会觉得它神神秘秘名字里带着“伪造”两个字听起来就像一种特别高深的攻击手段。但等你真正把它拆开看原理其实非常直白攻击者让服务器替自己发请求。就这么一句话说完了。我见过太多新手在SSRF上卡住不是因为知识点本身有多难而是因为没搞清楚“这个漏洞从哪里来又要往哪里打”。这篇保姆级教程我会从零开始把SSRF的原理、产生场景、利用思路、防御方案全部拆开揉碎然后带着你在CTFHub靶场上把各个关卡一层层打穿。整个过程中你不需要有多深厚的代码功底只要你用过浏览器、知道HTTP请求大概长什么样就能跟得上。如果你是开发者这篇文章也会帮你理解为什么“用户传一个URL进来服务端就去请求它”这种功能会变成漏洞的重灾区。老规矩先交代一下安全边界所有演示和解析都限定在CTFHub靶场、本地搭建环境这类授权范围内。你拿去打别人的业务系统那是另外一回事而且后果自负。做安全这一行授权意识越早建立越好。1. SSRF到底是什么先理解这张“内网地图”1.1 一句话版SSRF服务端被迫当“跳板”不扯官方定义了我用一个场景给你说明白。某公司有一个“图片处理”功能你在页面上填一个图片URL后端服务器会主动去下载这张图片裁剪压缩后再返回给你。这个功能本身完全正常但它有一个隐含问题后端服务器能访问你填写的那个URL地址。如果你是攻击者你填的URL不是一张图片而是http://127.0.0.1:8080/admin后端服务器就会替你去访问这台服务器自己本地的8080端口然后把响应内容原样返回给你。如果那个后端管理后台正好在这个端口上运行你就能通过服务器做跳板读到本应只允许本地访问的管理页面。这个过程就是SSRFServer-Side Request Forgery服务端请求伪造。攻击者并没有直接接触内网资源而是诱导服务器去访问了这些资源再把结果传回来。服务器从头到尾都蒙在鼓里它以为自己处理的是一个正常请求。我打个比方你让前台小姐姐帮忙去前台货架上取一份文件但她没有核对你指定的货架位置是不是你该访问的而是你让她去哪儿她就去哪儿拿到东西还会拿回来给你看。SSRF就是这样一种“越权取件”的过程而那个前台小姐姐就是服务器。1.2 哪些业务场景容易出现SSRFSSRF不是凭空冒出来的它和业务功能强绑定。只要一个功能需要“让服务端去访问某个URL”就有可能被掏空成为SSRF入口。我列几个真实业务中特别常见的类型图片处理/缩略图生成模块用户提交图片URL服务端拉取后处理。Webhook通知/回调接口用户配置一个接收回调的URL服务端后端去请求这个URL。PDF导出/截图服务把网页URL转成PDF或截图服务端会去访问传入的URL。URL短链接/跳转服务服务端判断目标地址内容后再返回。远程文件导入从URL导入CSV、模板、素材等。SSO单点登录配置允许配置远程元数据URL。这些功能不可避免地要接收用户可控的URL。如果后端在发起请求前不做严格校验或者只做了很容易绕过的校验SSRF就出现了。1.3 为什么SSRF危害很大甚至能捅穿云环境很多初学者会觉得SSRF只能“读点内网页面”格局小了。SSRF真正的杀伤力在于它把攻击面从“公网可见的一台服务器”扩大到了“这台服务器所在的内网环境”。探测内网存活主机和端口通过返回差异判断内网某IP的某端口是否开放。读取服务器本地文件如果协议没有被限制死可以用file://直接读/etc/passwd、配置文件、应用源码。访问内网Web服务内网的管理后台、监控面板、数据库Web管理页面这些系统往往没有很好的访问控制SSRF直接直达。攻击内网其他服务比如内网的Redis、MySQL、Memcached等通过构造请求发包可能触发未授权访问或命令执行。获取云平台临时凭证在云环境中请求http://169.254.169.254/各云平台通用的元数据地址可能拿到实例标识、临时访问密钥等敏感信息这是SSRF危害升级的一个重要方向。越往后的利用方式越复杂但核心始终没变服务器出网能访问到哪里攻击者就能跟着看到哪里。理解了这个你就知道为什么这个漏洞值得花一整篇博客来写了。2. 看懂触发点与控制面只有掌握原理进阶才不迷路2.1 代码层面的两种经典触发形式先看最典型的“直连型SSRF”代码长什么样。PHP版本大概是这样的?php $url $_GET[url]; // 用户可控 $data file_get_contents($url); // 服务端直接去请求 echo $data; // 返回响应内容 ?如果是Python Flask版本差不多是这样from flask import Flask, request import requests app Flask(__name__) app.route(/fetch) def fetch(): url request.args.get(url) # 用户可控 resp requests.get(url) # 服务端发请求 return resp.text # 返回内容这段代码起码有三个问题参数直接拼进请求里没有做协议白名单。没有校验目标IP是否属于内网/保留地址。把响应内容回显给用户导致攻击者能“看到”结果。实战里可能没这么裸奔但大致逻辑就是这样。还有一种情况是服务器代码里写了固定的目标但URL跳转会改变最终请求地址这种我们后面在靶场环节用302跳转来演示。2.2 协议层面能玩出什么花样服务端请求URL时如果支持多种协议攻击者就能选不同的协议做不同的事。这是SSRF利用的“控制面”。协议典型用途一句话描述http:///https://访问Web资源最常见探测端口、访问内网站点file://读取本地文件file:///etc/passwd直接读系统文件dict://探测端口、发送命令dict://host:port/info常用于探Redis等服务gopher://构造任意TCP数据包可以“隔空”发送完全自定义的协议内容gopher协议是SSRF利用里面最重量级的因为它可以发送任意TCP数据几乎等于允许你直接和一个内网服务的TCP端口对话。学习成本也确实高一些要自己构造协议数据格式比如你要打Redis就得理解Redis协议中*、$、\r\n这些符号的含义。靶场后期如果涉及也需要静下心来调包。2.3 防御方一般会怎么卡反过来看如果你是防御者通常会从五个方向下手协议白名单只允许HTTP/HTTPS禁止file、dict、gopher等协议。IP黑名单/白名单过滤内网地址、保留地址、回环地址或者只允许访问某些外网域名。DNS解析校验对域名先解析成IP校验IP合法性后再发起请求并防止解析结果变化DNS重绑定。响应限制不让用户无限制读取返回内容比如只返回图片而不是返回完整HTML。出网访问控制在防火墙层限制服务器只能访问必要的网络区域这是兜底方案。所以你在做SSRF利用时面对的往往是一整套过滤逻辑而不是一个简单的请求。后面的靶场实战本质上都是围绕“如何绕过这套拦截逻辑”来展开的。3. 保姆级CTFHub靶场实战从第一关到最后一关3.1 开战前的准备工作去CTFHub平台开启SSRF相关靶场并启动在线环境。这类平台通常会给一个动态的临时访问地址用浏览器打开即可看到题目页面。工具方面不用贪多建议准备这几样浏览器开发者工具F12打开用来改请求方法、查看请求参数和响应头。Python3环境写小脚本批量验证比手点浏览器快得多。Burp Suite想深入观察请求的全过程可以用它抓包改包零基础最开始不强制要求装。一台公网可访问的临时测试服务器用来放302跳转文件后续会用到。我自己做这类靶场时更喜欢直接用Python脚本去验证因为能看到完整的请求参数和原始返回不容易被浏览器特性干扰。3.2 第一梯队请求方式与URL编码绕过第一关请求方式进入题目后页面上一般会有一个表单传入url参数目标地址就是题目里提示的内网地址比如http://110.110.110.110/flag.php这是靶场专用的虚构目标地址不是什么真实IP。先看页面提示。通常这一关就是让你理解同样的功能点GET和POST两种请求方式都要能触发。直接填URL用GET请求发起可能得不到结果要改成POST方式提交或者需要带上特定的请求参数。实操思路在浏览器里打开靶场页面找到提交URL的地方。在开发者工具的“网络”面板里观察请求是GET还是POST。如果是GET先复制当前请求在控制台用Python发GET请求试试import requests url http://这道题目的地址/url?urlhttp://110.110.110.110/flag.php resp requests.get(url) print(resp.text)如果GET不出flag就改用POST提交把url作为POST参数带过去import requests url http://这道题目的地址/url data {url: http://110.110.110.110/flag.php} resp requests.post(url, datadata) print(resp.text)如果你在页面上看到“URL不能为空”之类的提示就要老老实实检查参数名是不是url以及提交方式是不是POST。提示这类题目考察的不是复杂技巧而是“三分链”中最基础的一环——请求要能成功发出去。别一上来就想着栈溢出先把请求原样送到对方手里。第二关URL编码绕过这一关开始引入“过滤”的概念。后端可能会检查URL里是否包含某些敏感字符或关键词比如看到110.110.110.110就拒绝。但它在后续解析时往往会先对参数做一次urldecode这就成了突破口。思路非常简单把目标地址做URL编码让过滤器第一遍检查时看不到“原形”。比如把http://110.110.110.110/flag.php整体URL编码变成http://%31%31%30%2e%31%31%30%2e%31%31%30%2e%31%31%30/flag.php程序如果先检查再解码检查时看到的是一堆%31%32%33自然放行。而后端真正发起请求前会先urldecode拿到真实内网地址。实操脚本import requests from urllib.parse import quote base http://这道题目的地址/url?url payload http://110.110.110.110/flag.php encoded quote(payload, safe) resp requests.get(base encoded) print(resp.text)不同关卡可能过滤的关键词不一样有些过滤的是flag.php有些过滤的是IP本身。你可以只编码被过滤的那一段也可以直接整体编码。试到出结果为止。3.3 第二梯队IP地址格式绕过如果后端直接对IP字符串做了黑名单过滤比如看到110.110.110.110就拒绝那就可以换个马甲。IP地址在计算机里本质上是一个32位整数只是人类习惯用点分十进制显示。下面几种写法都指向同一个地址十进制整数形式把每段按256进制合并。算式是110 * 256^3 110 * 256^2 110 * 256 110 1852730990所以访问http://1852730990/flag.php很多语言和库识别后还是去请求110.110.110.110。用Python计算ip 110.110.110.110 parts list(map(int, ip.split(.))) int_ip parts[0] 24 | parts[1] 16 | parts[2] 8 | parts[3] print(int_ip) # 输出 1852730990八进制写法每个十进制段转成八进制前面加0。110的八进制是156所以写成http://0156.0156.0156.0156/flag.php十六进制写法110的十六进制是0x6e所以写成http://0x6e.0x6e.0x6e.0x6e/flag.phpURL编码叠加把上面任意一种形式再做URL编码又能过一层过滤http://%31%38%35%32%37%33%30%39%39%30/flag.php到这一关你可以写一个循环脚本把几种形式挨个试一遍。很多过滤逻辑只防了最常见的点分格式对这些进制变化完全没有免疫力这也是安全测试中经常遇到的“过滤器覆盖面不足”问题。3.4 第三梯队302跳转、IPv6与端口限制302跳转绕过有些后端会先做一个“预检”就是把URL请求一次看返回地址或者状态码如果发现是内网地址就拒绝。但这种预检往往不会真的“跟随跳转到底”它只看了第一跳。于是思路来了准备一个公网地址它返回302跳转到内网地址。预检时服务器访问的是公网地址看起来完全正常等它真正发起业务请求时却跟着跳转到了内网。实操方法在你那台公网可访问的测试服务器上放一个PHP文件?php header(Location: http://110.110.110.110/flag.php); ?记住PHP的header(Location: ...)默认是302状态。然后把题目里的url参数填成这个公网文件地址http://你的测试服务器/redirect.php如果看到flag说明后端确实“先信任后跳转”没有对最终地址做二次校验。IPv6绕过如果过滤规则里只写了IPv4网段比如10.0.0.0/8、192.168.0.0/16、127.0.0.0/8那对IPv6地址可能完全没有覆盖。IPv4和IPv6之间有一种映射关系可以把一个IPv4地址写成IPv6的形式。访问http://[::ffff:110.110.110.110]/flag.php::ffff:是该映射格式的固定前缀后面跟点分IPv4地址。有些库在请求时会把这种IPv6映射地址翻译回IPv4地址但过滤器只看字符串发现不是它认识的IPv4黑名单就放过了。端口限制绕过有的题目固定只能访问某个端口比如只能访问80端口。但实际的flag服务可能跑在8080或其他端口上。直连被拦那就借用前面说的302跳转把跳转目标换成http://127.0.0.1:8080/flag.php。或者换协议用dict://直接探测端口比如dict://110.110.110.110:6379/info如果目标机端口开放并且返回的是Redis相关的响应说明这个端口可达且是个明文服务。dict协议经常用来验证内网端口开放情况因为它不需要完整的TCP交互发一行命令就能看出服务特征。这里也顺便说一句实战经验探测端口时不要只看Web端口。很多内网服务的价值远大于一个管理页面Redis、数据库、消息队列这些服务的端口一旦暴露给SSRF可操作空间会大很多。3.5 第四梯队16进制编码绕过与DNS重绑定16进制编码绕过有些后端流程是这样的拿到参数后先urldecode一次再尝试把结果按“十六进制文本”解码最后发起请求。这种多层解码的机制同样可以被反过来利用。做法是先把目标字符串整体转成十六进制文本。例如字符串110.110.110.110对应的十六进制是3131302E3131302E3131302E313130再把这一整串十六进制文本做URL编码塞到url参数里。后端解一次URL编码后得到3131302E3131302E3131302E313130如果它再按十六进制解码就还原成110.110.110.110请求就被转到了内网地址。这种题目的核心是“推导后端解码链路”。你不一定能一次猜准后端到底做了哪几层解码所以实操时要多试组合先试只做一层URL编码不行就再叠加十六进制、八进制、大小写变体直到让目标地址在某个环节被还原成原始值。DNS重绑定绕过假设后端对域名做了“先解析再校验”它请求域名前先把域名解析成IP发现是外网IP就放行。但它忽略了一个问题同一个域名在不同时间可能解析到不同IP。如果攻击者能让第一次解析返回公网IP第二次解析返回内网IP就形成了DNS重绑定绕过。实操上可以用一些专门提供重绑定解析的测试服务也可以自己在可控域名上配置解析记录让解析结果在多个IP之间轮换。靶场里如果有这一关通常会给一个专用域名你只需要把url参数填成这个域名多试几次总有一次“解析校验时是公网IP、实际请求时是内网IP”。注意DNS重绑定不仅对SSRF有用也是很多其他前置校验场景下的通用绕过手法。理解它的原理比硬记payload重要因为每次环境不同返回哪个IP、什么时候切换都需要现场判断。4. 解码与替换攻防双方的脑回路是怎样较量的4.1 为什么过滤器拦不住URL编码原因很简单过滤器和后端解析器的“视角”不一样。你提交urlhttp://%31%31%30...时Web服务器或框架在处理GET参数时可能已经做了一遍URL解码。如果过滤逻辑在黑名单检查之前不做同样的解码它看到的是一串%开头的字符串自然匹配不上。但要发起真实请求时后端可能又会把参数解码到真实值。这就是一种“检查时和运行时看的东西不一样”的经典漏洞。安全里有个说法叫“解析不一致”SSRF、目录穿越、命令注入里都能见到它的影子。实战印象最深的一个例子是某系统把用户输入URL之后先检查是否包含127.0.0.1再用curl发起请求。我把地址写成http://%31%32%37%2e%30%2e%30%2e%31/黑名单直接放行但curl自己会做URL解码实际访问的却是127.0.0.1。你看过滤器要是不了解下游解析器的行为防护就是一层纸。4.2 为什么302能绕过内网IP检测这题的命门在于“校验时机”和“请求时机”被分开了。正常流程应该是解析用户传入的URL。判断主机名对应的IP是否内网。判断通过后才发起请求。但很多实现成了这样解析用户传入的URL。只看第一次请求的返回状态和响应头。如果第一次能得到响应就认为安全然后让最终请求继续。公网URL返回302时第一步的安全检查已经通过。等最终请求真的执行时才会去跟随跳转这时候请求的目标已经变成内网地址了但再也没有人检查它。这种问题的本质是“信任了中间状态”。防御方必须意识到任何由攻击者控制的跳转链都必须把最终目标再校验一次否则等于没拦。4.3 云环境下的SSRF为什么大家都盯上了元数据云平台为每台虚拟机提供了一套元数据服务地址通常是http://169.254.169.254/用来查询实例ID、主机名、网卡信息、甚至云平台的临时访问凭证。这套服务只在云主机内部网络可达外部用户直接访问不到。但有了SSRF就不同了。攻击者让服务器去请求http://169.254.169.254/服务器会把这个元数据响应原样返回攻击者就能看到云实例的敏感信息。如果能继续请求元数据服务上的安全凭证接口甚至可能拿到可用于云API调用的临时令牌那就不是“读点信息”那么简单了。这是SSRF危害升级的重点场景也是为什么现在各大漏洞平台都会把SSRF单独列为高危漏洞。对测试者来说面对云上的目标时拿到SSRF第一件事就是尝试元数据接口对防御者来说这也是必须在服务器和网络层面双重拦截的原因。合规提示这些内容通常只讲原理和靶场验证。真实业务系统上未经授权测试元数据接口同样属于越权行为。学习阶段靶场是你的练习场外面的系统不是。5. 常见问题与排查技巧实录5.1 实例1靶场请求被测服务无响应现象url参数填进去页面一直转圈或返回空。排查思路先确认SSRF通道本身通不通。把url填成http://你自己的测试服务器/看能不能拿到响应。如果连自己的服务器都访问不到可能是靶场容器出网受限或者请求没发出去。再看是不是协议受限。只允许HTTP/HTTPS时你填file://或dict://请求会被直接拦截或返回异常。确认目标地址在靶场环境里是否真的可达。有些题目给的内网地址必须由靶机后台访问你本地浏览器直连肯定是不通的但这不代表漏洞不可用。用Python脚本打印响应头和状态码别只盯着页面文字看。5.2 实例2编码绕过不生效现象URL编码填进去以后请求还是被拦。排查思路检查编码层级。有些后端只解一次URL编码有些会不断解码你需要把你编码后的内容手动用urllib.parse.unquote逐层解码看在哪一层还原成目标地址。检查是否编码了不该编码的东西。比如协议头http://不能一起编码编码后后端无法识别协议。一般只对IP部分、路径部分或敏感关键词做编码。检查过滤器是不是“白名单”而不是“黑名单”。如果是白名单只允许特定域名编码没有意义这时候该考虑302跳转或DNS重绑定。不要在浏览器地址栏里手动测试编码地址浏览器的自动解码行为会干扰你的判断统一用Python脚本或Burp Suite发请求。5.3 实例3不知道访问哪个端口现象内网IP范围很大或者提示里没有明确端口。排查思路先用http://访问常见端口80、8080、8000、8443、8888等看哪些端口返回了Web页面。再用dict://IP:端口/去探如果服务支持明文协议往往能直接看到协议特征比如Redis会返回-ERR之类的错误信息。如果目标服务只监听在某个随机端口只能靠爆破常见端口列表或者看题目提示给的线索。5.4 实例4返回内容很长很乱现象拿到响应了但内容里夹杂着一堆HTML、JS、样式看起来不像目标文件。排查思路用curl或Python只打印响应Body并过滤HTML标签很多情况下flag就藏在注释里或者某个标签中间。加参数看响应头。有些题目支持?urlxxxdebug1这样的调试参数会直接输出后端请求过程。对返回内容做关键词搜索直接搜flag、{等特征。脚本里用in判断一下就把问题解决了。5.5 常见问题速查表现象可能原因解决办法url参数提交后无响应协议被白名单限制改用http://或https://重试访问file://返回空文件协议被禁用换dict://探测端口URL编码绕过无效解码层级判断错误手动逐层解码调整编码方式内网地址无法直连请求源在公网不是靶机确认通过SSRF服务端发起访问302跳转不生效跳转目标要求跨协议访问改同协议跳转检查PHP配置返回内容为空但状态码200目标页面确实返回空内容尝试访问其他路径或端口6. 这类漏洞的学习路线与个人心得6.1 学习顺序建议想真正吃透SSRF我建议按下面这个顺序来不要一上来就背payload扎实HTTP基础方法、状态码、缓存、重定向这决定了你能不能看懂请求链路。会读简单代码至少要能看懂PHP/Python/Java里发起HTTP请求的语句。SSRF本质是代码逻辑缺陷不会读代码就只能靠瞎试。先刷基础Web漏洞SQL注入、XSS、CSRF这些漏洞先把“请求是怎么被服务端处理”的大概念建立起来。再回头专攻SSRF理解原理后把CTFHub各类关卡逐个过一遍每过一关就记录“绕过点在哪一层”。动手写一个自己的漏洞Demo自己写一个有缺陷的file_get_contents(/fetch?url...)服务再试着加固再试着绕过。这个过程会极大加深理解。研究常见服务协议Redis、MySQL、Memcached的通信格式是理解dict和gopher利用的前提。读真实加固代码找一些开源项目里对URL请求的校验代码看看别人是怎么防的你就会知道哪些“绕过技巧”在现实里还有没有用。6.2 几条踩坑后的经验第一SSRF的关键不是“会背协议”而是**理清楚“谁在发请求、地址能不能被控制、响应能不能看到”**这三个问题。很多新手一到gopher协议就头晕其实只在少数场景会用到那么深的链路前期把HTTP层的各种绕过练熟收益已经很高。第二做这类题目时写脚本比浏览器好用得多。浏览器会自动解码、会自动跟随跳转、会缓存这些行为都会干扰你对“服务端到底收到什么”的判断。用Python一句一句发请求你能清晰地看到每一次请求的URL、参数和响应这比任何图形界面都管用。第三遇到绕不过去的关先回去看后端可能做了什么过滤。我见过很多人把编码试了一圈都失败了最后发现后端做的是“只允许指定域名”根本不吃黑名单那套。这时候换个思路去302跳转或DNS重绑定一下子就通了。SSRF的每一关本质上都是在猜“防御者的代码里写了什么”猜对了payload自然就出来了。第四也是最重要的一点授权边界永远要刻在脑子里。靶场里的漏洞叫学习案例公网上的目标叫事故现场。所有技巧只有在授权范围内使用才叫安全测试否则就是给自己挖坑。我见过太多人技术没学会先学会了把靶场那套直接往真实系统上套最后不仅拿不到结果还把自己搭进去了。做安全的底线不比任何漏洞利用技巧低。