1. 从一道题看透 parse_url 的想当然做 Web 方向题目的朋友大概都有过这种体验拿到一个 URL 解析相关的题目第一反应是这不就是个字符串处理吗然后随手写个parse_url跑一下发现结果和预期对不上再改改还是不对最后卡在那里怀疑人生。我最早接触这类题目的时候也是这样明明本地跑出来的解析结果看着挺合理提交上去就是过不了反复折腾半天才意识到——问题根本不在我的逻辑而在于我对parse_url这个函数的行为边界理解得太粗糙了。这篇内容就围绕parse_url在 CTF 场景下的典型考点展开把这道题背后真正值得挖的东西讲透。核心关键词是parse_url、URL 解析差异、绕过技巧、PHP 内置函数行为。它适合两类人看一类是刚接触 Web 题目、对 URL 结构还没建立起清晰认知的新手另一类是做过几道题但总是知其然不知其所以然、想彻底搞明白解析器行为差异的中级选手。我会从函数本身的机制讲起把常见的解析陷阱、绕过思路、以及我在实际调试中踩过的坑一条条摊开尽量做到你看完之后再遇到类似的 URL 解析题能有一个稳定的分析框架而不是靠猜。需要先说明一点parse_url的行为在不同 PHP 版本、不同上下文里是有细微差别的网上很多零散的结论其实是特定版本下的产物直接照搬容易翻车。所以下面讲的内容我会尽量把为什么讲清楚让你能自己判断某个结论在你当前环境里是否成立而不是死记硬背几个 payload。2. parse_url 到底怎么拆一个 URL2.1 它返回的到底是什么结构很多人对parse_url的印象停留在传个 URL 进去返回一个数组里面有 scheme、host、path 这些键。这个印象没错但太浅了。真正要命的是它并不是一个严格的 URL 校验器而是一个尽力而为的解析器。也就是说即使你传进去的字符串根本不是一个合法的 URL它也可能返回一个看起来有模有样的数组而不是返回false。举个最直观的例子。你传parse_url(//example.com/path)它会返回 host 为example.com、path 为/pathscheme 缺失。你传parse_url(example.com/path)它会把整串当成 pathhost 是空的。这两种情况在浏览器地址栏里的语义完全不同但在parse_url眼里都能解析。这种宽容性正是很多题目的突破口——出题人利用的就是解析器认为合法和业务逻辑认为合法之间的缝隙。返回数组里可能出现的键包括scheme、host、port、user、pass、path、query、fragment。注意这些键是按需出现的不是每个 URL 都会全都有。比如没有查询串就不会有query键没有锚点就不会有fragment键。写判断逻辑的时候如果直接$arr[query]去取遇到不存在的键就会触发警告甚至报错这也是实际开发中容易埋雷的地方。2.2 解析顺序决定了谁先被吃掉理解parse_url的关键是理解它的解析顺序。它大致遵循这样的优先级先找 scheme:之前的部分且要符合 scheme 的字符规则再处理//之后到下一个/、?、#之间的 authority 部分这里面再拆 user、pass、host、port然后是 path接着是?后的 query最后是#后的 fragment。这个顺序带来的一个直接后果是某些特殊字符出现在错误的位置会导致后面整段内容被归到意想不到的键里。比如在 authority 部分是用来分隔 userinfo 和 host 的如果你在 host 位置塞一个解析器会把前面的部分当成 user后面的当成 host。这就是经典的userinfo 混淆手法。再比如#一旦出现它后面的所有内容都会被当成 fragment哪怕后面还有?和也不会再被解析成 query。我在调试时养成了一个习惯拿到任何可疑 URL先把它丢进parse_url打印完整结果再对照 RFC 3986 的结构图逐段核对。这个动作看起来笨但能避免 90% 的我以为它是这样的误判。下面这张表是我整理的常见字符在 authority 段里的作用做题时对着看很省事字符在 authority 段的作用常见误判分隔 userinfo 与 host以为只是普通字符:分隔 host 与 port或 scheme 与后续端口位置放非数字会怎样?终止 authority进入 query以为 host 里能含?#终止一切进入 fragment以为 fragment 里还能有 query/终止 authority进入 path多个/的处理2.3 端口、userinfo 这些边角料才是主战场真正让题目有区分度的往往不是 scheme 和 host 这种主角而是 port、user、pass 这些边角料。parse_url对端口的处理有个特点它只认数字遇到非数字会怎样不同版本行为不完全一致有的会把整段当 host 的一部分有的会解析失败。这种不确定性恰恰是出题人喜欢利用的点——你的过滤逻辑假设端口一定是数字但解析器可能压根没把它当端口。userinfo 部分同理。user:passhost这种结构里user和pass都可能包含一些在别处会被过滤的字符因为它们在 userinfo 段里的语义和 path 段里完全不同。很多过滤函数只盯着 path 和 query 做文章却忘了 userinfo 也是一个可以夹带内容的通道。我在实际分析时会把 URL 拆成五段式来看scheme、authority再细分 userinfo/host/port、path、query、fragment每一段单独问一句这里能塞什么、过滤函数管不管得到往往就能找到缺口。3. 那些年我们一起踩过的解析陷阱3.1 反斜杠最容易被忽视的斜杠替身如果说这类题目有一个最高频的坑那一定是反斜杠\。在parse_url的眼里\和/在 authority 段附近的行为并不完全等价但在很多下游处理比如某些 URL 规范化、某些框架的路由匹配里\会被当成/处理。这种上游不认、下游认的错位就是绕过的温床。我印象很深的一次调试构造了一个 host 里带反斜杠的 URLparse_url解析出来的 host 是A但实际请求发出去之后服务端拿到的 host 变成了B。当时我盯着parse_url的输出看了半天没想通后来才意识到问题出在解析和使用是两个阶段两个阶段对同一个字符串的理解不一致。这个认知一旦建立后面很多题目的思路就顺了——不要只问 parse_url 怎么想要问拿到 parse_url 结果之后的那段代码怎么想。具体到操作上我的建议是构造 payload 时把parse_url的输出和最终被使用的值都打印出来对比。如果两者不一致那个不一致点就是你要利用的地方。下面是一段我常用的调试代码直接跑就能看到差异?php $urls [ http://example.com\\evil.com/, http://evil.com\\example.com/, http://example.com/path\\..\\admin, ]; foreach ($urls as $u) { echo INPUT: $u\n; var_export(parse_url($u)); echo \n---\n; } ?跑完之后你会发现反斜杠在不同位置引发的解析结果差异非常微妙有的被吞进 host有的留在 path有的直接让解析提前结束。把这些结果记下来比看十篇博客都管用。3.2 多重 scheme 与协议相对 URL 的混淆第二个高频陷阱是 scheme 的处理。parse_url判断 scheme 的依据是第一个:之前的内容是否符合 scheme 的字符规则字母开头后接字母数字加、-、.。这里有个细节如果:前面出现了不符合规则的字符它就不会把这部分当 scheme而是继续往后找或者干脆当成 path。这就引出了多重 scheme的玩法。比如http://a.comb.com和http://a.com:80b.com前者 host 是b.coma.com成了 userinfo后者因为:80的存在解析路径会发生变化。再比如协议相对 URL//host/path它没有 scheme但 host 是明确的。如果你的过滤逻辑写的是必须以 http:// 开头那//host/path就能直接绕过因为parse_url认它是合法的、有 host 的 URL只是 scheme 为空。我在整理这类陷阱时习惯按scheme 是否存在authority 是否存在path 是否以斜杠开头三个维度做组合每种组合对应一类绕过思路。这个组合表比零散记 payload 有用得多因为它能帮你在遇到新题时快速定位属于哪一类。实际做题时先判断目标代码用的是parse_url的哪个字段做校验再反推应该构造哪种组合效率会高很多。3.3 端口解析的边界非数字、超范围、空端口端口这块的坑特别隐蔽。parse_url对host:port的解析端口部分理论上应该是数字但实际实现里对边界情况的处理并不严格。比如host:后面跟空或者跟一个超出 65535 的数字或者跟字母返回结果可能让你意外。我遇到过一个场景过滤逻辑用parse_url取出 host 做白名单校验但端口部分塞了特殊内容导致 host 的提取结果和预期不同。当时排查了很久最后发现是端口解析把后面的内容吃掉了一部分host 因此变成了另一个值。这类问题的排查方法很朴素把端口位置的各种变体空、字母、超范围数字、带特殊符号都试一遍记录parse_url的输出找出那个host 变了但整体还能解析的临界点。需要提醒的是端口相关的行为在不同 PHP 版本间差异相对明显网上有些结论在旧版本成立、新版本就失效了。所以如果你在本地复现不出来先确认版本别急着否定思路。我一般会在题目环境里先跑一个phpversion()或者用php -v确认再决定用哪套结论。4. 从解析差异到绕过一条完整的分析链路4.1 先搞清楚校验的是哪个字段拿到题目第一步永远不是构造 payload而是读代码搞清楚它到底拿parse_url的哪个字段去做校验、又拿哪个字段去实际使用。这两者是不是同一个字段决定了你有没有操作空间。如果校验用 host、使用也用 host那你要做的就是让 host 在校验时是白名单、在使用时是目标如果校验用 host、使用用整串 URL那空间就更大了。我见过不少新手一上来就背 payload结果题目稍微改一下字段就懵了。正确的顺序应该是定位校验点 → 定位使用点 → 找两者之间的解析差异 → 构造能放大差异的输入。这个链路走通了payload 是自然推导出来的而不是猜出来的。举个具体的推导例子。假设代码是取 host 判断是否等于 trusted.com然后把整个 URL 交给下游请求。那么思路就是让parse_url认为 host 是trusted.com但下游实际请求的是别的地方。这时候 userinfo 混淆、反斜杠、多重这些手法就派上用场了因为它们恰好能制造解析视角和请求视角的 host 不一致。你看思路是从代码结构推出来的不是从 payload 列表里挑的。4.2 构造与验证把每一步的输出都打出来推导出方向之后构造阶段最重要的是可观测。我的做法是写一个小脚本把候选 payload 列表批量跑一遍每个都输出parse_url的完整结果然后人工比对哪个符合预期。这一步千万别偷懒因为很多 payload 看起来只差一个字符解析结果却天差地别。验证阶段还要注意一点本地环境和题目环境可能不一致。PHP 版本、是否开启了某些扩展、下游用的是哪个 HTTP 客户端都会影响最终结果。所以本地验证通过只是第一步真正的确认要在题目环境里做。如果题目环境不方便直接调试那就用二分法——先确认解析阶段是否符合预期比如通过报错信息、响应差异间接判断再确认使用阶段的行为。下面这个表格是我常用的差异定位表构造时逐行填能快速锁定问题出在哪一段检查项预期值实际值是否一致parse_url 的 hosttrusted.com??parse_url 的 user空??下游实际请求 hosttrusted.com??过滤函数看到的 hosttrusted.com??填完这张表如果某一行的实际值和预期值对不上那一行就是你的突破口。4.3 一个容易忽略的点编码与二次解析还有一个经常被低估的维度是编码。URL 里的百分号编码、双重编码在不同解析阶段被解码的次数不一样。parse_url本身对百分号编码的处理比较老实它通常不会主动解码但下游在使用时可能会解码一次甚至多次。这就造成了解析时看到的是编码后的串、使用时看到的是解码后的串的差异。我在实际题目里遇到过这样的情况host 里塞了百分号编码的字符parse_url原样保留白名单比对时因为编码形式不同而通过但下游解码后变成了另一个 host。这种手法需要对谁在什么时候解码有清晰的判断不能想当然。排查方法是在每一个可能的解码点前后都打印一次值看它在哪一步发生了变化。5. 实操心得让这类题不再靠运气5.1 建立自己的解析行为速查表做了几道题之后我发现靠记忆记 payload 是最不靠谱的因为环境一变就失效。真正有价值的是建立一张解析行为速查表——针对你常用的 PHP 版本把各种边界输入反斜杠、多重、空端口、特殊 scheme、编码字符的parse_url输出都跑一遍记下来。这张表是你自己的比任何博客都贴合你的实际环境。维护这张表的方法很简单每次遇到新的解析差异就往表里加一行注明输入、输出、以及这个差异能用来干什么。时间长了你会发现自己对解析器的直觉越来越准遇到新题能快速判断这个方向有没有戏而不是盲目试。5.2 别只盯着 parse_url要看它的下游这是我最想强调的一点。parse_url本身只是一个解析函数它不产生漏洞漏洞产生于解析结果被如何使用。所以分析的重心应该放在下游拿到 host 之后是拼进请求、还是做字符串比较、还是再经过一层规范化每一层处理都可能引入新的差异。我踩过的一个典型坑是只验证了parse_url的输出符合预期就以为大功告成结果下游还有一层 URL 规范化把我精心构造的差异给抹平了。后来我养成了习惯构造完 payload 之后一定要把从输入到最终使用的完整链路在脑子里过一遍每一环都问一句这里会不会改变我的值。这个习惯帮我省了很多次返工。5.3 版本差异要主动确认不要假设前面提过好几次版本差异这里再强调一遍。parse_url在 PHP 的不同大版本之间对某些边界输入的处理确实存在差别尤其是端口、反斜杠、以及畸形 scheme 这几块。网上很多流传的结论没有标注版本直接拿来用风险很高。我的做法是拿到题目先确认环境版本然后针对性地验证关键行为而不是假设应该和上次一样。如果题目环境拿不到版本信息那就用行为探测——构造几个已知在不同版本下表现不同的输入通过响应差异反推版本区间。这个方法虽然绕但比瞎猜靠谱得多。5.4 记录失败路径比记录成功路径更有价值最后分享一个习惯每次做题不管成没成我都会把试过的思路和结果记下来尤其是那些看起来应该行但实际不行的路径。这些失败路径往往揭示了某个我没意识到的约束条件下次遇到类似结构就能直接排除省下大量时间。比如有一次我试了三种 userinfo 混淆的变体都不行记录的时候发现共同点是下游对 host 做了严格的正则校验这个信息让我立刻转向了 path 和 query 方向。如果当时只是记userinfo 不行下次可能还会再试一遍。记录要记到为什么不行这一层才有复用价值。6. 把这道题当成一个分析模板回过头看parse_url这类题目的价值不在于某一个具体 payload而在于它逼着你去建立一套解析器行为分析的方法论先理解解析器的规则和边界再定位校验点与使用点的差异然后构造能放大差异的输入最后在真实环境里验证。这套方法可以迁移到任何涉及字符串解析 安全校验的场景不管是 URL、JSON、还是自定义协议。我个人在实际操作中的体会是越是看起来简单的内置函数越值得花时间把它的边界摸清楚。parse_url就是典型——它的文档很短但行为细节很多而这些细节恰恰是题目和真实系统里最容易出问题的地方。把这道题吃透你收获的不只是一个 wp而是一种面对解析类问题时的稳定心态和分析路径。下次再遇到类似的函数你会知道从哪里下手而不是对着屏幕干瞪眼。
阅读完成 · 觉得有帮助?