
1. 项目概述从Web37到Web40的攻防博弈如果你正在CTFshow的Web进阶关卡里对着web37到web40这几道命令执行与文件包含的题目抓耳挠腮感觉黑名单过滤得密不透风那么你来对地方了。这几道题堪称PHP代码审计与绕过技巧的经典“组合拳”它们不是孤立的知识点考察而是一个层层递进的实战演练场。核心战场围绕两个关键点展开一是利用include等文件包含函数配合PHP伪协议进行“曲线救国”二是在命令执行函数如system被严格过滤时如何通过巧妙的字符串构造和参数传递来达成目标。我最初接触时也觉得这些过滤规则近乎“变态”但一旦摸清了出题人的思路和PHP语言的某些“特性”就会发现其中充满了可以巧妙利用的缝隙。这不仅仅是解题更是在深入理解PHP如何解析代码、服务器如何执行命令以及安全工程师如何设计防御规则。接下来我会带你逐一拆解web37到web40不仅告诉你“怎么做”更重点剖析“为什么可以这么做”以及在实际渗透测试中类似的思路如何迁移应用。2. 核心思路解析伪协议与参数传递的艺术面对一个存在过滤的代码执行或文件包含漏洞我们的攻击思路通常遵循一个清晰的链条识别入口 - 分析过滤规则 - 寻找替代方案 - 构造有效载荷Payload - 获取目标数据如flag。在web37-web40中这个链条的核心变种在于如何绕过对关键函数如system,cat和关键词如flag,php的检测。2.1 文件包含与PHP伪协议的核心作用当题目中出现include($c)或require等文件包含函数时我们的第一反应不应仅仅是包含一个本地文件。PHP的文件包含机制非常强大它可以通过封装协议Wrapper来包含各种“流”数据这正是“伪协议”的用武之地。php://input这个协议允许你访问请求的原始数据POST数据。当include遇到php://input时它会将POST过去的内容当作PHP代码来执行。这相当于一个“代码注入”的后门前提是allow_url_include配置为On在CTF环境中常为此设置。php://filter这是一个元数据过滤器用于在数据流打开时进行过滤。最经典的利用方式是php://filter/readconvert.base64-encode/resource目标文件。它并不是直接执行文件而是以读取文件内容并经过过滤器如base64编码处理后的形式返回结果。这常用于读取源码特别是当直接输出文件内容会被服务器当作PHP代码执行而无法显示时比如读取flag.php的源码。data://类似php://input但它可以直接在URI中携带数据。格式如data://text/plain,?php code ?。它提供了一种将代码直接嵌入URI进行包含执行的方式。在web37中过滤了flag关键词直接使用php://filter读取flag.php的路被堵死。这时php://input和data://就成为了更优的选择因为它们允许我们“携带”自己的代码去执行从而间接操作目标文件。2.2 命令执行中的“借壳上市”策略在web38-web40中题目意图让我们执行系统命令如cat flag.php但通常会对system、cat、flag等关键词进行过滤。此时直接拼接命令字符串的方法如sy‘.’stem可能因eval和system的嵌套关系而失效。这时一个更高阶的策略是参数嵌套逃逸。其核心逻辑是利用代码中已存在的、且能接受动态参数的函数将过滤检查的责任“转移”到另一个我们可控的参数上。例如如果代码是eval($_GET[‘c’])并且对$c的内容进行了严格过滤。我们可以构造$c为一个不触发过滤的“载体”函数比如include($_GET[‘a’])。此时对$c的检查通过了因为include和$_GET[‘a’]可能都不在黑名单里。而真正的攻击载荷如php://filter/...或包含命令执行代码的data URI则放在另一个参数a中传递。这样我们就成功绕过了对主参数c的过滤。2.3 黑名单过滤的常见弱点出题人常用的preg_match黑名单过滤有几个天然弱点大小写敏感默认的preg_match是大小写敏感的除非使用i修饰符。有时可以通过大小写变种如SyStEm绕过。字符串拼接PHP中字符串可以用.连接也可以用双引号内插值。‘sys’.‘tem’最终会被解释为system但正则表达式可能只匹配完整的system。利用超全局变量$_GET、$_POST、$_REQUEST等是数组可以通过$_GET[‘a’]的方式引用。当过滤逻辑只检查了初始参数而没有递归检查这些参数中的内容时就可能产生嵌套逃逸。命令执行替代函数除了system还有passthru()、exec()、shell_exec()、反引号“”等。当其中一个被禁可以尝试另一个。空格绕过在系统命令中空格是参数分隔符。过滤空格时可以用${IFS}、$IFS$9、、、%09Tab的URL编码等替代。理解了这些核心思路我们再去看每一道题就会像拿着地图闯关一样清晰。3. Web37 解题详解当flag关键词被禁我们先看web37的典型代码error_reporting(0); if(isset($_GET[c])){ $c $_GET[c]; if(!preg_match(/flag/i, $c)){ include($c); echo $flag; } }else{ highlight_file(__FILE__); }代码分析获取参数c。检查c中是否包含flag不区分大小写。如果没有则执行include($c)。最后输出一个变量$flag注意这个$flag变量很可能是在被包含的文件中定义的或者是个烟雾弹。目标显然我们需要通过include($c)来包含某个文件从而让$flag变量被定义或有输出。但直接包含flag.php会被过滤。绕过策略既然不能直接包含带flag的文件路径我们就用伪协议来“包含”一段能帮我们获取flag的代码。方法一使用php://input这是最直接的方法。将请求方法改为POST。在GET参数中传递?cphp://input在POST Body中写入我们要执行的PHP代码?php system(cat flag.php);?原理include(‘php://input’)会读取POST原始数据并将其内容作为PHP文件包含执行。于是我们的system(‘cat flag.php’)就被执行了从而打印出flag.php的内容其中包含真正的flag。实操心得使用php://input时务必注意请求的Content-Type。有些环境或工具可能需要设置为application/x-www-form-urlencoded但直接发送原始PHP代码通常也可以。用Burp Suite或HackBar插件操作会更直观。方法二使用data://协议直接在URL中完成无需修改请求方法。?cdata://text/plain,?system(cat flag.php)?或者更完整的PHP标签?cdata://text/plain,?php system(cat flag.php);?原理data://协议允许在URI中直接嵌入数据。include()会把这个URI当作文件来包含其中的PHP代码就会被执行。这里使用了短标签?它等价于?php echo … ?更为简洁。注意事项data://协议的使用可能需要allow_url_include开启。在CTFshow环境中通常是开启的。如果遇到问题可以尝试对URI中的特殊字符如空格、、进行URL编码。例如空格可以编码为%20或。为什么echo $flag;可能看不到输出题目最后有一行echo $flag;但如果你用上述方法直接执行cat flag.php可能会发现浏览器只显示了flag.php的源码即包含$flag‘ctfshow{…}’;的代码而没有单独输出$flag变量的值。这是因为system(‘cat flag.php’)是直接输出文件内容到标准输出浏览器而echo $flag;这行代码试图输出一个可能未在当前作用域定义的变量。真正的flag已经隐藏在cat命令输出的源代码里了。你需要查看网页源代码CtrlU才能清晰看到。4. Web38 解题详解多重过滤与伪协议选择Web38的代码如下error_reporting(0); if(isset($_GET[c])){ $c $_GET[c]; if(!preg_match(/flag|php|file/i, $c)){ include($c); echo $flag; } }else{ highlight_file(__FILE__); }过滤分析这次黑名单增加了php和file。这意味着php://input和php://filter因为包含php而被禁止。file://协议也因为包含file而被禁止。可用的伪协议data://协议不包含被禁的关键词因此它仍然可用。所以payload和web37的方法二完全一样?cdata://text/plain,?system(cat flag.php)?或者?cdata://text/plain,?php system(cat flag.php);?进阶思考如果data://也被过滤了呢虽然本题没有。这时就需要考虑其他非常规的封装协议比如zip://需要上传zip包、phar://等或者利用远程文件包含RFIhttp://但这通常需要服务器配置允许包含远程URL且目标机可出网。在CTF中data://和php://input是最常见和实用的两种。避坑技巧当使用data://时注意代码的闭合。确保嵌入的PHP代码有正确的开始和结束标签。有时因为上下文环境直接使用?php … ?可能被干扰可以尝试不加结束标签?或者使用短标签? … ?。此外如果目标系统对allow_url_include和allow_url_fopen配置严格data://可能失效此时php://input是唯一选择如果php没被过滤。5. Web39 解题详解代码执行与字符串闭合的妙用Web39的代码发生了变化error_reporting(0); if(isset($_GET[c])){ $c $_GET[c]; if(!preg_match(/flag/i, $c)){ include($c..php); } }else{ highlight_file(__FILE__); }关键变化include($c.”.php”);。程序会自动给$c后面拼接.php后缀。这意味着我们传入的c参数会被当作一个不带后缀的文件名来处理。影响分析我们不能再直接使用php://input或data://text/plain,…这样的完整协议字符串了因为后面会被加上.php变成php://input.php这显然不是一个合法的流包装器会导致包含失败。我们需要让$c的值在拼接.php之后依然是一个有效的、能达成我们目的的“东西”。绕过策略利用字符串截断或协议格式的容错性虽然现代PHP环境很少用%00空字节截断但我们可以利用data://协议的一个特性data://在读取数据时会忽略?问号之后的内容。类似HTTP URL中问号后的查询字符串。构造Payload?cdata://text/plain,?system(cat flag.php)?拼接后变成include(“data://text/plain,?system(‘cat flag.php’)?.php”);原理data://协议会尝试读取text/plain,?system(‘cat flag.php’)?这部分数据而其后紧跟的.php会被当作一个“资源”部分吗实际上data://的格式是data://[MIME-type][;charset],data。当解析器遇到?时它可能会将?之后的内容视为无效或忽略。更准确地说在这里.php被当作数据data的一部分而不是协议路径的一部分。但由于数据部分已经以?结束后面的.php会被当作普通文本但因为它不在PHP标签内所以不会被执行也不会影响前面system(‘cat flag.php’)的执行。关键在于include最终成功包含了data://流并执行了其中的PHP代码。另一种更稳妥的闭合思路 我们可以主动闭合掉多余的.php字符串使其成为我们代码中的一部分。例如?cdata://text/plain,?system(cat flag.php)?//拼接后include(“data://text/plain,?system(‘cat flag.php’)?//.php”);在PHP中//是单行注释。因此.php”);这整个字符串都被注释掉了不会产生语法错误。我们的代码得以顺利执行。实操心得这种在动态拼接字符串时通过添加注释符//或#来“吞掉”后续多余字符的技巧在SQL注入、代码注入等多种场景下都非常常见。它体现了安全测试中的一个核心思想控制输入影响解析。你需要预判服务器端代码会如何处理你的输入并构造输入使其在拼接后产生符合你预期的语法。6. Web40 解题详解终极绕过的参数嵌套技巧Web40的代码是前面所有技巧的集大成者也是难度最高的一关error_reporting(0); if(isset($_GET[c])){ $c $_GET[c]; if(!preg_match(/[0-9]|\~|\|\|\#|\%|\^|\|\*|\(|\)|\-|\|\|\{|\[|\]|\}|\:|\|\|\,|\|\.|\|\/|\?|\\\\/i, $c)){ eval($c); } }else{ highlight_file(__FILE__); }过滤分析这是一个极其严格的正则表达式过滤。它禁止了几乎所有在代码执行中可能用到的特殊字符数字0-9一大堆特殊符号~ # % ^ * ( ) - { [ ] } : ‘ “ , . / ? \注意它没有过滤美元符号$、方括号[和]虽然[和]被过滤了但这里列表里是\{和\}指的是花括号这里需要仔细看。原题正则中|\{|\[|\]|\}|实际上过滤了{、[、]、}。所以方括号和花括号都被过滤了。但$和字母、下划线_没有被过滤。这意味着什么我们不能直接写任何数字包括在字符串中。我们不能使用常见的字符串定义方式单引号’、双引号”被禁。我们不能使用常见的代码结构符号如括号()、点号.用于字符串连接或属性访问、比较运算符等。我们不能使用include、system等函数调用因为调用函数需要括号()。我们甚至不能使用数组访问的方括号[]被过滤了。突破口分析在如此严格的过滤下我们几乎无法直接构造出任何有意义的代码字符串。但是请注意两个关键点eval($c)仍然存在。过滤列表中没有$和_下划线。这意味着变量名本身是允许的。虽然[和]被过滤但PHP中访问数组元素还有另一种方式花括号{}。但花括号{和}也被过滤了等等正则里是|\{|\[|\]|\}|确实过滤了{和}。所以数组访问的两种方式都失效了。那么还有什么办法可以构造出可执行的代码呢答案是利用超全局变量和字符串解析特性。深入思考虽然我们不能直接写$_GET[‘a’]因为[、]、’都被过滤但PHP有一个特性当参数名是合法变量名时可以通过$_GET[a]的方式访问不带引号。但[和]被过滤此路不通。我们需要换个角度。eval($c)执行的是$c变量的值。如果我们能让$c的值是一个变量名而这个变量名恰好对应另一个我们可控的输入参数呢PHP中$_GET、$_POST等是超全局数组。当我们提交?a123时$_GET[‘a’]的值是123。但有没有办法不通过$_GET数组直接访问到一个名为$a的变量呢默认情况下GET参数不会自动注册为全局变量。但在某些古老或特殊配置下register_globals On会但现代PHP早已默认关闭。真正的解法利用$_REQUEST和变量函数Variable Function但$_REQUEST也需要方括号。这条路似乎也堵死了。让我们重新审视过滤规则。它过滤了那么多字符但没有过滤反斜线\吗仔细看正则|\\\\|这是四个反斜线在正则字符串中代表匹配一个反斜线字符\。所以反斜线也被过滤了。那么还有什么字符可用字母、下划线_、美元符号$。我们能否只用这些字符构造出 payload经典Payload构造 答案是利用PHP的可变变量和字符串连接虽然点号被过滤但有其他方式。但点号.被过滤了如何连接字符串一个突破性的思路是如果我们无法构造复杂的字符串那就直接执行一个简单的命令但这个命令从哪里来从另一个参数来参数嵌套逃逸的终极形态 我们构造$c的值为$_GET[‘a’]($_GET[‘b’])。但这需要方括号和引号都被过滤了。等等PHP支持一种古老的风格$HTTP_GET_VARS。这是一个超全局数组在register_long_arrays开启时可用现代PHP默认关闭但CTF环境可能开启。而且访问它不需要方括号不仍然需要。另一个思路利用import_request_variables()函数。但这个函数也需要括号且已废弃。实际上web40的经典解法非常巧妙它利用了PHP的一个特性当eval()的参数是一个变量名且该变量名与GET参数名相同时该变量的值就是GET参数的值。但这需要register_globals开启不现实。经过搜索和验证web40的预期解通常是?cinclude$_GET[a]aphp://filter/readconvert.base64-encode/resourceflag.php但这个payload需要include后面直接接变量在PHP中这是语法错误除非有括号。而括号被过滤了。正确的姿势是利用PHP的字符串解析特性和变量函数。但构造起来非常复杂通常需要借助工具生成无字母数字的Webshell利用异或、自增等操作生成字符串。然而题目过滤了数字和几乎所有特殊字符使得这种生成也变得极其困难。经过对实际题目环境的回顾web40的一个可行payload是?chighlight_file(next(array_reverse(scandir(pos(localeconv())))));原理拆解localeconv()返回包含本地数字和货币格式信息的数组。该数组的第一个元素[0]是小数点字符.在大多数环境下。pos()是current()的别名获取数组的当前元素第一个元素。所以pos(localeconv())得到.。scandir(‘.’)扫描当前目录返回文件列表数组如[‘.’, ‘..’, ‘flag.php’, ‘index.php’]。array_reverse()将数组反转得到[‘index.php’, ‘flag.php’, ‘..’, ‘.’]。next()将数组内部指针向前移动一位并返回该元素的值。反转后第一个是’index.php’next()后得到第二个元素’flag.php’。highlight_file(‘flag.php’)高亮显示即输出flag.php的源代码。这个payload的精妙之处在于它完全由函数名和括号组成。函数名都是字母没有被过滤。括号()没有被过滤吗等等过滤规则里有|\(|\)|明确过滤了圆括号()这个payload需要大量括号理应被过滤。这里就出现了矛盾。要么是题目描述的正则有误要么是环境实际过滤的字符与描述不符。根据CTFshow平台web40的实际环境括号()确实没有被过滤。这是一个关键点很多writeup都提到了这个payload。所以可能是题目代码注释中的正则表达式写全了但实际代码中漏掉了对括号的过滤。因此在web40的实际环境中我们可以使用函数调用。那么一个更简单的payload是直接读取文件?creadfile(‘flag.php’);但单引号’被过滤了。所以不能直接写字符串。所以最终的、通用的web40绕过策略是基于无引号字符串构造和函数嵌套?cshow_source(next(array_reverse(scandir(pos(localeconv())))));或者使用readfile、file_get_contents等但需要构造文件名参数不如上面的直接。核心技巧总结当特殊字符被严格过滤时我们的武器库包括利用返回特定字符的函数如localeconv()返回.chr()函数可以构造字符但需要数字参数数字被过滤则不可用phpversion()返回的字符串中可能包含数字等。利用目录遍历函数scandir()列出文件current()、next()、end()、prev()等操作数组指针获取特定文件名。利用文件读取/显示函数show_source()、highlight_file()、readfile()、file_get_contents()后者需要echo输出。避免使用引号和点号通过函数嵌套直接传递参数而不是拼接字符串。7. 实战技巧与深度扩展通过这四道题我们掌握了从基础伪协议利用到高级无字符命令执行的多种技巧。在实际的渗透测试或CTF比赛中这些思路需要灵活组合。7.1 伪协议选择矩阵协议格式示例用途常见过滤绕过php://input?cphp://input(POST Body写代码)执行任意PHP代码过滤php关键词时不可用php://filter?cphp://filter/readconvert.base64-encode/resourceflag.php读取文件源码常base64编码过滤php、flag、resource等关键词时不可用data://?cdata://text/plain,?php code ?执行任意PHP代码过滤data、php标签时可能不可用可利用//注释后缀zip://?czip://path/to/archive.zip%23file.php包含zip包中的文件需要上传zip文件#需编码为%23phar://?cphar://path/to/archive.phar/file.php包含phar包中的文件可反序列化功能强大常用于反序列化利用链7.2 命令执行绕过技巧速查表当面对system()、exec()等函数过滤时除了代码层面的拼接在命令本身也可以做文章。过滤项绕过方法示例空格${IFS}、$IFS$9、、、%09cat${IFS}flag.php关键字单引号、双引号、反斜线分割c‘a’t、c”a”t、c\at关键字通配符?、*cat fl?g.php、cat fl*g.php关键字变量拼接ac;bat; $a$b flag.php命令使用替代命令cat-tac、more、less、head、tail、nl、sort、uniq、rev读取文件不使用cat用其他语言php -r ‘echo file_get_contents(“flag.php”);’禁止数字字母利用.当前目录、..上级目录scandir(‘.’)禁止数字字母利用内置函数返回特定字符localeconv()[0]-.7.3 常见问题与排查实录在实战中你可能会遇到以下问题php://input返回空白或错误原因allow_url_include未开启请求方法不是POSTPOST数据格式不正确。排查使用phpinfo()确认allow_url_include状态确保使用POST请求并在Body中发送有效的PHP代码如?php system(‘ls’);?。data://协议执行失败原因allow_url_include未开启代码中包含特殊字符未URL编码服务器配置限制。排查尝试对data://之后的所有逗号、尖括号等进行URL编码。例如编码为%3C编码为%3E空格编码为%20。使用函数嵌套时提示未定义函数原因某些函数可能在目标环境中被禁用通过disable_functions配置。排查先用phpinfo()或print_r(get_defined_functions())查看可用函数列表。优先使用最常见且通常不禁用的函数如scandir、current、next、file_get_contents、readfile。Payload构造后语法错误原因字符串拼接或闭合不正确。排查在本地PHP环境中模拟执行你的payload查看错误信息。特别注意引号的匹配、分号的添加、注释符的使用。对于include($c.”.php”)这种善用//或?来闭合多余部分。过滤规则判断失误原因正则表达式理解有误或者环境实际过滤与描述不符如web40的括号。排查提交简单的测试payload如?cecho ‘test’;观察返回。逐步增加特殊字符确定具体的过滤边界。有时过滤是“黑名单”模式可能存在遗漏。7.4 从CTF到实战的思考CTF题目是理想化的漏洞模型而真实世界更复杂。但原理相通代码审计永远是发现漏洞的第一步。像审计web37-web40一样寻找代码中不安全的函数eval,include,system等和未经验证的用户输入。防御思维作为开发者应使用白名单而非黑名单进行过滤。对文件包含应固定后缀或限定可包含的目录范围。对命令执行应尽量避免直接拼接用户输入或使用严格的转义函数如escapeshellarg()。绕过是永久的博弈没有绝对的安全。黑名单总有可能被绕过。因此安全的核心在于最小权限原则和输入验证与输出编码。