
ctfshow的Web赛道刷到web329这一关时我大概停了快两天。说实话这道题表面上看就是一个不能再普通的登录页一个用户名输入框、一个密码框、一个登录按钮页面干净得几乎没有多余的信息。但正是这种“什么都看不出来”的题才最容易让人对着页面发呆。web329最后被我拆成了三步先从URL参数里找到文件读取入口再用伪协议把源码偷出来审计最后通过PHP弱类型比较加命令执行过滤绕过拿flag。这篇就把我自己的完整思路、踩过的坑和顺手总结出来的通用方法写出来给正在刷这类题、或者做Web项目测试时总在信息收集环节卡住的朋友做个参考。题目环境的具体实现可能每次不一样但核心考点是相通的。1. 入口页面看起来没给线索问题全在URL参数里1.1 登录框只是伪装真正入口是随手试出来的 ?f打开web329的靶机环境后我先按老规矩做了三件事F12看源码、看网络请求、把登录框当成SQL注入点试了几发。结果都让人失望源码里没有任何注释网络请求里没有多余接口输入单引号、双引号、 or 11之类的万能密码也没有任何回显变化。退出登录界面之前我顺手看了一眼地址栏因为是从题目列表点进来的URL很简单没有任何参数。当时由于刷过的题目多了养成了个习惯——只要页面是PHP常见框架或者原生写法就喜欢手动在URL后面加参数试试。于是我敲了个?f1页面刷新后报了一个PHP警告大意是“include后面没找到文件”。那一刻我心里基本有数了这题走的是文件包含或者文件读取的路子登录框真正的作用就是把人往SQL注入的方向带偏。后来验证了一下默认页面其实有一个隐藏逻辑当URL没有f参数时页面展示登录表单一旦带上f参数后端就会把参数值拼进一个include操作里。这个设计很符合CTF常见套路参数名越短越有可能指向文件操作。所以刷Web题的第一原则拿到目标后不要只盯着可见功能先把URL手写参数试一遍。1.2 为什么优先怀疑文件读取而不是直接猜路径这里有个判断层面的东西值得多说一句。当参数f被include之后常规操作是尝试读index.php的源码但直接传?findex.php大概率什么都看不到。因为include会把文件执行掉你想看的是代码不是执行结果。正确的做法是使用PHP伪协议中的过滤器http://靶机地址/index.php?fphp://filter/convert.base64-encode/resourceindex.phpconvert.base64-encode的作用是在文件内容被include执行之前先做一个base64编码这样源码不会被执行而是以编码后的字符串形式输出到页面。拿到这一串base64解码源码就到手了。这个原理说白了就是代码执行不了你你就把它变成字符串自己读。这一步执行完返回了一整段乱码一样的base64字符。复制到编辑器里解码第一份关键源码就出来了。filter伪协议在真实渗透测试里也是读取配置文件、源码文件的利器配合readfile、file_get_contents、include这类函数都很稳。只要目标站点用的是PHP并且存在文件读取类漏洞优先试php://filter是不会浪费时间的。1.3 从index.php源码里顺藤摸瓜找到第二个关键文件解出来的index.php代码并不长但信息量很大。里面有两个关键点一是确认了f参数会拼一个.php后缀进去比如include($file . .php)二是文件里include了另一个名为common.php的文件而且登录功能真正执行的逻辑都在common里。看到.php后缀拼接的第一反应是直接用filter读文件的时候路径里的resourceindex.php会再次被拼接成resourceindex.php.php理论上会报错。但实际操作中我们传的是resourceindex还是resourceindex.php要看服务端写法这里能解码成功说明代码里可能对php://开头的参数做了特判或者过滤规则本身不严格。顺着这个思路下一步就非常明确了用同样的方式去读common.phphttp://靶机地址/index.php?fphp://filter/convert.base64-encode/resourcecommon这次返回的base64解码之后题目真正的考点直接暴露在眼前。文件读取这个阶段做完相当于拿到了整张地图接下来就是审计代码找可以利用的逻辑。2. 源码审计阶段重点盯登录校验和命令执行触发点2.1 common.php里的关键代码弱类型比较看得我眼前一亮解出来的common.php核心逻辑简化之后大概是这样的?php error_reporting(0); session_start(); $secret ctfshow_secret_value; if (isset($_GET[pass])) { if (md5($_GET[pass]) md5($secret)) { $cmd $_GET[cmd]; if (preg_match(/flag|cat|system|\s/i, $cmd)) { die(hacker); } system($cmd); } else { echo login error; } } else { echo login first; }看到第一眼我的注意力就被md5($pass) md5($secret)这行吸引住了。这里用的是PHP的宽松比较不是严格比较。在PHP里md5()函数接收的期望类型是字符串如果给一个数组比如pass[]1函数本身不会把你传进去的值正常哈希而是在报出一个warning之后返回null。于是两边都变成null null宽松比较下结果为真。这就直接绕过了登录校验。这个点是非常经典的PHP弱类型绕过方式在CTF里出现的频率极高现实里如果确认目标后端是PHP且校验代码用了md5()加优先试数组绕过基本不会错。当时看完这段我还没急着发包因为后面还有一段命令执行过滤规则。preg_match(/flag|cat|system|\s/i, $cmd)过滤了四个东西flag这个关键词、cat命令、system函数、还有任何空白字符。后面真正执行用的是system($cmd)。说明题目设计者希望我们做到两件事绕过登录再绕过过滤执行系统命令。2.2 include链路的妙用不传f时登录功能根本不会触发这里有一个非常容易踩空的地方也是我第一次绕过了登录校验却发现命令不执行的原因。前面说了index.php会把f参数拼成PHP文件路径并include。如果我直接在首页URL上写http://靶机地址/index.php?pass[]1cmdid登录校验确实不会报“login error”了但页面会大概率报文件找不到。因为正常状态下页面include的还是默认文件common.php里的登录判断根本没被加载。正确做法是让页面先包含common.php再触发登录逻辑。也就是把URL改成http://靶机地址/index.php?fcommonpass[]1cmdid这给了一个很有价值的实战思路源码审计时不只是看“哪里有漏洞”还要看“漏洞代码在什么条件下会被加载”。include的参数如果可控它不光是文件读取的入口也是把远程或本地其他文件拉进来执行的开关。这个点理解透了后面构造完整payload就会很顺畅。2.3 读多个文件交叉验证比盯着一段代码猜更高效我后来又尝试读了几个同目录下的文件比如login.php、config.php、flag.php。config.php没读到login.php读出来的东西和登录表单长得差不多重点是flag.php。用filter去读flag.php的时候返回的base64解码后已经是flag内容了。如果只是想拿flag到这一步其实已经结束了。但web329真正有意思的地方在于它把命令执行和过滤绕过也做成了考点只靠文件读取拿到flag属于取巧并不能算完整通关。所以我继续顺着命令执行的思路把过滤绕开也完整做了一遍这个环节恰恰是很多人口中的“最简单的题为什么我卡这么久”的答案。3. 命令执行过滤绕过从只读文件到拿到shell回显3.1 先用无害命令确认执行点是否真的存在构造payload之前我先用一个不会被过滤的简单命令做测试。id命令不包含flag、不包含cat、不包含system也没有空格理论上可以直接执行。但这里注意我的URL已经要同时携带f、pass[]、cmd三个参数URL最终长这样http://靶机地址/index.php?fcommonpass[]1cmdid页面直接返回了uid33(www-data) gid33(www-data) groups33(www-data)。看到这串输出整个人就从卡壳状态切换到兴奋状态了。命令执行点确认剩下的就是处理过滤。处理过滤的顺序一定是先跑通最简单的命令再加payload否则一旦失败你根本说不清是参数没传对还是规则没绕开。这里分享一个我自己的测试习惯构造命令注入payload时先测whoami、id、ls这类不带特殊字符的命令确认执行点再测ls /看命令里带空格能不能run最后再叠加上目标关键词。这样每一层过滤规则都能单独验证。3.2 过滤了空格和flag关键词的绕过思路拆解现在来拆一下过滤正则/flag|cat|system|\s/i。flag被过滤命令里不能出现“flag”四个字母也就是说不能直接写cat flag.php。cat被过滤命令里不能出现“cat”这个子串正常读文件命令需要换。system被过滤这说明execute函数里可能也会检测system关键词防止利用system套娃不过我们直接构造system()之外的命令即可。\s被过滤命令里不能有空格、tab、换行等空白字符。第一个绕法是把读文件命令从cat换成tac或者nl。tac是cat的反向输出从文件最后一行开始往前输出。nl则会给输出的每一行加行号。这两个命令都不包含被过滤的cat子串所以能直接通过。第二个绕法是处理空格。Linux shell里可以用环境变量${IFS}当作默认分隔符。$IFS是内部字段分隔符默认情况下包含空格、tab和换行。shell在执行命令时会把${IFS}展开为一个空格等价物所以nl${IFS}flag.php等效于nl flag.php。而且${IFS}的字符串形式里也没有空格不会被\s命中。第三个绕法是处理文件名里的flag关键词。这个很简单用通配符fl*、fla?都可以。shell在匹配文件时会自动把fl*展开成当前目录下所有以fl开头的文件名比如flag.php。由于过滤规则检查的是你传入的原始cmd字符串而不是shell展开后的最终命令所以fl*能安全绕过去。3.3 最终payload构造过程与逐步验证综合上面三个绕法构造出的最终payload是http://靶机地址/index.php?fcommonpass[]1cmdnl${IFS}fl*命令实际执行的是nl flag.php但因为原始命令里没有出现空格、没有出现flag、没有出现cat、没有出现system所以正则检测全部通过。返回结果里每一行都加了行号flag就直接显示在终端输出里。我还试了一下用tac替代版本http://靶机地址/index.php?fcommonpass[]1cmdtac${IFS}fl*同样能执行成功输出顺序是倒着的。平时CTF题里如果nl也被加入过滤黑名单还可以继续换more、less、head、tail只要不带被过滤的关键字就行。通配符也不只限于fl*完全可以用f*或者[f]lag这种写法。关键在于把“读文件”“展示文件内容”“绕过关键字检测”三件事拆开一个命令不行就换一个思路通了方法很容易扩展。为了更直观把测试阶段的一组结果记录成表格传入cmd过滤结果执行结果结论id未触发显示当前用户命令执行点确认cat flag.php触发过滤页面输出hacker无执行需要绕过关键字nl?flag.php未触发问号会变成真实文件名可读文件需要多用通配符做验证nl${IFS}fl*未触发正则不匹配展开前字符串输出flag文件内容拿到flag全链路打通最后看到flag在终端输出的那一刻说实话真的有一种“原来如此”的感觉。整个题目最难的并不是命令怎么构造而是你愿不愿意在登录框这个假的入口上停下来、换个方向去找真正的参数。4. 复盘最开始的卡壳卡在自动化工具和固定套路上4.1 解题过程最大的弯路我盯着SQL注入和目录扫描浪费了一下午这题的卡壳经历复盘下来非常有代表性。刚开始我完全按照常规Web测试流程走见登录框就测SQL注入见输入框就上万能密码。Burp Suite里抓了包Intruder跑了一堆常见payload全部没回显。接着我又上了目录扫描工具扫了几分钟扫出来一堆404没扫到任何备份文件。问题出在哪里我太依赖“登录框SQL注入”这种条件反射也太相信目录扫描的结果。目录扫描工具扫的是路径是否存在它根本不会去帮你测试URL参数。而这个题目压根没把入口放在路径上而是放在“手动传参后触发include”这个行为上。这类题的隐蔽性就在这里页面没有任何链接指向带参数的URL你只能靠主动去试。后来我想明白一个道理刷CTF题目和做真实的渗透测试一样工具只是辅助理解和手动验证才是核心。Burp Suite适合抓包和重放但它不会告诉你“这里应该试一下 ?f”。这个思路必须由你自己产生。4.2 重新梳理之后的高效工具链卡了许久之后我重新整理了Web题的信息收集工具链后面刷同类题目效率明显提升。第一步打开靶机页面后用浏览器开发者工具只看三个东西源码里的注释、网络面板里的请求列表、还有Cookie和响应头。很多题目把线索放在注释里而这一题虽然没有注释但响应头的Server字段确认了后端是PHP这决定了后面能用伪协议。第二步手动把URL参数过一遍。?f、?file、?page、?path、?src、?include这些短参数名都可以试。web329用?f算比较隐蔽但也不算特例。这个动作不需要工具浏览器地址栏就行速度反而最快。第三步确认文件读取能力之后再用Burp Suite重放请求重点观察base64返回是否完整、页面有没有被截断。这样比用浏览器手动复制粘贴base64要高效得多。第四步所有源码解码之后集中放到一个文件里看把和文件读取、登录校验、命令执行相关的代码高亮标出来。这一步我用的就是一个普通编辑器谈不上多高级但比在终端里翻来覆去强太多。4.3 给同样卡住的同学判断自己卡在哪一层的三个自检问题当时为了复盘我给自己总结了三个自检问题如果你也卡在某道Web题上可以考虑对号入座。第一个你看到页面后除了点击可见按钮有没有主动改过URL参数如果没有先做这一步目标页面的隐藏功能往往藏在参数里。第二个你拿到源码之后是只看了第一层还是顺着include、require继续往下挖了很多逻辑都藏在第二层甚至第三层文件里web329真正核心的common.php就是这么挖出来的。第三个你在构造payload之后是直接试完就换还是先拆解了过滤规则、单独验证每一层如果是前者很容易陷入“怎么都过不掉”的循环。这三个问题本质上是同一个方法论信息收集要主动、审计要完整、绕过要拆解。做题卡住的时候与其一遍遍重放同一个payload不如退回去重新看看自己是不是漏了一个参数、漏了一个文件。5. 举一反三以后再遇到“登录框隐藏参数”的题我是这么做的5.1 一套可以复用的文件读取与源码获取链路从web329里沉淀出来的第一条通用思路是只要页面是PHP且存在可疑的文件类参数先走一遍伪协议文件读取链路。?fphp://filter/convert.base64-encode/resourceindex ?fphp://filter/convert.base64-encode/resourcecommon ?fphp://filter/convert.base64-encode/resourceconfig ?fphp://filter/convert.base64-encode/resourceflag这个名单不用死记核心在于先主文件再公共文件再配置文件再目标文件。遇到参数加水后缀的情况就调整文件名遇到没有后缀拼接的就直接写完整文件名。伪协议读取时如果返回的内容是正常HTML而不是base64多半是代码里做了展示判断或者目标文件本身不含PHP代码那就换一个filter写法。5.2 PHP弱类型比较的高频考点建议系统过一遍web329的登录校验用的是md5() 的数组绕过。这类PHP弱类型比较在CTF里衍生出了一整个家族强烈建议有空的时候把以下情况系统过一遍宽松比较与严格比较的区别0e开头的字符串在弱比较中等价于数字0md5()、sha1()接受数组时返回null两个null弱比较相等strcmp()且未开启严格模式时传数组可能导致返回null或非零值intval()处理数组、小数时的截断行为这套考点不只在ctfshow任何PHP题目都可能换个外衣出现。而且很多后端开发在写登录逻辑时确实会写出md5($pass) md5($secret)这种代码属于真实世界里也可能遇上的问题类型。5.3 命令执行过滤绕过的三维度记忆法命令执行过滤绕过我习惯从三个维度去拆命令词的替代、分隔符的替代、目标文件名的替代。命令词的替代cat被过滤换tac、nl、more、lessls被过滤换dirwhoami被过滤可以读/proc/self/status里的uid字段。分隔符的替代空格被过滤用${IFS}、$IFS$9、重定向符、{cmd,arg}的花括号方式。文件名的替代用*、?、[a-z]等通配符完成模糊匹配甚至可以用flag这种引号拆分写法绕过子串检测。这三个维度组合起来能覆盖绝大多数正则过滤场景。关键是要明白一点正则检测的是你提交的字符串而shell执行的是展开后的命令这种“检测态和执行态不一致”的特性天然就留了绕过空间。5.4 做题顺序比工具数量更重要最后聊聊顺序。web329给我的最大教训是先看源码再做自动化扫描永远比先跑工具再回头看页面高效。目录扫描器、端口扫描器这些东西不是没用而是应该在手动信息收集和源码审计之后再用用来补充验证而不是作为起点。现在的Web题尤其是CTF里偏教学向的题目设计者故意把入口藏在代码逻辑里目的就是逼你去读代码、理解执行链路。工具只能帮你扩大覆盖面不能代替你理解“为什么这个参数会被拼进include、为什么过滤能被数组绕过去”。当你把顺序调整为“手动观察、参数探测、源码审计、逻辑分析、精准构造payload”之后很多以前觉得难的题会突然变得清晰。我自己回过头来看web329最有价值的收获不是多会了一个payload而是真正养成了“先不急着动手先想清楚漏洞产生的链路”的习惯。这个习惯放到真实的项目测试里同样适用——再复杂的系统最终突破口往往藏在一段不起眼的参数拼接里。