最近带着几个新人把 CTFHub 技能树 Web 篇里的 SSRF 关卡从头到尾刷了一遍,一边打一边记录,回来整理成这篇通关教程。SSRF(Server-Side Request Forgery,服务端请求伪造)这个漏洞在 CTF 里出镜率非常高,CTFHub 技能树里专门给它排了一条从入门到进阶的关卡链,从最朴素的直接访问内网地址,一直到改协议头、变换进制绕过、配合重定向抓内网资源,层层递进。
这篇教程不打算只给答案,我会把每一关背后的原理讲明白,再附上我实际通关时用到的请求包、payload 和踩过的坑。适合刚接触 Web 安全的读者照着一步步操作,也适合刷完题但没想透“为什么要这么绕”的同学回头补原理。全程在 CTFHub 的靶场环境里进行,不需要自己搭环境,打开浏览器就能开始。
1. SSRF 到底是个什么东西:一次服务端请求引发的连锁反应
1.1 从正常功能到漏洞:请求从来没说过只准发给外部
很多初学者第一次看到 SSRF 这个名词,会本能地觉得“这跟 XSS、SQL 注入一样,是不是往参数里塞一段特殊字符就能拿 flag”。方向算对了一半,但 SSRF 的触发逻辑和前端漏洞完全不同。XSS 是把恶意脚本塞进页面让浏览器执行,SQL 注入是把 SQL 片段拼进查询让数据库执行,而 SSRF 的本质是服务端代码接收了你提供的“目标地址”,然后替你去访问这个地址。
举个例子,很多网站有“头像抓取”功能:你提交一个图片 URL,服务器后台用 curl 之类的方法把图片下载下来存到本地。写程序的工程师为了让代码灵活一点,直接把 URL 参数透传给了 curl,于是你传https://example.com/1.jpg,服务器就去访问https://example.com/1.jpg。问题来了,服务器自己所在的网络中,除了公网,往往还有一块不被外部访问的内网区域,这块区域里可能跑着数据库、缓存服务、内部管理后台、运维接口。如果你把 URL 从https://example.com/1.jpg改成http://127.0.0.1/admin,服务器就会带着它自己的身份和网络权限,去访问本机或内网里的管理后台。
CTF 里经常用一句话概括这种利用方式:攻击者没办法直接访问内网,但服务端可以。攻击者要做的,就是想办法让服务端替自己访问。CTFHub 技能树的 SSRF 关卡,整体就是围绕“怎么让服务端把请求发到不该发的地方”设计的,每一关都在逼你突破一类限制。
1.2 触发 SSRF 的常见函数与协议面
要判断一个接口是否存在 SSRF,首先要看服务端用什么函数发起了请求。PHP 环境是 CTF 中最常见的,这里列三个出现频率最高的函数族:
curl_exec():最灵活,支持 http、https、file、dict、gopher、ftp 等多种协议,CTF 里绝大多数高阶玩法(尤其是 gopher 打内网服务)都依赖它。file_get_contents():默认支持 http,也支持 file 协议直接读本地文件,如果代码里没做协议限制,一个file:///etc/passwd就能把服务器上的文件内容拉出来。fsockopen():偏向底层,适合构造原始 TCP 数据包,在需要手动组装请求的场景里有用。
协议面决定了你能让服务端“访问什么类型的目标”。http://只能访问 Web 服务,file://能读文件,gopher://能像一个原始 TCP 客户端一样向任意端口发送自定义数据。CTFHub 的关卡里,前面几题你只会用到 http,后面就开始考察协议切换和请求构造能力了。
提示:看到 URL 参数名像
url=、target=、path=、dest=、addr=的接口,都可以下意识在脑子里给这个接口打一个“可能 SSRF”的标签,然后拿http://127.0.0.1/去试。CTF 里这种参数命名习惯非常多。
2. CTFHub 技能树里 SSRF 关卡的整体设计逻辑
2.1 题目编排:每道题差的不只是过滤规则
CTFHub 的 SSRF 关卡不是简单地在同一个漏洞点上换几个过滤函数,它有一套递进逻辑:先让你理解“什么是内网地址”,再让你面对“过滤器把内网地址拦了”的困境,接着给你一个只能走特定协议的苛刻环境。我把它理解成一堂设计过的安全实验课,而不是一道一道孤立的考题。
第一类关卡是没有任何过滤的,直接让你把 URL 指向内网地址拿 flag;第二类开始出现字符串黑名单,常见的过滤手段是把127.0.0.1、localhost这些关键词直接替换或拦截;第三类加入了协议白名单,只允许 http 或 https,这时候你想用file://读文件就会被挡;第四类把解析差异和重定向结合起来考,需要你利用 URL 解析器与请求库之间的理解差异来绕过;最后一类是高阶玩法,配合 gopher 协议去访问内网的 Redis、FastCGI 等服务。每一关对应一类真实场景,刷完基本就把常见的绕过滤手段摸熟了。
从出题人的角度想,这个顺序设计得非常合理:如果一上来就考 gopher 协议打 Redis,新手一定一脸懵,因为很难理解“服务端帮我发一串自定义 TCP 数据”这件事能造成什么后果。但如果你已经经历了前面几关,理解了服务端是“带着自己的身份去请求”,再看到 gopher,就很自然地联想到“我能不能让服务端冒充内网客户端去和 Redis 说话”。
2.2 考点分类:直接访问、进制绕过、重定向、协议限制
把 CTFHub 的 SSRF 关卡拆开看,核心考点可以归纳成四类。
第一类是基础访问型。题目代码只做了最基本的 URL 获取,连过滤都没有,你只需要把请求目标改成内网地址。考察点是对 SSRF 概念的理解。
第二类是字符串绕过型。过滤规则往往是preg_replace('/127\.0\.0\.1/', 'localhost', $url)或直接strpos检查目标地址里是否包含内网字样。应对方法是把 IP 换成等价变体:十进制整数形式、十六进制形式、八进制形式,或者利用 URL 解析器对主机名的宽容处理,比如127.1这种省略写法。
第三类是重定向与解析差异型。过滤规则拦的是“最终请求的目标”,但服务端拿到 URL 后会先做一层跳转,在跳转过程中可能绕过检查。利用方式是自己搭建一个返回 302 跳转的地址,跳转到内网地址。解析差异则利用 PHP 的parse_url与 curl 对同一个 URL 的不同理解,经典例子是http://example.com@127.0.0.1,有些解析器会以为主机名是example.com,但 curl 实际请求的是127.0.0.1。
第四类是协议限制与数据构造型。这类题目限制了只能使用http://,但你可能需要访问的不是 Web 端口,这时候要用gopher://构造原始请求,或者利用dict://探测端口和发送简单命令。
2.3 通关准备:一个趁手的抓包工具能帮你省下一半时间
CTFHub 的靶场直接在浏览器里交互就能做,但建议别只靠浏览器。我刷这组题时全程开着抓包代理工具,浏览器只负责发请求,工具负责看请求详情、改包重发,这个习惯在调试 gopher payload 时帮了大忙。你不需要装多复杂的工具,社区里最常见的抓包工具就足够,关键是熟悉“拦截请求—修改内容—重放”这条流程。
另外一个建议是准备一个能快速算进制转换的终端。后面有一类题要把127.0.0.1转成各种进制表示,虽然在线转换网站也能算,但命令行里一条printf解决效率更高,而且不会因为网页弹窗打断思路。我习惯用 Python 交互式环境,int('0x7f000001', 16)这类计算随时能写。
3. 逐关拆解:从直接打内网到 gopher 协议构造
3.1 第一类关卡:无过滤直接访问内网地址
这一关打开之后,页面会有一个输入框,让你填 URL,提交后服务端就把这个 URL 的内容返回给你。没有过滤,没有协议限制,唯一的目的就是让你建立 Web 请求的映射关系:在浏览器输入http://127.0.0.1/返回的是本机 curl 访问的结果,而不是你浏览器访问的结果。
请求 Payload 直接填http://127.0.0.1/flag.php(具体文件名看题目标签和提示,通常关卡的提示会直接说明内网服务路径,如果没说,可以先用http://127.0.0.1/探一下根路径,看返回内容是什么)。也可以填http://127.0.0.1:80/,80 端口一般就是本机 Web 服务。这个关卡里,返回内容中直接包含 flag,不需要额外操作。
注意:如果填
127.0.0.1没反应,可以先检查是不是把 URL 写成了https://。CTFHub 的靶场基本都走 http,https 的请求会在 SSL 握手阶段失败,返回空内容。另一个排查点是确认服务端是否要求带端口,有些关卡的内网服务起在某个特定端口,比如http://127.0.0.1:8080/。
3.2 进制与省略写法绕过:把 127.0.0.1 变成各种“马甲”
第二类关卡开始有过滤了,常见的是把127.0.0.1这个字符串直接替换为空,或者检测到就返回 ”forbidden”。这时候要思考一个问题:过滤器过滤的是字符串,而 curl 请求时解析的是地址。字符串层面的拦截,完全可以用地址层面的等价变体来绕过。
127.0.0.1的等价变体有这么几类:
| 表示方式 | 写法示例 | 说明 |
|---|---|---|
| 十进制整数 | http://2130706433/ | 把 IP 四段按 256 进制合并成一个整数 |
| 十六进制整数 | http://0x7f000001/ | 同上,用 16 进制表示那个整数 |
| 单个八进制数 | http://0177.0.0.1/ | 每段可用八进制表示,127的八进制是0177 |
| 混合写法 | http://127.1/、http://0x7f.1/ | IP 地址段可以省略为 8 位和 16 位形式 |
| 点分十进制变体 | http://0177.0.0.1/ | 四段进制混用,通常也能通 |
我实际通关时用的最多的组合是http://2130706433/和http://127.1/。前者是把127.0.0.1当成一个 32 位无符号整数:127 是前 8 位,0 是后 24 位,计算结果就是127 * 256^3 + 0 * 256^2 + 0 * 256 + 1 = 2130706433。后者127.1利用了 URL 解析器的宽容规则,第一段正常,第二段被当作 16 位整数,1就代表0.1,最终解析为127.0.0.1。
这类关卡的过滤器如果只过滤了127.0.0.1这个字符串,你用一个进制变体直接就能过。如果过滤器把0x7f、2130706433这些也拉黑了,那就顺着上面的表往下试,通常总有一个不在黑名单里。CTF 的出题人一般只会写几行简单的正则,不会把整个 IP 变体体系全防住,这也是为什么这类题在实战里非常有代表性——很多真实系统的过滤规则同样只挡了最明显的写法。
补充一个我自己常用的 Python 换算小工具:
ip = "127.0.0.1" parts = list(map(int, ip.split("."))) decimal = parts[0] * 16777216 + parts[1] * 65536 + parts[2] * 256 + parts[3] print(decimal) # 2130706433 print(hex(decimal)) # 0x7f000001 print(".".join(oct(x).replace("0o", "0") for x in parts)) # 0177.0.0.13.3 URL 解析差异:@符号、短域名与重定向机制
第三类关卡明显感觉出题人开始“不讲武德”了,因为过滤器不再只查 IP 字符串,而是直接把127.0.0.1、localhost、2130706433、0x7f全塞进了黑名单。这时候再单纯换进制写法就不好使了,得换思路。这一类关卡的共同点是考你URL 的组成部分在哪个环节被解释。
第一个关键点是@符号。一个 URL 形如http://user:pass@host:port/path,@前面是用户名和密码,@后面才是真正的主机名。有些过滤器用正则去匹配://后面的一整段,然后检查里面有没有黑名单 IP,但它忽略了@符号的存在。http://example.com@127.0.0.1/这个 URL,正则可能以为主机名是example.com,但 curl 实际把@前面的部分当作认证信息丢弃了,真正去连的是127.0.0.1。
第二个关键点是重定向。过滤器校验的 URL 与最终实际访问的 URL 可以不是同一个,因为一次 HTTP 请求里,服务端可以返回 302 状态码,并在Location头指定浏览器或 curl 的下一个访问地址。很多过滤逻辑只检查了用户提交的原始 URL,没有跟踪重定向链。实现方式是先找一个能正常通过校验的地址,比如http://example.com/,然后在某台服务器上部署一个返回 302 的脚本,Location: http://127.0.0.1/。curl 遇到 302 默认会跟随跳转,过滤检查却只发生在请求发出之前。
第三个关键点是短域名。有些外部域名服务允许把127.0.0.1映射成短域名,比如形如http://xxx的域名,解析后就指向本机。过滤器看到的是一个普通域名,curl 解析后却拿到了内网 IP。
CTFHub 这类关卡中,@和重定向机制比较常见。我当时通关用的 payload 是http://example.com@127.0.0.1/,如果题目过滤得严,再换 302 重定向的方案。重定向方案的具体做法后面第 4.2 节我会重点复盘,因为那里有一个非常典型的坑。
3.4 协议限制下的再突破:file 协议读文件与 http 协议打内网
到了协议限制型关卡,题目代码明确写了只允许 http 和 https,其他协议一律拒绝。表面上看,这条路已经被堵死了,因为你没法再用file:///etc/passwd直接读文件。但仔细观察出题逻辑会发现,协议限制只是缩小了你的武器库,没有缩小内网服务暴露的攻击面。既然只允许 http,那就用 http 去访问内网里跑着的 Web 服务。
CTFHub 平台上这类关卡通常有一个隐藏的内网服务在某个端口监听,比如 8080、8000、8888 甚至随机端口。通关思路是:先用 http 协议做一个内网端口探测。如果你能拿到响应状态码或响应时间差异,就能判断哪些端口开放。举个例子,填http://127.0.0.1:80/和http://127.0.0.1:8080/,如果一个返回正常页面一个连接失败,端口开放情况就出来了。除了逐端口试,也可以扫一段内网 IP 范围,比如http://127.0.0.1:65535/明显不对,但http://172.17.0.2:8080/这种常见 Docker 内网地址值得试一轮。
有些关卡在 http 协议下塞了一个限制:不允许访问外网地址,只能访问内网。这反而简化了判断——你只需要在给定的 IP 段里找 Web 服务。我刷题时习惯先试127.0.0.1,再试172.17.0.1和172.17.0.2这几个容器网络里最常见的地址,大部分靶场的内网服务都会落在这些网段里。找到内网 Web 服务后,再用同样的 URL 参数访问它的具体路径,flag 通常就藏在页面上或者某个文件路径里。
如果此时真的需要读取服务器上的本地文件,而协议被卡死在了 http,还有一个思路:通过内网 Web 服务的任意文件读取漏洞来间接读文件,比如目标内网服务本身就有?file=../../etc/passwd这种接口。这类二次利用在 CTF 里并不少见,因为出题人布置的内网服务很少是“孤岛”,往往复用了真实服务里常见的功能缺陷。
3.5 gopher 的终极玩法:从打 Web 端口到打 Redis 端口
gopher 协议的用途可以类比成“用一条 URL 就拼出一个原始 TCP 请求”。普通 http 协议只能发 HTTP 格式的数据,gopher 协议则允许你指定目标 IP、端口、以及要发送的原始字节流。这意味着,只要内网里任何一个服务接受 TCP 明文数据,你就能用它来通信。在 CTF 的 SSRF 题目里,最常见的两个目标是 Redis 和 FastCGI。
以 Redis 为例,假设内网 IP 是127.0.0.1,端口 6379 跑着未授权访问的 Redis 服务。常规 HTTP 协议拿它没办法,因为你没法在 HTTP 请求里塞进 Redis 的 RESP 协议命令。但 gopher 请求长这样:
gopher://127.0.0.1:6379/_*1%0d%0a$8%0d%0aflushall%0d%0a..._后面的部分就是要发送的原始 TCP 数据。这里有个关键细节:所有换行符必须 URL 编码成%0d%0a。Redis 的协议是行基协议,每条命令以\r\n结尾,如果你在 URL 里直接写了换行符,传输过程中会被剥落或截断,命令根本执行不了。我在这个编码细节上卡了半小时,后来把 FLUSHALL、SET、CONFIG SET 一整条命令链都编码之后,才成功写入了 WebShell。
FastCGI 也是类似思路,gopher 构造 FastCGI 的二进制数据包,打到内网 PHP-FPM 端口,从而执行任意 PHP 代码。这类更进阶的利用在 CTFHub 技能树里不是所有版本都有,但思路值得理解:服务端能访问什么协议,你就能用什么协议,最终目的都是让服务端替你在内网里执行一段数据。
构造 gopher payload 的时候,我强烈建议用脚本生成而不是手写编码后的字符串,否则一旦命令长了很容易漏一个%0d%0a。下面这个 Python 片段可以把 Redis 命令列表直接转成 gopher 的 payload:
import urllib.parse def redis_gopher(commands): payload = "" for cmd in commands: payload += f"*{len(cmd)}\r\n" for arg in cmd: payload += f"${len(arg)}\r\n{arg}\r\n" return "gopher://127.0.0.1:6379/_" + urllib.parse.quote(payload) commands = [["info"], ["config", "set", "dir", "/var/www/html"]] print(redis_gopher(commands))4. 实战复盘:我踩过的三个典型坑
4.1 file 协议读文件的路径问题
有一类题目没限制协议,第一反应自然是直接用file:///etc/passwd读文件,但我在 CTFHub 上第一次试的时候返回了空页面。原因排查后发现,靶场环境运行在容器里,默认工作目录和文件系统结构与标准 Linux 不一定一致,而且有些题目把 flag 放在了 Web 目录下,跟/etc/passwd八竿子打不着。
正确操作是先用file:///etc/passwd确认文件读取能力是否存在,如果返回了正常的root:x:0:0:root:/root:...开头内容,说明 file 协议是通的;然后根据题目提示找 flag 的具体路径。常见路径有/flag、/flag.txt、/var/www/html/flag.php、/var/www/flag。如果你发现file:///etc/passwd读不出来,先确认请求的目标主机是不是127.0.0.1,不要漏掉 URL 里的主机名部分,完整写法是file://127.0.0.1/etc/passwd,直接把路径放在file://后面在某些环境下会被当成相对路径处理。
提示:用 file 协议读文件时,如果页面返回空白,先把响应头打开看状态码。200 返回空白可能只是文件内容里有特殊字符被前端隐藏了,右键查看源代码往往就能看到 flag;502 或 403 才是真正的路径或权限问题。
4.2 重定向方案在这个平台上为什么不生效
我在第 3.3 节提到重定向绕过过滤是个好思路,但实操时发现一个容易翻车的点:如果你直接把重定向目标指向http://127.0.0.1/,很多版本的 curl 默认会带上CURLOPT_FOLLOWLOCATION跟随跳转,可目标 URL 中的 IP 如果被某种协议层策略拦截(比如服务端对最终解析 IP 有限制),还是会失败。另一个更隐蔽的坑是:服务端代码如果用了parse_url验证原始 URL 的 host 是否合法,那么你提供的重定向服务域名必须能正常解析,否则请求根本发不出去。
我在本地验证环境里测试了一套可行的方案:在某台公网服务器的 Web 目录放一个r.php,内容就一行:
<?php header("Location: http://127.0.0.1/flag.php");然后把关卡 URL 填成http://我的服务器域名/r.php。这样过滤规则看到的是一个普通域名,通过校验;curl 请求时拿到 302 后自动跟到内网地址,flag 内容就返回了。CTFHub 的靶场如果限制了你访问公网域名,这个方案会失效,那就只能换回进制绕过或@符号绕过。说明一下:这里的“服务器”泛指你能够控制的一个 URL 响应源,技术上也可以用各种免费托管服务的动态接口实现,不一定要真的购买一台云主机。
4.3 gopher 构造请求时的 URL 编码细节
gopher 协议最大的坑就是编码。我在刷 Redis 相关关卡时,第一次生成 payload 后在本地用curl "gopher://..."测试,发现 Redis 完全没有反应,命令没有执行。检查之后发现问题出在一个细节上:gopher 的 URL 里,_后面的部分会被当作请求数据,而这个数据块里面的所有\r\n必须编码成%0d%0a,但这是针对传给 HTTP 代理或者直接通过 curl 访问的情况。在 CTF 场景里,服务端用 curl 拿到了这条 gopher URL 后,会先 URL 解码,再把内容通过 TCP 发出去。如果你把整段 payload 再手动编码一次,就会导致服务端解码后发给 Redis 的数据是双重编码的乱码。
换句话说,直接把原始 Redis 协议数据做一次 URL 编码,是安全的;人工在网页上复制粘贴时不小心把已经编码的%再转成%25,是事故。解决办法是全程用脚本生成 payload,不要手动改。我后来写了一个小工具函数,先构造出完整的 Redis 请求文本,再统一 quote,保证只编码一次。
另外,gopher 的协议头后面通常用一个_来占位,这个_本身没有任何含义,只是防止 URL 里协议名和路径之间出现空段。你也可以换成任意字符,但_是社区约定俗成的写法,避免混淆。
4.4 一条完整的通关思路复盘:从无过滤到 gopher
把前面几关串起来,我总结出了一个适用于大部分 SSRF 关卡的排查顺序,照着这个顺序做,基本不会漏掉突破口。
先看题目代码或提示,确认有没有协议限制。没有协议限制,优先尝试file:///etc/passwd确认文件读取,再用http://127.0.0.1/探测内网 Web 服务;有协议限制,先把进制变体、@符号、重定向这些绕过手段按顺序试一遍。若所有绕过手段都失效,且题目提示里有“内网服务”字样,就用 http 协议做端口探测和目录探测。最后,如果明确要求“访问 Redis 的 6379 端口”或“使用 gopher 协议”,再进入数据构造阶段,用脚本生成 payload,打完收工。
这套顺序的好处是每一步都在增加信息量:文件读取能告诉你服务器文件系统结构,内网探测能告诉你网段和端口分布,gopher 能让你在拿到基线信息后精准打击特定服务。刷题不是目的,搞清楚每一步为什么这么试,才是这套技能树真正想训练的东西。
5. 常见问题速查与避坑清单
5.1 报错类型与排查思路汇总
我把刷题过程中遇到过的几类现象和对应排查方法整理成了速查表,方便大家对照操作。
| 现象 | 可能原因 | 排查操作 |
|---|---|---|
| 返回空白页面 | 协议被限制 / 路径不对 / 内容被前端隐藏 | 右键查看源代码,检查响应头状态码 |
| 返回 “forbidden” 或过滤提示 | 触发了关键词黑名单 | 换进制变体、@写法、重定向方案 |
| URL 提交后没有任何请求发出 | 协议校验不通过,不合法协议被拦截 | 查看代码提示中的协议白名单 |
| 端口探测时所有端口都超时 | 内网 IP 判断错误 / 防火墙封锁 | 先试127.0.0.1,再试172.17.0.1~172.17.0.5网段 |
| gopher 请求发出去但服务无响应 | 编码错误 / Redis 命令格式错误 | 用脚本生成 payload,检查%0d%0a数量 |
| 302 重定向到内网仍返回空白 | 目标路径不存在 / 请求被内网其他策略拦截 | 先测http://127.0.0.1/确认连通性,再测具体路径 |
5.2 新手最容易忽略的五个细节
先说第一个:URL 参数里的 IP 地址不等于最终请求的 IP。过滤规则检查的是字符串,curl 解析的是 DNS 和 IP 地址,这两层之间永远存在绕过的空间。第二个是不要忽略端口的存在,很多关卡的内网服务就在某个隐蔽端口上,先用http://内网IP:端口/去测,比你盲目录爆破有效得多。第三个是协议限制不只是 http/https 两种,gopher、dict、file 各自能到达不同的协议空间,必要的时候全部试一遍。
第四个细节是关于@符号和 URL 编码的:某些过滤规则会先urldecode再检查,如果你把%40当作@传进去,可能在解码之后反而触发了检测。最好直接传原始字符,不要自己二次编码。最后一个也是最重要的:所有操作在靶场或授权环境里进行,学习漏洞利用不是为了打真实系统,而是为了理解边界条件。CTF 的价值在于通过模拟题建立对漏洞的直觉,这种直觉在做代码审计和漏洞挖掘时非常有用。
5.3 学习拓展:从 CTFHub 通关到发现真实业务里的 SSRF
刷完 CTFHub 这套题之后,我建议你再做两件事来巩固:第一,把 SSRF 从“利用”角度翻到“防御”角度,想一想如果让你设计一个 URL 参数接口,要怎么避免被这样绕过——限制协议、做 DNS 解析验证、禁用重定向、封锁内网 IP 段、最小化服务端网络权限,每一条能不能在 CTFHub 的题目里找到对应的绕过方式,反过来理解会更深刻。第二,尝试把 PHP 代码审计里的 SSRF 漏洞场景拿到本地环境里复现一遍,改几行过滤规则,观察哪个绕过手段失效、哪个依然有效,这样你能真正掌握“过滤的本质是对字符串的判断,而请求的本质是对地址的解析”这句话。
我自己刷这套题最大的体会是:SSRF 的难度不在于记住多少条 payload,而在于理解“服务端发起请求”这个行为的完整链路——用户输入、代码拼接、解析器理解、库函数执行、协议层请求、内网路由,每一个环节都有可能被利用,也有对应的防御手段。把这条链路想通了,CTFHub 技能树里这些关卡就不再是一道道孤立的谜题,而是一幅完整的 Web 安全地图。
最后分享一个实用的小技巧:做题时始终开着抓包代理工具,并且把每一道题的请求包、响应包、payload 都记录到一个本地笔记里。我刷完这套题后回头翻笔记,发现很多当时卡住的点,在笔记里都有一两句“为什么会这样”的记录,这比刷题本身更有积累价值。希望这篇 CTFHub SSRF 通关教程能让你少走一些弯路,也欢迎按这套思路去技能树里逐个验证。