最近在带新人刷 ctf.show 的 Web 关卡,到 web5 的时候,队里两个小朋友同时卡住了。问了一圈,他们的第一反应都是:“网上说传两个参数就能出 flag,但我把参数名抄回去还是不行。”你看,这不就是典型的“只会抄作业,不会看题目”嘛。
web5 其实是很多 CTF 新手从“跟着教程点”过渡到“自己动手试”的一道分水岭。关卡本身不复杂,但它是少数能一次性把“PHP 弱类型比较”这个高频考点讲明白的入门题。这篇不打算只贴一句 payload,而是把你做这题时需要理解的判断逻辑、绕过原理、以及以后遇到类似题怎么举一反三,完整拆开讲清楚。
1. web5 卡人卡在哪:页面倒是简单,脑子没跟上
ctf.show 上 Web 方向的前几关,基本属于“友好热身”:有的一看就是改 URL 参数,有的是让你看响应头,有的是直接读源码里的注释。到了 web5,页面几乎不给任何多余提示,源码里也只有一段很短的 PHP 判断逻辑,很多新人就是在这里开始蒙的。
1.1 打开之后你大概会看到什么
一个典型的 web5 题目,页面通常就一行输出,类似 “no flag”。如果你按 F12 看源码,会发现后端逻辑简化为类似这样:
<?php include 'flag.php'; $a = $_GET['a']; $b = $_GET['b']; if ($a != $b && md5($a) == md5($b)) { echo $flag; } else { echo "no flag"; }注意,不同版本题面给的具体变量名可能不一样,有的叫v1、v2,有的叫key1、key2,有的甚至直接写成$_POST。但判断逻辑基本就是这一句话:两个参数不相等,但它们的 md5 值要相等。
这一下就把很多人干沉默了。两个不同的字符串,md5 结果怎么可能相等?讲道理,真正的 md5 碰撞是可能的,但需要构造复杂的二进制文件,不可能让你在浏览器地址栏里随手敲出来。
1.2 “并不存在的相等”是怎么成立的
如果后端用的是严格比较===,那你会彻底卡住。但题目用的是==,这就是漏洞的入口。
在 PHP 中,==是松散比较,它比较的是“值”,而且比较之前会先做类型转换。比较规则里和这道题强相关的有两条:
- 如果一个字符串和数字比较,字符串会被尝试转换成数字。
- 如果一个字符串形如科学计数法,例如
"0e123",PHP 会把它当作0 * 10^123,也就是数字 0。
于是你构造两个 md5 值都以0e开头的字符串,它们在==面前就都成了数字 0,自然“相等”。
这个玩法在 CTF 圈里叫“0e 绕过”,也有人叫“魔幻 Hash”。关键不是真的让 md5 内容一样,而是让比较结果一样。
1.3 第一次见到“0e字符串”的直觉反应
我第一次做类似题时,第一反应是“怎么可能有这种字符串”,直到我亲手在终端里敲了这两行:
php -r "echo md5('240610708'), PHP_EOL;" php -r "echo md5('QNKCDZO'), PHP_EOL;"输出是这样:
0e462097431906509019562988736854 0e830400451993494058024219903391两个值都以0e开头,后面全是数字。PHP 做==比较时,认为左边是0 * 10^462...,右边是0 * 10^830...,都是 0,于是判定相等。我第一次看到的时候,脑子里只有一个想法:这语法糖也太毒了。
2. 完整解题链路:从读源码到拿到 flag
既然知道了核心考点,下面就是我建议新手完完整整走一遍的操作链路。每一步都不要跳,这对你以后刷别的题会有帮助。
2.1 第一步:把“黑盒”变“白盒”
千万不要一上来就乱传参数。先把页面源码、响应头、可能存在的注释全部撸一遍。web5 这类题,源码范围很小,直接右键查看源码就能看到后端主要逻辑。如果题目把include的文件藏起来,也别慌,一般 flag 文件就在同目录。
拿到源码后,把关键判断摘出来,翻译成人话:
- 网页给你两个参数位
a和b。 a不能等于b。- 但
a的 md5 和b的 md5 必须相等。
这就是完全信息破解。你看不到的内容,逻辑已经告诉你该怎么走了。
2.2 第二步:选定最快能验证的 payload
如果你已经背过常见的 md5 0e 字符串,那这里可以直接用两组经典值:
a=240610708 b=QNKCDZO完整的 URL 是这样:
http://your-target.com/web5/?a=240610708&b=QNKCDZO我为什么强调“背过”而不是“现场算”?因为现场算很难算出来。这两个值属于 CTF 高频常客,建议直接记牢。除了这两个,还有s878926199a、s155964671a、s214587387a等等,它们 md5 后同样都是0e开头。
如果你不想背字符串,还有另一个更通用的绕过方式:把参数以数组形式传进去。比如:
http://your-target.com/web5/?a[]=1&b[]=2原理是:md5()无法处理数组,会报错并返回null,而null == null是成立的;同时$a != $b也成立,因为一个是array(1),一个是array(2)。这个技巧在某些题目里更稳,因为它不依赖具体字符串,只要你确认后端没有专门过滤数组类型。
2.3 第三步:在浏览器里验证前,先想清楚会发生什么
很多新手拿到 payload 就往地址栏里一贴,发现没反应,然后开始怀疑人生。其实你只需要在脑子里过一遍:
- 请求发出去后,服务器执行
md5($a)。 - 如果参数是
240610708,md5 结果就是0e...。 - 再执行
md5($b),结果也是0e...。 - 两个值在
==比较下相等。 - 于是进入输出 flag 的分支。
如果能自己把这一步说清楚,说明你是真的理解了,而不是碰运气。再把 URL 粘贴进浏览器或 curl 里,结果就应该是 flag:
curl "http://your-target.com/web5/?a=240610708&b=QNKCDZO"2.4 第四步:如果 payload 没生效,按三条线排查
不是每次都能一把过,特别是当你自己做题,而不是对着现成 writeup 抄的时候。payload 没生效,我建议按顺序排查:
- 参数名对没对上?题目用的是
a和b,你传的是v1和v2,必然失败。先回源码确认变量名。 - 请求方式对不对?有些关卡会把参数放在 POST 里,你用 GET 发,后端收不到。
- 是不是
===严格比较?如果源码写的是md5($a) === md5($b),那0e绕过直接失效。遇到 PHP 7 以上版本、又使用===的场景,你得用“数组 null 比较”或者找真正的 md5 碰撞二进制。说实话,真正的碰撞在 Web 题里很少考,因为太麻烦,更多是让你走数组绕过。
这三条基本覆盖了新手 90% 的失败原因。
3. 别只记 payload,你得理解“弱类型”为什么会成为真实漏洞
如果只是把这道题抄完,那价值不大。web5 真正的价值在于,它让你打开了一扇门:原来一个宽松的等号判断,就能把一个严格的安全检查变成摆设。
3.1 弱类型在现实代码里的危害
你可能觉得md5($a) == md5($b)这种写法只出现在 CTF 题里,太假了。但现实里的危险程度一点都不低。
举一个我印象很深的例子:有些网站把支付金额、折扣比例、积分余额等参数从前端接收后,直接用==和数字比较。如果你传入一个字符串,PHP 会做隐式转换,比如:
if ($_GET['money'] == 0) { // 理论上这里不该进,但如果你传入 money=abc }"abc"在 PHP 8 之前的版本会被转换成 0,于是这个字符串就和数字 0 相等了。攻击者不需要知道任何密钥,只要传入一个非数字字符串,就可能把一个“应为非零”的判断变成“等于零”。
还有一个常见场景是登录校验:
if ($input_password == $db_password) { // login success }如果$db_password是"0e123..."这类特殊字符串,而$input_password也被构造成同型字符串,密码校验就可能被绕过。虽然真实系统中密码哈希几乎不可能恰好算出0e开头,但审计代码时只要看到==比较哈希值,就值得多盯几眼。
3.2 从 web5 延伸出来的几类兄弟题型
搞懂 web5 之后,你会发现同族题目一抓一大把。我总结过几种最常见的变体,你在别的 CTF 平台也会反复见到:
| 变体思路 | 典型判断 | 常见绕过 |
|---|---|---|
| 0e 字符串绕过 | md5($a) == md5($b) | 传已知0e字符串 |
| 数组绕过 | md5($a) == md5($b),但参数能传数组 | a[]=1&b[]=2 |
| 数字字符串比较 | $num == "0" | 传abc或0e1 |
| JSON 中的宽松比较 | 判断前没做类型校验 | 利用 JSON 中的数字类型误导 |
| 反序列化后比较 | 对象属性值比较 | 构造等价对象 |
表格里的每一种,背后都是同一个底层问题:开发者在“比较”这件事上不够严格。
3.3 拿到 flag 后,自己给自己出三道思考题
做题最忌讳“对完答案就关页面”。我建议你给自己留几个问题,逼自己把原理内化:
- 如果把
==改成===,这道题还能用0e绕过吗? - 如果输入的是数组,
md5()报错会不会打断输出?为什么在这个场景下报错反而帮了你? - 假如题目要求你做一个“严格相等”的 md5 碰撞,你需要准备什么样的工具链?
能把这三个问题都答清楚,web5 才算出师。
4. 从 web5 往前看:后续 Web 关卡的推进路线和建议
ctf.show 的 Web 关卡,从 web1 到 web5 是一个很清晰的递进:先学会传参,再学会看响应,然后开始碰 PHP 代码审计和弱类型。过了这一关,后面会遇到更难啃的骨头——比如文件包含、SQL 注入、命令执行、变量覆盖。这些不是孤立考点,而是需要你不断积累“读代码 + 试 payload + 看现象”的循环。
4.1 建议的练习清单
如果你刚刷完 web5,想继续在 Web 方向深耕,我建议按下面这个顺序往下练:
- PHP 伪协议:
php://input、php://filter、data://,先搞懂它们什么时候能用、怎么用。 - 文件包含漏洞:从本地文件包含到远程文件包含,理解
include和require的危险半径。 - SQL 注入基础:先手工测出闭合方式,再用 sqlmap 验证结果,不要只会工具一把梭。
- 命令执行:重点练空格过滤、关键字过滤、编码绕过的组合技。
- 变量覆盖:
extract()、$$、parse_str这些函数如何改变变量值,进而改变判断结果。
这套顺序基本对应 ct f.show Web 系列从入门到进阶的主线。
4.2 几个能提升效率的小工具
做题终归要落地到操作。我的习惯是浏览器 + curl + Burp Suite 三件套:
- 浏览器用来快速看页面、看源码、看响应头。
- curl 用来验证各种手动构造的请求,尤其是批量测试参数时比重开浏览器快得多。
- Burp Suite 用来抓包改包,处理需要登录态或跨请求的复杂场景。
遇到 web5 这种一眼能读源码的题,其实 curl 就够了。你可以把基础命令写成一个 shell 别名,比如:
alias ctfget='curl -s -L --max-time 10 -A "Mozilla/5.0"'这样每次发请求不用重复敲一长串。
4.3 先看现象还是先读代码?我的选择
“拿到题先干嘛”这个问题,我听过很多答案。有人喜欢先扫目录,有人习惯先点按钮,但如果是像 web5 这样的代码审计题,我的建议永远是:先读代码,再动手试。CTF 练习阶段,读源码的成本最低,收益最高。题目设计的每个判断逻辑,都是在给你划考点。与其拿 payload 瞎打,不如先把自己当成一个“代码评审员”,把考点圈出来,再去找对应的绕过技巧。
这不是说扫描和枚举没用,而是在 Web 入门阶段,代码审计是性价比最高的路线。你每看懂一行后端逻辑,就能预判下一步该传什么参数。
4.4 做题节奏:别再当“flag 搬运工”
最后聊一个我很想强调的点。新手最大的问题不是不会做,而是太想“抄到 flag”。一旦页面出现 flag,就觉得任务完成了,关掉浏览器,下周另一道题换个马甲又不认识了。这种“flag 搬运工”式刷题,刷一百道也进步有限。
我个人的习惯是:拿到 flag 之后,多问自己三个问题——这题考的是什么函数?这个函数在什么真实业务场景里会被滥用?如果我是开发者,该怎么写出安全的代码?
web5 这关特别适合做这个思维训练,因为它短小、考点清晰、可扩展性强。你把弱类型比较这个点吃透了,再去看别的平台上的“哈希比较绕过”“字符串 0 比较”类题目,基本就是一眼识破。
我自己在带新人时,特别愿意让他们记住的一句话是:“CTF 题目只是给你一个引子,真正的战场是你未来遇到的那段没有经过加固的业务代码。” ctf.show 的 web5 就是一个合格的引子,它不至于让你劝退,又足够让你第一次意识到,代码里一个不起眼的==,在特定条件下能颠倒黑白。
如果你刚好也卡在这一关,希望上面的思路能帮你把这一步走得稳一点。做完之后,别忘了亲手在控制台跑一遍 md5 那几个0e字符串,看见输出那一刻,你对这个知识点的记忆会深很多。