资讯动态

PHP特性CTF刷题实战:弱类型比较、MD5绕过与变量覆盖总结

发布时间:2026/10/20 6:17:58 来源:尧图企业网站定制
从朋友那看到一张CTFshow刷题进度截图他刷到WEB-PHP特性89-115这段时卡了两星期来问我“是不是越往后越吃天赋”。我的回答是这段题不吃天赋吃“对PHP默认行为的熟悉度”。我自己刷完上篇89-115之后最大的感受就是看起来每道题换了个马甲但翻来覆去考的仍然还是弱类型比较、MD5绕过、变量覆盖、正则过滤那几座大山。这篇文章就把这一段的刷题心得、踩过的坑、总结出的通用套路完整记录下来给正在刷同一段位或者准备刷CTF Web方向的朋友一个可供直接复用的参考。1. 起点89到115这段到底在刷什么1.1 题位定位从扫盲题到特性题的过渡段CTFshow的WEB题前面有一大段是给新手适应环境的点开就是源码、改个参数、看个响应头基本属于“把工具用熟”的阶段。到了WEB-PHP特性这个系列题号从89开始拐了一个大弯题目真正开始考“PHP语言本身的坑”。这个区间不是让你背函数手册而是让你亲眼见识一次“一个没写严格的能让整个鉴权形同虚设”“一个md5函数在数组面前原地摆烂”之类的真实场景。我自己的体感是89到115算是整个PHP特性系列里比较醇厚的一段。它不像前面的题那样单点考察不少题是两三个知识点叠在一起考比如先给你一段正则过滤过滤完了又要求执行命令“既要绕过正则又要凑合法调用”这种复合题往后只会越来越多。所以这个区间最值得花时间。1.2 刷完能带走哪些技能清单这一段刷干净以后至少要在脑子里留下这么几样东西否则大概率是白刷技能点具体场景关键函数/机制弱类型比较字符串和数字比大小、数字字符串转换规则科学计数法绕过让md5值以0e开头0e开头的16位数字字符串数组绕过md5、sha1比较相等md5(array)返回NULL变量覆盖用GET参数改写变量extract、parse_str、$$正则过滤器绕过preg_match过滤字母数字数组参数、换行锚点、回溯上限伪协议读取源码include处查看文件内容php://filter、php://input这不是一张平铺的技能表而是一条循环链路读源码、找判定、找过滤、选绕过、拼payload。刷到后面你会发现每道题就是在这条链路上挑几个点做文章。2. 弱类型比较这段题反复埋的“”坑2.1 和的差别以及字符串转数字的隐藏规则先直白说结论是比较“值”比较时会做类型转换是连类型带值一起比。PHP里这道坎几乎每条WEB题里都有机会出现89到115这段更是被我踩了个遍。最容易踩的坑是“非数字开头的字符串在比较时被当成0”。在PHP 7及以后如果拿一个字符串去和数字做比较PHP会尝试把字符串转成数字。转换规则是看字符串开头如果开头是数字就截取到非数字为止如果开头就不是数字那整个字符串就是0。所以下面这段代码在CTF里非常经典if ($_GET[num] 0 !is_numeric($_GET[num])) { echo $flag; }这里$_GET[num] 0要求传入的值等于0!is_numeric()又禁止它是数字。普通思维会卡住因为0传进去是数字is_numeric(0)为true直接进不了第二个条件。但如果你传numabcabc 0这个判断在PHP里成立因为字符串“abc”转成数字就是0而它又不是一个数字字符串两个条件全部满足。同理还要记住另一种形态10abc 10是成立的PHP会截取开头的数字部分10来参与比较。如果题目里有“输入一个数让它等于某值但又不能用纯数字”这种表述这招就是万能钥匙。2.2 0e科学计数法0的另一种写法如果说字符串转数字是靠PHP的“宽容”取胜那0e字符串就是弱类型比较里最被人熟知的一颗明星。PHP在处理0e123这种字符串时会把它当成科学计数法0乘以10的123次方还是0。所以0e123 0是true。这道题的难点不在于理解科学计数法而在于找到一个能让md5值正好落在“0e开头且后面全是数字”的字符串。网上已经有公开的“0e串甜点库”最常出现在题目里的几个如下字符串哈希函数结果特征240610708md50e462097431906509019562988736854QNKCDZOmd50e830400451993494058864219903391s878926199amd50e545993274517709034328855841020aaroZmOksha10e66507019969427134894567494305185566735这里的原理是md5或者sha1的输出恰好以“0e”开头且e后面全部是数字PHP解析科学计数法时就会误认为数值0。刷这段题之前把这些能背的背下来遇到“md5值相等但不等于原字符串”或者“让md5结果0”的题目直接填入即可。不过要注意如果源码里写的是而不是这招就失效了。因为比较的是字符串本身0e462... ! 0e830...两边内容不一样就是false。所以看到判断符之前一定要先确认是单等号还是三等号。2.3 in_array、strcmp这些函数自带的松散陷阱单纯的比较已经是老熟人了很多题还会把比较逻辑藏在函数里让你看得不那么明显。经典代表是in_array()它的第三个参数控制是否严格比较默认情况下不传就是松散比较底层同样走。这就导致in_array(abc, [0])返回true因为“abc”被转成0。另一个高频陷阱是strcmp()。strcmp($a, $b)用来比较两个字符串相等时返回0。但如果你传进去的是数组这个函数会直接返回NULL。在PHP的弱类型规则下NULL 0成立。所以遇到类似下面这种代码$pass $_GET[pass]; if (strcmp($pass, $flag) 0) { echo getFlag(); }直接传pass[]1让strcmp返回NULL绕过条件。这个坑的本质是写代码的人默认参数一定是字符串但攻击者永远可以传一个数组过去破坏类型假设。2.4 数组与NULL的奇妙组合数组在弱类型里是另一种“万能怪”。array() 0不成立但count(array())可以数出0更常见的是md5(array())、strcmp(array(), x)这类函数直接返回NULL。在PHP 8之前数组传给非数组参数会产生Notice警告或者强制转换但返回值往往是NULL或者false这类“弱值”。所以刷这一段我给自己定了一条原则只要看到任何函数被套在左边或者右边先尝试传一个数组过去看函数表情。很多看起来“非爆破不可”的题传数组以后题目直接白给。这条经验在后一半题里救了我好几次。3. MD5绕过三件套高分频段的固定三连3.1 数组绕过最无脑但最实用代码里写md5($_GET[a]) md5($_GET[b])还想限制a ! b这是出题人给新手埋的最常见“劝退点”。很多人第一反应是找两个字符串让它们的md5完全相等然后陷入寻找碰撞的深渊。实际上PHP里md5()接收数组参数时直接返回NULL所以只要让a[]1b[]2两边都是NULLNULL NULL成立a ! b也成立一行payload直接通关。if ($_GET[a] ! $_GET[b] md5($_GET[a]) md5($_GET[b])) { echo flag; }这里我要特别提醒有些题用的是$_POST而不是$_GET传数组的方式对应改掉即可。另外sha1()也是一样的尿性数组传参返回NULL遇到sha1判断能否相等同样可以数组绕过。不看函数名只看它是否吃数组。3.2 0e字符串用已知甜点库PK弱等于数组绕过虽然舒服但很多出题人会先处理掉这个口子比如显式判断参数is_array()或者用加“参数非数组”的双重限制。这时候如果比较用的是0e字符串就有机会上场。需要我多说一句的是0e绕过不仅仅针对md5。遇到sha1、crc32这类哈希结果也可能出现0e开头的情况只是md5最常考。平时刷题多留意网上公开的0e字符串列表存一份备用。还有一点在于如果题目把两个参数都做了一次比较比如md5($a) md5($b)而且又禁止数组你可以用两个不同的0e字符串同时传进去左边的md5值等于0右边的也等于0两边在数值上相等。3.3 真碰撞当代码写出的时候如果条件同时包含“非数组”和“”那么0e和数组都不好使了只能上真正的MD5碰撞。这里的原理是MD5算法本身的缺陷它可以被人为构造出两个内容不同但哈希完全相同的文件。工具上最常用的是fastcoll能在一两秒内生成一对前缀相同、内容不同的二进制文件。实际操作里我会这么做用fastcoll生成两个二进制文件读取它们的内容并做URL编码然后拼到payload里传入两个参数。要注意的是二进制内容里可能有不可见字符直接放URL里会被截断必须百分号编码后再传。这类题一般不会让你直接看到payload里的原始字节所以用脚本生成才是正道手工拼是不可行的。# fastcoll生成一对碰撞文件 ./fastcoll -o msg1.bin msg2.bin # 之后用Python读取并URL编码import urllib.parse with open(msg1.bin,rb) as f: data1 urllib.parse.quote(f.read()) with open(msg2.bin,rb) as f: data2 urllib.parse.quote(f.read()) print(fa{data1}b{data2})3.4 和混着出现时的读题顺序一段区间刷下来我发现真正难的题不是考单个绕过而是把几种绕过组合在一个判断链里。一上来就硬试是浪费时间的建议按这个顺序梳理先把条件拆开逐个判断用的是还是。看参数能否传数组。能传数组优先数组绕过因为它最简单。不能传数组再看比较条件里有没有“0”这类数值判断有就上0e字符串。如果既有非数组限制又有严格比较才考虑真碰撞工具。做题的时候拿张小纸片把这个顺序写下来效率能提高一半。我最初卡在md5题上就是因为上来就纠结“怎么找碰撞”完全没意识到题目里其实没有禁止数组参数。4. 变量覆盖与危险函数代码里写着验证实际到处是后门4.1 extract与parse_str的默认解包PHP里extract($_GET)会把关联数组的键当成变量名、值当成变量值导入当前作用域。这句话听起来平平无奇但如果变量在此之前已经赋了一个“判定值”整个逻辑就崩了。最典型的场景$flag no; extract($_GET); if ($flag yes) { echo getFlag(); }虽然代码先写了$flag no但extract把$_GET[flag]导入以后$flag直接被用户传入的yes覆盖“yes”“yes”成立验证形同虚设。这题的考点很简单但它的变形很多比如extract里面套一层foreach对$_GET做循环。parse_str()同样危险区别在于它解析的是字符串。如果代码里只写了parse_str($_SERVER[QUERY_STRING])没有传第二参数那么查询字符串里的键值对就会被当成当前作用域的变量。对一个数组参数做parse_str还能产生更多类型的变量名比如parse_str(a[]1)生成一个数组变量这类扩展能力有时候反而是漏洞的入口。4.2 $$动态变量的连环套动态变量名$$是另一个考点。如果代码里有类似这样的结构foreach ($_GET as $k $v) { $$k $v; }那么GET里任何参数都会变成同名变量效果和extract几乎一样。但这种写法还会带来一个衍生玩法利用变量名本身构造更复杂的覆盖比如传入GLOBALSxxx让$GLOBALS被赋值从而污染全局变量环境。我在刷题时的经验是看到$$基本就等于宣告“变量覆盖知识在此”接下来的方向就是找哪个变量控制最终输出或执行。4.3 先赋值后覆盖的通用解题思路这种题不管外壳怎么换解法的核心就一句话找一个正解应该出现的变量名然后想办法把它的值覆盖成程序预期的值。第一步看代码里最终判断用到了哪个变量第二步看这个变量能不能被外部输入控制第三步确认控制方式是extract、parse_str还是foreach$$。三步跑完绝大多数变量覆盖题都能出payload。顺带说一个细节有些题会在覆盖之前加一个if (isset($_GET[xxx]))看起来是限制其实反过来提醒了你“用xxx参数来覆盖”。看到isset的时候不要紧张它往往是出题人送出来的线索。5. 正则过滤与文件读取绕过姿势往往是一种规模游戏5.1 preg_match的数组特殊行为preg_match()处理数组参数时不会去匹配每一个元素而是直接返回false并抛出一个警告。很多题的过滤逻辑是if (!preg_match(/[a-z0-9]/i, $code)) { eval($code); }这个逻辑想表达的是“没有字母数字才允许执行”但如果你把code传成一个数组比如code[]phpinfo()preg_match返回false取反后变成trueeval照样执行而且eval接收数组时会直接输出“Array”或者报错并不会真正执行数组内容——这里的关键在于有时候eval并不需要真正执行只要走到执行分支就能拿到flag更多时候这道题还会配合assert之类能处理数组的函数出现。所以遇到“preg_match过滤执行函数”的组合先不要急着构造合法的无字母数字payload先试试code[]phpinfo()可能直接就绕了。5.2 换行锚点与PCRE回溯上限如果preg_match加上了^和$比如preg_match(/^[a-z0-9]$/i, $code)很多人以为只有纯字母数字才能通过。但这里有两个经典逃逸途径。第一个是换行。当正则里没有使用s修饰符时$在默认情况下只匹配字符串结尾或者匹配结尾换行符之前的位置。换句话讲/^\d$/可以匹配123\n因为$匹配了\n前面的位置。如果过滤的是/^[a-z0-9]$/i你传的值里带上换行PCRE会认为换行前的内容满足[a-z0-9]且位于行尾于是匹配成功而代码后续处理字符串时又保留了这个换行新的攻击面就出现了。第二个更隐蔽PCRE有一个回溯次数上限默认是1000000次。当输入字符串很长、正则匹配过程中需要不断回溯尝试超过这个上限后preg_match会返回false。如果题目过滤逻辑是“preg_match不通过才执行”你就可以用一个超长字符串把正则引擎活活跑崩让它返回false从而走进执行分支。这个技巧在后期题里出现过不止一次属于“不读PHP源码就完全想不到”的点。5.3 php://filter与include的经典联动PHP特性题里还有很多考文件读取的核心工具是伪协议。include($_GET[file])这类代码你想看源码又不想直接泄漏页面可以用php://filter/readconvert.base64-encode/resourceflag.php原理在于include遇到php://filter流时会按照read过滤器先对文件内容做base64编码再输出。这样虽然还是把文件内容“读”了出来但输出的是base64字符串不会因为文件本身是PHP代码而被执行。拿到base64以后用base64_decode解开源码就全出来了。如果题目把filter、convert、base64这些词过滤了也别急着放弃还有不少替代路径有的题不拦resource可以用php://filter/readstring.rot13/resourcexxx把文件内容做ROT13变换有的环境还支持php://filter/zlib.deflate做压缩编码。最好在本地起个PHP环境把这些过滤器挨个试一遍把“读源码不执行”的滤镜组合存成自己的字典后边的题直接查表。5.4 无字母数字的“手里没剑”场景过滤一旦严格到“不允许出现任何字母数字”常规的system(cat flag)直接没法写。这时候需要用到PHP里非常狂野的两个技巧异或构造和取反构造。异或构造的思路是两个非字母数字的字符异或之后可以得到一个字母数字字符。把一串字符拼起来就能构造出“system”“assert”“_GET”这样的关键词。取反构造的思路类似用~对不可见字符串取反得到可打印函数名。PHP 7/8里还支持(phpinfo)()这种把字符串直接当函数调用的写法。举例来说取反构造system$code (~%9C%8E%9E%8D)(~%93%8C%8D%9A%89%9E%8D%9C);这里的%9C%8E%9E%8D取反以后得到字符串system后面括号里是参数部分。这个技巧属于不看原理直接抄payload会很懵、理解了以后又会觉得有趣刷89到115这段如果碰上限制特别死的题可以提前把它加进武器库。说实话真正高频遇到的还是前几种这个更多是为后面更难的题做铺垫。6. 从刷题日记到方法库怎么让这27道题不白刷6.1 三栏错题模板条件、绕过、原理以前我刷题就是“解出来开心两分钟解不出来看一眼writeup就翻页”结果过一周再碰到同类型的题照样卡。后来改成三栏记录法效果好很多。题目阶段关键条件我的卡点 / 正确姿势弱类型num0且非数字卡在“又想等于0又不能是数字”解法是abcMD5非数组且一直找碰撞被0e误导最后用fastcoll变量覆盖extend前已赋值没注意GET参数可以同名覆盖正则preg_match过滤且要eval卡字母过滤最后用数组返回false绕过这个模板的精髓在于把“卡点”和“正确姿势”并列写下来刷完一段就翻一次能明显看出自己反复在哪个环节迷路。我后来发现很多人卡题不是不会payload而是不会从源码里提取“关键条件”。三栏模板逼着你去分析题目的判定结构正好补这块短板。6.2 一题多解的扩展思维刷到哪一道题真正被打动是在一个求md5($_GET[a])md5($_GET[b])的题里。当时我用了数组绕过题确实过了但写完发现出题人如果想禁掉数组这道题可以立刻变成“0e字符串寻找题”如果再禁掉0e字符串它又变成“真碰撞题”。一题三解全是在同一个判断条件上改出来的。从那以后我给自己的刷题规则加了一条每道题至少把一个条件替换成更强的限制再想一遍新解法。比如原题条件是就试着把它改成再解一次。这样做看起来费时间但好处极其明显——当你在一个新题里遇到的时候你不会觉得“这题不会做”只会觉得“这不就是我之前推演过的那个变体吗”。6.3 合并同类题建“条件反射卡”刷完89到115我整理出一套“条件反射”卡遇到类似代码直接在脑内触发对应分支看到 0且不允许数字第一反应是字母开头的字符串。看到md5($a)md5($b)先想数组再想0e最后想碰撞。看到preg_match(/^...$/)先测换行再测超长回溯字符串。看到include($_GET[file])先试php://filter读源码。看到extract($_GET)或foreach($_GET as $$)直接找目标变量传同名参数。这张卡节省了大量现场试错时间。刷题这东西前期靠知识量中期靠经验量后期靠的就是这种“看到特征直接有下一步动作”的反射速度。写在89到115阶段的末尾这一段刷下来最大的变化不是会解的题多了而是我对PHP“默认行为”有一种警觉看到会下意识想类型转换看到preg_match会下意识想它能不能被数组骗过看到include会下意识想伪协议。这种直觉不是刷一两道题能建立的是十几道换皮题反复锤出来的。给正在刷这段的朋友一个实操建议不要只刷题把每个知识点对应的payload沉淀成自己的小样本库。哪怕就是一个a[]1b[]2也要搞清楚它为什么能过、在什么条件下失效。等你能把一道题的“限制条件”和“绕过方式”一一对应起来89到115这段就算真正通关了。同一区间里还有更多复合玩法没展开那些我打算在下一篇日记里继续写保持这个刷题节奏就好。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑