首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
PHP弱类型比较与MD5绕过:从easy_md5看CTF代码审计核心套路
📅 2026/9/29 15:23:48
✍️ 爱科研究院
👁 阅读 3,247
SWPUCTF 2021 新生赛的 classic 题目 easy_md5我到现在还记得当时群里一帮人对着它折腾到凌晨。原因很简单这题名字里挂着“easy”实际上一进来就给你个 PHP 文件源码里面就一行核心判断md5($_GET[a]) md5($_GET[b])。你要是真以为是考 MD5 加密去算哈希找碰撞那基本就走进死胡同了。CTF 入门阶段最容易翻车的点就在这你以为它考密码学其实它考的是 PHP 语言的“性格”。这道题适合所有刚入坑 Web 方向、开始接触代码审计和 PHP 弱类型比较的新手也适合想系统梳理 MD5 绕过家族的选手当作复习提纲。本文就围绕这道题把弱比较、数组绕过、0e 魔法串、强比较破局这些套路全部拆干净顺手给你一份可以直接照着抄的实操记录和避坑清单。1. 题目拆解easy_md5 到底在考什么1.1 从题目名字看命题人的心思新生赛的名字一般都很“诚实”easy_md5 说它考 MD5 真没骗你但考的不是 MD5 的算法细节而是 MD5 在 PHP 里被拿来比较时产生的那些奇奇怪怪的行为。这类题目的标准“长相”是这样页面上有高亮源码你往里传两个参数a和b后端核心逻辑类似下面这样?php error_reporting(0); include flag.php; highlight_file(__FILE__); if (isset($_GET[a]) isset($_GET[b])) { if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b])) { echo $flag; } } ?这里有两个条件一是$a和$b的值本身不相等二是它们的 MD5 结果用比较时相等。如果你没有意识到和的区别脑子里第一个方案就是找 MD5 碰撞。但真正的 MD5 碰撞需要构造特定二进制内容在 URL 参数里可不好传——再说了题目名叫“easy”命题人想要的显然不是那种硬核解法。所以这道题真正的考点就两条线PHP 弱类型比较和PHP 对数组参数的异常处理。两个思路都能独立击穿这个判断逻辑它俩的共同点是“不碰 MD5 算法本身”而是利用 PHP -engine 层面的设计疏漏。1.2 PHP 的和看起来像骨子里完全不同很多新手写过 PHP 但没认真想过的规则。是松散比较比较前会做类型转换是严格比较类型和值都得一致。问题就出在那个“自动类型转换”上。用一个生活化的类比就像你问两个人“你们工资一样吗”只要数值对齐就算一样哪怕一个人领的是人民币一个人领的是美元则是“你俩不仅数字一样币种还得一样”。PHP 的会尝试把两侧变量转成同一种类型再比于是出现了很多反直觉的案例比如0e123 0e456这样的字符串比较时PHP 会把它们都按科学计数法解释成 0于是两个完全不同的字符串就被判定为相等。这正是 CTF 里大名鼎鼎的“0e 绕过”。MD5 值是 16 进制字符串如果某个字符串的 MD5 长成0e...开头后面全是数字PHP 在比较时会把它当浮点数 0 处理。两个不同的字符串各自的 MD5 都以0e开头时md5($a) md5($b)就变成0 0直接成立。网上一搜能查到一大堆这类“魔法字符串”比如最经典的QNKCDZO它的 MD5 是0e830400451993494058024219903391正好满足条件s878926199a的 MD5 是0e545993274517709034328855841020也是经典选手。这类字符串不需要两个都存在只要其中一个以 0e 开头和另一个同类的组合就能绕过。1.3 MD5 拿到数组时的“罢工”行为另一条路线更无赖直接给a和b传数组。PHP 里md5()这个函数要求参数是字符串你给个数组它会直接报错并返回NULL这时候关键点来了md5(array()) // PHP 7.x 下返回 NULL NULL NULL // 成立所以只要md5($_GET[a]) md5($_GET[b])两边都是NULL比较自然成立。同时$_GET[a] ! $_GET[b]因为一个是数组一个是不同内容的数组也成立。几个条件全满足flag 就到手了。我为啥一直强调 PHP 7.x因为 PHP 8 对数组传给标量函数的行为改了直接抛TypeError整个脚本直接崩不再返回NULL。CTF 赛题为了兼容经典解法通常跑 PHP 5 或 PHP 7但如果比赛平台环境比较新你会发现数组大法“失灵”了这就是后面要专门聊的坑。2. 绕过原理一句弱比较为什么能被玩出花2.1 0e 魔法串科学计数法下的一堆“零”先看第一种解法的完整原理。PHP 的字符串比较有个 hidden 规则如果两个字符串都能被当作数字解释——比如一个是0e123一个是0e456——就会先转成数字再比较。0e123的含义是0 乘以 10 的 123 次方结果还是 0。0e456一样是 0。于是两个完全不同的字符串被认定相等。在 MD5 场景里我们要的是某个字符串哈希后恰好以0e开头、且第 3 位之后全部是数字。因为 MD5 输出是 32 位十六进制字符0e后面如果出现a~f就坏事了——那就不再是“纯数字科学计数法”了。想让某个字符串的哈希撞上这种形态靠手算不现实网上流传的常用串直接用就行。我自己整理过一份常见的可直接用于 CTF 的 0e 串列表这类内容在很多 writeup 里都能看到建议收藏一份原始字符串MD5 值QNKCDZO0e830400451993494058024219903391s878926199a0e545993274517709034328855841020s155964671a0e3427684168224515249741172544692406107080e462097431906509019562988736854s214587387a0e848240448830537924465865611904构造请求时任选两个不同的串分别塞给a和b比如/?aQNKCDZObs878926199a页面直接返回 flag。整个过程完全不依赖算法碰撞是纯“类型特性”解法。这里有个容易被忽略的细节0e绕过能否成功取决于 PHP 是不是把两侧都当数字字符串处理。如果题目代码变成了md5($a) md5($b)严格比较这套就完全失效因为0e... 0e...对两个不同哈希来说永远是 false。这也是为什么很多题目表面上像 easy_md5实际代码里把等号换成三个难度直接抬升一截。2.2 数组绕过让函数直接“空手而归”数组绕过的核心价值在于它不止能打在某些情况下连都能打。原理很简单PHP 7 及以前给md5()传数组会触发 warning但脚本不会停止函数返回NULL。于是md5($_GET[a]) - NULL md5($_GET[b]) - NULL NULL NULL - true NULL NULL - true // 强比较也成立这段逻辑非常有意思很多新手认为数组绕过只能欺负弱比较但实际上因为NULL NULL为真遇到强比较的题只要 PHP 版本是 7 以下数组法依然能打穿。前提是另一个判断条件$_GET[a] ! $_GET[b]或者!也得过。如果两边都传空数组a[]b[]那$a和$b完全一样第一个条件直接 fail正确的姿势是传不同内容的数组/?a[]1b[]2这时候a数组是[1]b数组是[2]它俩肯定不相等但 md5 结果都是NULLflag 顺利拿到。我经常看到有人在这个细节上翻车直接传a[]1b[]1然后发现第二个条件过了第一个条件不过。记住数组内容不同才是正确姿势。如果你不确定目标 PHP 版本0e 串在某些环境里可能因为php.ini的md5()行为变更失效数组法在高版本下又直接崩所以实战里最好两种都试哪个响应不报错就用哪个。2.3 两种解法怎么选从题目特征反推环境比赛现场没有“试错成本”的概念你可以同时验两套 payload。我的经验是先把数组法丢上去如果页面白屏或者 500八成是 PHP 8 环境立刻转 0e如果页面正常返回 flag那就是 PHP 7 老环境数组法更稳。0e 串的好处是不挑 PHP 版本5、7、8 都能跑的字符串转数字逻辑一直在坏处是一旦目标代码用了它立刻失效数组法在 PHP 7 下通吃两种比较但在 PHP 8 下直接死。两套互为补充组合使用覆盖绝大多数题面。3. 完整通关实录从读题到拿到 flag3.1 第一步打开页面先看源码SWPUCTF 这类新生赛题目通常开着highlight_file(__FILE__)也就是源码直接高亮显示在页面上。你不需要任何扫描器打开题目页就等于拿到答案纸。第一步要做的就是逐行看条件别急着传参。我见过太多人上来就admin、password一顿乱试这种习惯在 CTF 里非常浪费时间。源码里写着include flag.phpflag 就在服务器上你只需要让那个 if 判断通过所以目标就一个字让两根参数满足“值不同但哈希值相等”。如果页面没有直接显示源码右键查看源代码也不会看到 PHP 代码它已经被执行了。这种情况通常需要结合备份文件、源码泄露或参数注入去读文件但 easy_md5 不会搞这么复杂题目敢叫 easy就说明入口就是高亮源码。3.2 第二步构造 Payload多套方案轮着试明确了目标以后直接构造请求。最省事的方式是浏览器地址栏但为了方便记录和后续修改我更推荐用 curl 或 Burp Suite。比如用 curlcurl http://目标地址/?aQNKCDZObs878926199a如果拿到 flag题目结束。如果返回空白或报错换数组套路curl http://目标地址/?a[]1b[]2这里面有一个动作很多人会忽略观察响应的差异。页面返回空白不等于失败有的题目把 flag 藏在响应头里有的藏在注释里有的是直接输出但被前端样式隐藏了。所以建议用 curl 的时候加个-v看完整响应或者直接看 Burp 的 Raw 报文。我习惯把每次请求保存到历史里方便对比到底哪一步开始出现异常。3.3 第三步验证输出顺手记下判断依据拿到 flag 之后别急着关页面。作为新手建议做一次“逆向复盘”把?aQNKCDZObQNKCDZO这种相同参数试一下你会发现第一个条件$a ! $b不满足输出为空这能帮你反向验证代码逻辑再试?a[]1b[]2体会数组报错和 NULL 相等的过程。这些动作看着多余但对建立“漏洞触发链路”的直觉特别有帮助。我接手的很多入门学员就是卡在“看 writeup 全会自己动手全废”。核心原因就是只看结论不验证过程。你在本地搭个 PHP 7 环境把题面代码跑一遍把两种 payload 分别在 PHP 7 和 PHP 8 下试一次十分钟就能彻底吃透这个知识点的所有边界条件。本地环境用php -S 127.0.0.1:8080起个服务配合浏览器改参数比在线平台更容易观察 warning 和错误日志。4. 常见问题与排查技巧实录4.1 0e 串打不动先看等号和 PHP 版本遇到过不少读者反馈“QNKCDZO 和 s878926199a 都试了flag 就是不出来。”这种情况最可能的原因有三个。第一个是题面代码用了而不是0e 绕过直接失效得换思路第二个是 PHP 8 环境对字符串和数字比较的行为做了调整部分弱比较场景下0e串不再被当成 0第三个是目标可能对参数值做了过滤比如检测QNKCDZO这种特征字符串。排查方法很简单看标题或者题目描述里有没有“PHP 8”之类的提示看报错类型如果是TypeError妥妥高版本如果题目源码里明确写着那就赶紧放弃 0e 改数组或真碰撞。如果三者都不是还有个大概率原因——参数名不是a和b。很多题目会把参数名改成x、y或者v1、v2你拿着通用 payload 直接打当然没效果。务必以源码为准。4.2 数组传参报 500多半是 PHP 8 在制裁PHP 8.0 之后内部函数对参数类型要求更严格md5([])不再是返回 NULL而是直接抛TypeError程序中断于是你看到 500 或者一片空白。这个问题在新生赛旧题里不常见但在一些新平台或者复现老题目的自定义环境里还真能碰到。遇到这种情况先切 0e 串如果目标又是强比较那就只能走真正的 MD5 碰撞了。好在 CTF 题面一般不会把难度拉这么高真遇到强比较的新生赛通常还有别的线索——比如题目本身给了某种限制让你可以控制$a和$b为对象利用__toString等魔术方法绕过去。对于 easy_md5 来说不会走到那一步但如果 PHP 8 频繁出现建议本地直接装个 PHP 7.4 用 Docker 复现省时省力。4.3 参数被 WAF 或过滤拦了怎么办有些平台会对请求做简单过滤看到[]就直接拦。这种情况优先尝试 URL 编码a%5B%5D1b%5B%5D2。PHP 解析后依然能得到数组。如果过滤的是某些特定字符串比如QNKCDZO可以试大小写变形或者换一组 0e 串比如s155964671a这种冷门一些的。再不行用 Burp 的 Intruder 批量拉一个 0e 串字典跑一轮几千条常见串几十秒就能过完。但注意乱跑字典前先确认题目环境能接受高频请求。有些线上靶场对单 IP 有速率限制扫太快会被封。我一般先手工验证两三个 payload确定方向没错再上工具上来就全量爆破是新手最爱的错误姿势。4.4 问题速查表遇到情况直接对照现象可能原因解法0e 串无效代码用的是改用数组法或真碰撞数组法报 500/TypeErrorPHP 8 环境改用 0e 串传空数组不弹 flag$a $b导致第一个条件失败传不同内容a[]1b[]2页面一直空白参数名与源码不一致看源码确认a、b实际叫什么flag 有输出但被截断页面布局遮挡/编码问题curl 查看原始响应检查响应头报错里出现syntax error手滑把[]写错位置确认a[]1中无多余空格5. 举一反三从这道题看懂整个 MD5 考点家族5.1 变体一这里是强比较的“”怎么办如果把 easy_md5 的条件改成if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b]))这就是更高级的“强比较 MD5”题。PHP 7 环境下数组法依然能打穿因为NULL NULL成立但 PHP 8 下就得用真正的 MD5 碰撞。你可能听过fastcoll这个工具它能生成两个内容不同但 MD5 完全相同的二进制文件然后把内容传给参数即可。注意二进制碰撞串在 URL 里直接传会乱码通常需要配合POST表单或者写脚本用 requests 发。这也是为什么碰到强比较别急着关先去试数组数组失效再考虑碰撞难度阶梯就这么一路往上爬。顺带提一个常见套路强比较 is_string($a)限制。如果代码先检查参数必须是字符串数组法直接出局只能碰撞。这时候可以关注题目有没有“文件包含”入口把碰撞二进制写进文件再读出来属于进阶玩法了。5.2 变体二嵌套的md5(md5($a))怎么处理有些题会把比较写成md5(md5($a)) md5(md5($b))以为多套一层就安全了。实际上数组法依然适用md5(md5([1]))内层md5([1])返回 NULL外层md5(NULL)实际上是md5()两边结果一样比较照样成立。这类嵌套题验证起来特别快你把a[]1b[]2丢上去通常直接过。0e 串在嵌套场景下就不太好使了因为你得找一个串让它的 MD5 值再 MD5 一次后依然以 0e 开头这种“二次 0e”字符串比较稀有网上有跑好的字典但没必要背。所以遇到嵌套第一优先级永远是数组法。5.3 从题到知识体系MD5 考点还能怎么考easy_md5 本质上是 PHP 代码审计里的一个“点”但它串联出来的知识面非常典型。以 MD5 为核心整个考点家族包括普通弱比较0e 串、数组返回 NULL、强比较真碰撞、魔术方法触发__toString里调 md5、变量覆盖导致$a值可控、反序列化配合 md5 校验等。每一类都能在 SWPUCTF 和各类 CTF 练习平台找到对应题目。如果你想系统练这块我建议按这个顺序刷这道 easy_md5把弱比较和数组基础打牢再找一道强比较的题比如用的变体逼自己研究fastcoll和 requests 脚本找一道变量覆盖或参数覆盖的题理解extract($_GET)这种函数如何破坏判断逻辑最后去刷反序列化相关的 md5 利用把魔术方法串联进来。至于工具平时做题建议备上“随波逐流 CTF 编码工具”这类合集、本地 PHP 环境、Burp Suite 和一个能写脚本的 Python 环境。MD5 校验工具也别落下不只是做题平时下载文件验证完整性也会用到。练习平台方面除了 SWPUCTF 对应的随波逐流社区还有 buuoj 等热门平台都有大量同款题目。ctfshow 上也能搜到衍生题适合批量刷。最后分享一个我的个人习惯每次做这类“看似简单”的 Web 题我会特意把 payload 的所有变体都跑一遍包括失败的组合。因为新手阶段最容易出问题的地方不是不知道解法而是不知道解法为什么有效、什么时候失效。你把和在 PHP 7、PHP 8 下的行为差异亲手验证一遍把 0e 串和数组法各自的应用边界摸清楚以后再遇到任何 MD5 相关的题目你都不会再被“easy”两个字欺骗。这个知识点的性价比在 CTF Web 入门阶段数一数二花两个小时彻底搞懂能帮你省下以后无数个凌晨。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/9/29 15:23:48
OpenHarmony Flutter插件适配实战:以get_ip_address为例实现MethodChannel链路
2026/9/29 15:23:48
鸿蒙Flutter定制阅读器:epubx解析引擎适配与实践
2026/9/29 15:23:48
DeepSeek家庭辅导实战:从提示词设计到避坑指南
2026/9/29 16:13:58
Rockchip RKISP驱动深度解析:从设备树绑定到AE算法定制
2026/9/29 16:13:58
AI 1:1复刻网页UI:从截图到前端代码的完整流程
2026/9/29 16:13:58
WinForm SQL真分页控件:OFFSET/FETCH+UI状态同步实现
2026/9/29 16:13:58
数据总线上的实时计算:ZCBUS如何实现政务数据按需分发
2026/9/29 16:13:58
微信小程序高校选课系统毕业设计:从数据库设计到并发控制全解析
2026/9/29 16:08:58
堆的三种真面目:数据结构、内存管理与数据库堆表
2026/9/29 0:02:32
开源模型端侧落地实战:量化、推理加速与Agent上下文管理
2026/9/29 0:02:32
AI Evals实战指南:从零搭建LLM应用评估体系与CI/CD集成
2026/9/29 0:02:32
Java采购管理系统实战:从数据库设计到事务一致性
2026/9/29 11:29:08
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
2026/9/29 13:01:36
新手入门看这篇:建设网站加盟避坑指南与SEO实操
2026/9/29 14:07:33
论文AIGC疑似度是什么意思?想查论文AI率有哪些免费工具?