SSRF漏洞原理与实战:从内网访问到gopher构造POST请求全解析

发布时间:2026/9/11 11:21:34
SSRF漏洞原理与实战:从内网访问到gopher构造POST请求全解析 SSRF服务端请求伪造这个词第一次看到的人多半会皱眉头但它背后的逻辑其实一句话就能说透让服务器替你去访问它本不该访问的地址。我在CTFHub靶场里把SSRF技能树全部关卡打了一遍从最简单的内网访问一路做到gopher构造POST请求打完之后最大的感受是——这个漏洞零基础绝对学得会只要理清原理再动手一遍就行。这篇文章就按这条路带你把原理、检测、实战、绕过和防护完整串起来。如果你正准备入门网络安全、想打CTF比赛或者已经在做渗透测试但被SSRF绕晕过这篇文章都适合你。文章不会堆砌晦涩的术语每个概念都会配一个通俗解释实战部分更是直接把payload和使用步骤写全你照着做就能在CTFHub上拿到flag。需要说明的是所有实验都在CTFHub靶场等授权环境中完成学技术的同时我们也要守住底线。1. SSRF原理拆解服务器为什么会变成攻击者的“代购”1.1 一句话说清SSRF让服务器替你“代购”内网资源SSRF是Server-Side Request Forgery的缩写翻译过来是服务端请求伪造。关键在“服务端”三个字发起请求的不是你的浏览器而是目标服务器。你可以把服务器想象成一个跑腿小哥你在后台填写了一个收货地址它会按这个地址去取东西。正常业务里这个地址应该是公网上的合法资源但如果这个地址被换成服务器自己的内网地址甚至换成服务器本地文件路径跑腿小哥也会照做不误。这就是SSRF最核心的成因——服务端信任了用户提供的地址没有做任何校验。内网这个词很多人听说过但不太理解。简单说服务器所在的局域网里有很多服务是只对内网开放的比如数据库端口、内部管理系统、云环境里的元数据接口。这些资源对外部攻击者来说不可见但服务器自己访问它们毫无障碍。所以SSRF一出现攻击者等于借用服务器的手摸到了原本碰不到的内网资源。为什么这个漏洞在CTF里出镜率越来越高因为它往往不是终点而是渗透链路的起点先用SSRF探测内网再结合其他漏洞打进内部系统最终拿到敏感数据或服务器权限。1.2 代码成因用户可控URL直接进入请求函数先看一段经典的漏洞代码?php $url $_GET[url]; $content file_get_contents($url); echo $content; ?这段代码就是很多人第一次接触SSRF时看到的经典例子。问题一目了然用户传入的url参数直接被file_get_contents当作请求地址。如果后台运行着这样一个页面攻击者传一个?urlhttp://127.0.0.1/flag.php服务器就会用自己身份去访问本机服务并原样输出结果。类似的高危写法还有PHP的curl_exec配合来自用户的URLPython的requests.get直接接收参数Java的HttpClient执行用户传入的地址我见过不少真实业务代码表面上是“图片代理”或“URL转码”内部就是拿用户URL直接请求完全没有校验。尤其是一些后台功能觉得只有管理员能用就放松了警惕结果一旦后台弱口令或XSS打进去SSRF就成了深入内网的跳板。这里的核心教训是只要请求的最终地址由用户可控又没有白名单限制SSRF就随时可能出现。1.3 危害拆解从读文件到打内网的完整杀伤链SSRF的危害可以从四个层面来看读文件file协议可以直接读取服务器上的文件比如file:///etc/passwd、file:///var/www/html/flag.php。如果应用支持直接回显等于网站源码和系统文件全部暴露。探测内网用dict协议或http协议访问127.0.0.1的不同端口能判断内网哪些服务是存活的这个信息是后续利用的基础。攻击内网无防护服务很多内网服务比如Redis、MySQL开发时默认不设密码或使用弱密码。SSRF结合gopher协议可以给这些服务发送命令进而往服务器写Shell。在内网渗透里一台能被SSRF控制的Web服务器常常是整个内网的突破口。云元数据如果服务器部署在云环境访问元数据服务地址可能拿到临时凭证、角色信息等进一步横向到云上其他资源。所以SSRF的完整杀伤链通常是发现一个可回显的SSRF → 读取源码或探测内网 → 找到内网脆弱服务 → 通过gopher等协议发起攻击 → 控制服务器或核心服务。理解这条链路后边的CTFHub关卡自然会豁然开朗。2. 上手检测前先搞懂这6类高发功能点和3种危险协议2.1 6类最容易藏SSRF的功能点附自查清单SSRF不是凭空冒出来的它藏在具体业务功能里。这些年我总结下来最容易出问题的功能点有这么几类图片加载和裁剪用户提交一个图片URL服务器远程拉图再处理。这类功能最容易藏雷因为请求目标完全由用户指定。网页截图和URL转码输入一个网址服务端生成缩略图或页面快照。我见过不止一个在线截图工具被拿去扫内网。文件导入和导出比如通过URL方式导入CSV、PDF解析远程资源这类功能通常要访问用户给的地址很容易忽略校验。Webhook回调填一个回调地址服务器去POST通知。回调地址本身就是用户可控的天然具备SSRF条件。在线翻译和预览输入目标链接服务端去获取页面内容后处理和图片加载是一个套路。日志与报告生成根据URL抓取数据生成报告原理同样是把用户输入交给服务端请求。判断方法其实很简单用Burp Suite抓包留意url、link、src、target、dest、redirect、image_url这类参数名。把参数值改成自己的DNSLog域名如果服务器产生了DNS解析记录基本可以确定存在SSRF。这一步是零成本且高效的验证方式比直接打各种协议稳妥得多建议新手先从这个动作练起。2.2 危险协议盘查file、dict、gopher在实战中的定位SSRF能不能打深关键在于服务端支持哪些协议。不同协议的能力差别很大我整理了一个对照表协议示例格式主要用途风险级别filefile:///etc/passwd读取服务器本地文件高dictdict://127.0.0.1:6379/info访问TCP服务、进行端口探测中高gophergopher://127.0.0.1:80/_POST...构造任意TCP数据流极高http/httpshttp://127.0.0.1/flag.php访问内网Web服务中ftpftp://user:passhost/file读取FTP资源中为什么gopher最危险file只能读取http只能按照Web方式请求而gopher允许你自定义发送到目标服务的整个数据流。你可以像拿着TCP工具一样给内网任意端口发送任意内容。打个比方file和http相当于只能通过固定窗口和商家交易gopher则是直接撬开后门想送什么就送什么。所以后边遇到构造POST或上传文件最终都要落到gopher上。2.3 无回显场景用响应时间和DNSLog做盲判断很多SSRF是不直接输出内容的这就需要用“盲”思路判断。最常用的两种方法第一种是响应时间差异。让服务器去请求一个不存在的内网端口通常立刻报错去请求一个开放端口响应时间会有微妙差别。利用这个差异可以写一个简单脚本遍历端口记录响应的状态码和耗时识别哪些端口是开放的。这种方法在没有回显、只能看到请求是否成功时非常管用。第二种是DNSLog。先把URL改成http://你的子域名.dnslog.cn/xxx如果DNSLog平台收到解析记录说明服务端确实发出了外部请求。这个方法特别适合没有回显、也无法从响应中判断的场景。值得注意的是在靶场里多观察每个请求的返回长度很多时候flag就藏在响应差异中。3. CTFHub靶场实战保姆级通关SSRF全部关卡3.1 环境准备注册CTFHub并理解题目入口CTFHub是一个在线CTF训练平台SSRF技能树就在Web方向下。注册账号、进入技能树页签就能看到SSRF系列题目。这些题目都是在线环境不用自己搭建打开即可访问这一点对新手非常友好。进入题目后你会看到一个类似http://challenge-xxx.ctfhub.com/?urlxxx的页面所有题目都围绕这个url参数展开。开始之前我先说一个贯穿始终的思路每道题都在考察同一个核心能力——能不能控制服务端去访问指定目标并把响应拿回来或通过某种方式验证到。因此拿到题先不用着急打先观察url参数、尝试访问一次http://www.baidu.com看看是否回显再决定下一步用哪种协议。我几乎每道题都会做这一步“基线测试”既能确认漏洞存在也能熟悉靶场环境的回显风格。3.2 第一关 内网访问最简单的直连拿flag题目要求是访问位于127.0.0.1上的flag.php。直接把url参数改成http://127.0.0.1/flag.phphttp://challenge-xxx.ctfhub.com/?urlhttp://127.0.0.1/flag.php提交后页面上直接出现flag。为什么这么简单因为127.0.0.1代表的就是服务器自己业务代码在请求这个地址时和访问公网URL没有任何区别它自然就把本机的flag.php内容拿回来了。这题的意义在于让你理解内网资源对攻击者不可见对服务器自己可见SSRF正好把这个差异拉平了。做这题我建议你在Burp里操作不要直接在浏览器地址栏改。因为浏览器会自动处理一些编码和跳转可能掩盖真实的请求过程。用Burp Repeater发送的话你能清楚看到服务端返回的完整HTTP响应对于判断回显非常有帮助。3.3 第二关 伪协议读取文件file://打穿源码这题要求读取位于/var/www/html/flag.php的文件。既然题名提示“伪协议”直接用file协议http://challenge-xxx.ctfhub.com/?urlfile:///var/www/html/flag.php注意file协议后面是三个斜杠file://再加上一个绝对路径的前缀斜杠。如果路径不太确定可以先读取file:///etc/passwd确认操作系统信息再结合题目提示猜路径。靶场里通常直接访问/var/www/html/flag.php或/flag这类路径就能拿到flag。这道题暴露了SSRF的一个很现实的问题它能读文件但不一定知道文件在哪。所以信息收集很重要。读懂响应差异、多试几个常见路径往往比死磕一个路径更高效。如果页面直接把PHP源码显示出来说明服务器是把file_get_contents的原始返回直接回显了处理这种“带有源码回显”的SSRF时读PHP文件可以顺便把业务逻辑也看清楚。3.4 第三关 端口扫描dict://探测内网端口题目要求探测内网端口。由于url参数最终发起的是一个HTTP请求直接用http://127.0.0.1:端口可能得不到准确的开放信息。这里比较合适的协议是dict它会把TCP连接后的响应内容原样返回给调用方非常适合端口探测。比如请求dict://127.0.0.1:6379/info如果端口开放页面会返回Redis版本等响应内容如果端口关闭则返回连接失败或空内容。CTFHub这一关通常会让你扫描几个常见端口找到开放的那个并去访问它。实际测试中用Burp的Intruder遍历端口即可每一条payload的回显长度就是判断依据回显长度异常的端口值得重点看。我提供一个简单的Python扫描脚本思路import requests target http://challenge-xxx.ctfhub.com/?url for port in range(8000, 9000): payload fdict://127.0.0.1:{port}/ try: r requests.get(target payload, timeout5) if len(r.text) 0: print(f[] {port}: {r.text[:120]}) except Exception: pass这里的思路是字典协议请求开放端口时会返回一些内容关闭端口则基本为空。通过比较返回长度快速锁定存活服务。实际做题看到明显回显不一致的端口再用HTTP去直接访问确认基本就能定位到答案。3.5 第四关 POST请求gopher构造完整HTTP数据包这题要求以POST方式请求http://127.0.0.1/flag.php参数为keyflag。直接修改url参数发起的请求永远是GET所以必须换用gopher协议主动构造一个POST数据包。gopher的格式比较特殊完整表达为gopher://127.0.0.1:80/_开头下划线是一个占位符会被gopher客户端吃掉真正传给服务端的是下划线后面的全部内容。我们需要把下面这个原始HTTP请求整体编码后拼进去POST /flag.php HTTP/1.1 Host: 127.0.0.1 Content-Type: application/x-www-form-urlencoded Content-Length: 8 keyflag注意这里的换行必须是CRLF也就是\r\n在URL编码里就是%0D%0A。把所有内容URL编码后构造出的最终payload类似gopher://127.0.0.1:80/_POST%20/flag.php%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1%0D%0AContent-Type%3A%20application/x-www-form-urlencoded%0D%0AContent-Length%3A%208%0D%0A%0D%0Akey%3Dflag把这段拼到url参数后面提交服务端就会以POST方式把keyflag送到flag.php返回flag。当时我第一次做这题时最大的坑是忘记在gopher://127.0.0.1:80/后面加下划线结果请求一直为空。所以给每个gopher payload都养成“先写下划线再写内容”的习惯能省不少调试时间。3.6 第五关 上传文件multipart表单的完整构造这题是在POST基础上再进一步要求以multipart/form-data方式上传一个名为flag.txt的文件。也就是说你不仅要构造POST请求还要构造一个合法的文件上传表单仍然使用gopher。原始HTTP数据包长这样POST /flag.php HTTP/1.1 Host: 127.0.0.1 Content-Type: multipart/form-data; boundary----WebKitFormBoundaryabc123 Content-Length: 168 ------WebKitFormBoundaryabc123 Content-Disposition: form-data; namefile; filenameflag.txt Content-Type: text/plain test ------WebKitFormBoundaryabc123--这里几个参数要严格对齐boundary值要和Content-Type里的保持一致文件内容随意但结尾的--一定不能少Content-Length必须准确等于整个实体部分的字节数多一个空格都不行。最容易出错的就是Content-Length我曾经把换行符数错导致服务端一直不认这个上传包。有一个实际建议先用Python脚本自动生成URL编码后的payload不要手写。我常用的做法是先把上面原始请求存成字符串再用urllib.parse.quote编码然后把换行统一替换成%0D%0A最后在前面拼上gopher://127.0.0.1:80/_。脚本化生成能极大减少手误。import urllib.parse payload POST /flag.php HTTP/1.1 Host: 127.0.0.1 Content-Type: multipart/form-data; boundary----WebKitFormBoundaryabc123 Content-Length: 168 ------WebKitFormBoundaryabc123 Content-Disposition: form-data; namefile; filenameflag.txt Content-Type: text/plain test ------WebKitFormBoundaryabc123-- encoded urllib.parse.quote(payload).replace(%0A, %0D%0A) print(gopher://127.0.0.1:80/_ encoded)把这个脚本的输出拼接到url参数里提交后就能拿到flag。这个技巧不只是打靶场有用真实做授权测试时遇到内网的文件上传接口同样的思路可以直接复用。3.7 进阶关卡提示FastCGI与URL BypassCTFHub的SSRF技能树还有FastCGI这类进阶题做法更复杂通过gopher向内网FastCGI服务发送协议数据设置PHP的auto_prepend_file等变量配合写入日志触发任意代码执行。这个方向原理深代码量大建议先把前面几关吃透再来研究FastCGI协议格式。另外还有一类URL Bypass题专门考察过滤绕过过滤了127.0.0.1就直接用十六进制IP或短域名过滤了内网地址就通过可控的302跳转绕过去。这种题非常考思维也正好把下一章我要讲的绕过技巧提前用上了。入门阶段不必强求通关每一题但每道题背后对应的绕过思路应该记下来这些都是后续实战的武器库。4. 实战中最容易踩的坑过滤绕过与gopher构造细节4.1 目标IP被过滤进制转换与解析绕过技巧靶场有时会过滤掉127.0.0.1等明显内网地址常见绕过办法如下绕过方式示例说明十进制IPhttp://2130706433/浏览器解析为127.0.0.1十六进制IPhttp://0x7f000001/同上八进制IPhttp://0177.0.0.1/兼容写法IPv6回环http://[::1]/部分环境可用域名简化http://localhost/大多数系统解析到本机末尾加点http://127.0.0.1./部分解析器会忽略末尾点语法http://example.com127.0.0.1/实际请求127.0.0.1302跳转自己服务器跳转到内网绕过URL校验这些技巧的原理都是利用各种解析器对IP和URL的理解差异。同一个地址字符串看起来不同但解析结果完全相同。实际绕过滤时可以先用进制转换撞一撞不行再上302跳转类方案。这类技巧在CTFHub的URL Bypass关卡里经常是唯一解。4.2 URL编码和特殊符号三个容易翻车的小坑第一个坑是符号。如果url参数后面还有其他参数而你的payload里包含它会被Web框架当成参数分隔符导致请求内容被截断。解决办法是对做URL编码为%26。第二个坑是换行符。gopher构造的HTTP数据包必须使用CRLF\r\n也就是编码后的%0D%0A如果用LF\n很多HTTP服务端不会正确解析。第三个坑是能编码就尽量全部编码不要偷懒只编码特殊字符否则在线靶场的URL解析逻辑可能对某些字符做二次解码导致最终到达gopher协议的数据不完整。我踩坑之后养成了习惯所有payload一律先用Burp Decoder整体编码再拼接到完整URL中用Burp Repeater发送验证不在浏览器地址栏里做最终提交。地址栏会自动美化URL比如把%3A还原成冒号非常影响调试。4.3 gopher构造自查Content-Length、换行与占位符检查项正确做法错误后果下划线占位符gopher://ip:port/_后接数据数据不发送换行统一替换为%0D%0A服务端解析失败Content-Length与实际实体字节数一致请求被截断或超时末尾空行HTTP头结束后必须有一个空行请求不完整我分享一个自测方法先在本地用netcat监听一个端口把构造好的gopher数据发到本地看收到的原始数据包是否和预期完全一致。确认无误后再套进靶场地址调试效率会高很多。这比在靶场里反复猜原因靠谱得多。5. 从攻击回到防御SSRF修复方案与代码审计思路5.1 服务端修复从协议白名单到重定向处理修复SSRF核心是三个字别信用户。具体到落地第一步是协议白名单只允许http和https禁用file、dict、gopher、ftp等危险协议。第二步是IP校验不能只判断用户传入的字符串要先解析DNS得到真实IP再判断是否属于内网地址段。这里特别提醒只校验一次还不够curl默认会跟随重定向第一次校验通过后重定向跳转到内网依然会请求成功所以要么关闭重定向要么对重定向后的地址再次做同样的校验。第三步是端口限制业务不需要的端口一律拒绝通常只保留80和443。第四步是响应处理不要把服务端请求的原始响应全部返回给用户尽量只返回业务需要的部分。最后如果业务必须使用动态URL建议引入地址白名单或哈希验签机制从根上限制用户可以访问的目标范围。5.2 代码审计挖掘SSRF的经验路线做代码审计时优先找这些函数调用PHP的file_get_contents、curl_exec、fsockopenPython的requests.get、urlopenJava的HttpClient、URLConnection。拿到调用点之后向上追踪变量来源看最终请求的URL是否来自用户输入。很多SSRF都藏在图片处理、URL预览、Webhook这类不起眼的业务功能里所以审计时不要只盯着登录认证这类核心模块。另外一个容易被忽略的地方是“间接可控”。比如用户上传一个文件文件内容里包含一个URL程序解析文件后发起请求这个URL同样属于用户可控输入。追踪数据流时这类间接链路也要覆盖到。5.3 合规意识SSRF测试的边界SSRF之所以被很多人认为“危险”不只是因为技术本身更是因为一旦用在不该用的地方很容易碰到别人家的内网和云上资源。做任何测试前先确认目标是否授权是不是自己搭建的靶场或官方训练平台。CTFHub这类平台是专门用来练手的在这样的环境里随便折腾没有问题但到了真实的业务系统没有书面授权就做SSRF探测很可能会触发法律风险。更稳妥的做法是本地用Docker搭建一个简单的漏洞环境或者使用CTFHub、Pikachu这类训练靶场把文章里的payload全部跑一遍既练了手又不用担心越界。技术本身是中性的但使用技术的边界必须清晰。我在实际打穿CTFHub的SSRF技能树时最大的体会就是这个漏洞的门槛没有想象中高真正的难点在于信息收集和请求构造。很多人在gopher协议上卡住其实只要成功构造一次POST后面上传文件、FastCGI利用都只是同一个套路的变体。所以如果你的时间有限先把第三关、第四关和第五关吃透其他的遇到再查都来得及。最后再分享一个小技巧打这类靶场时别急着上脚本先把每次修改url参数后的响应差异记录下来“有回显”和“无回显”的边界往往就是漏洞利用最关键的那条线索。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询