
1. 开篇RCE-labs Level 3/4 到底在考验什么RCE-labs 这套靶场很多新人应该都刷过。基本节奏是Level 1 给你一个直接能system()的输入框你输个id就出结果Level 2 稍微包一层过滤字符串拼一下照样打穿。但到了 Level 3 和 Level 4肉眼可见地卡壳了——常见的空格被吞掉、分号被拦、cat被替换成空字符串很多人第一次在这里对着一个靶场页面发呆半小时。我当初也是这样Level 3/4 卡了大概一个下午反复看代码、反复试 payload、反复在 Burp Suite 里看响应包最后才摸清楚这里的出题思路。这篇博文就是把我从 Level 3 打到 Level 4 的整个思考和操作过程记录下来从环境准备、代码审计、绕过滤、踩坑排查到最后的经验总结一次性讲清楚。这篇内容适合这几类人刚入门 Web 安全想在本地搭一套 RCE 练习靶场把命令注入的几种典型绕过姿势练熟的新人已经刷完 Level 1/2但卡在空格过滤、关键字过滤、字符拼接这些坎上的朋友准备参加 CTF 或者护网面试想系统梳理命令注入绕过技巧的人。先说清楚一个态度问题本文所有操作都在本地靶场或者你拥有授权的测试环境中进行。RCE-labs 这类靶场本身就是拿来给你合法练手的不要在真实系统上拿这些 payload 瞎试。做安全测试授权边界比技术本身重要得多。2. 从 Level 1 到 Level 4RCE 过滤是怎么一步步升级的2.1 先搞清楚你要绕过的是什么RCE 的核心场景用大白话说就是服务器把你的输入值拼进了一条系统命令里然后执行它。比如典型的 PHP 代码?php $cmd $_GET[cmd]; system($cmd); ?这种情况你传id系统就执行id你传;whoami系统就执行;whoami。命令注入漏洞的本质就是你控制了本该由开发者控制的命令参数.但现实中的靶场不会这么裸奔。出题人会在代码里加过滤常见的过滤手法有四种直接黑名单匹配如果输入的字符串里包含cat、flag、空格等关键词直接拒绝或替换为空。正则表达式拦截匹配更严格的模式比如只允许字母数字或者只允许某种格式。字符级别替换把空格替换为空字符串把分号和 替换为空这类替换逻辑往往可以绕过因为它们是一次替换而不是循环替换。参数拼接方式限制有的代码用system(ping -c 3 .$ip)这种拼接导致你输入的内容被放在命令末尾有的用exec($cmd)直接执行整个字符串。这两种利用方式完全不一样。RCE-labs 的 Level 3/4 就是在这个思路上逐步加码Level 3 把空格过滤了Level 4 把更多常见命令字符、关键字和分隔符一起过滤了。你要做的不是去网上抄一个 payload 粘进去碰运气而是根据过滤规则用绕过技巧构造出一个不触发过滤规则但仍然能执行命令的输入。2.2 命令注入的输入点在哪里做题之前先看代码。Level 3/4 的典型代码长这样以 PHP 为例实际上很多 Web 环境都是 PHP 写的?php $cmd $_GET[cmd]; if (preg_match(/\s/, $cmd)) { die(no space); } system($cmd); ?这是 Level 3 最常见的写法——正则匹配空白字符。\s在正则里代表空格、Tab 等空白符。你输入ls /etc时因为带了空格会被preg_match命中然后die(no space)拒绝执行。Level 4 就要狠一点?php $cmd $_GET[cmd]; $cmd str_replace(cat, , $cmd); $cmd str_replace(flag, , $cmd); $cmd str_replace( , , $cmd); $cmd str_replace(;, , $cmd); $cmd str_replace(, , $cmd); $cmd str_replace(|, , $cmd); system($cmd); ?这种写法属于黑名单替换而且是只替换一次不是循环替换。它把cat、flag、空格、分号、与、竖线全部洗掉。很多人到这里直接懵了命令都被洗光了还怎么打但实际上这种替一次的过滤反而给了我们很大的操作空间——后面你就能看到单次替换和循环替换对抗难度完全不在一个量级。2.3 为什么 Level 3/4 对新人是一道分水岭Level 1/2 考的是你会不会构造最基础的命令注入Level 3/4 考的是你懂不懂 Linux 命令、Shell 解析、字符编码的底层逻辑。这两者之间差的不是 payload 数量而是你理解命令执行链路有多深.举一个例子你知道cat /etc/passwd能读文件但你知道cat /etc/passwd在 Shell 眼里是怎么被拆解的吗是先按空格拆成几个 token再找cat程序去执行还是别的顺序其实 Shell 对命令的解析流程是先处理变量展开比如${IFS}、再处理路径展开比如*、?、再处理命令替换比如ls或$(ls)、最后才把字符串按空白分割成参数、找到对应程序、执行。过滤拦截的是你的输入字符串但执行的是展开之后的命令。这就是所有绕过的核心理论——你在利用过滤层看到的文本和Shell 实际执行的文本之间的差异。所以刷 Level 3/4就是对什么是 Shell 展开顺序、什么是变量替换、什么是通配符、什么是命令拼接这些东西做一次实战训练。把这些搞明白后面打更复杂的 RCE 靶场才有底气。3. Level 3 实操空格被过滤还有哪些分隔方式3.1 先搭环境把靶场跑起来我是在本地用 Docker 跑的一套 PHP 靶场。如果你手头没有现成的自己搭一个也不难准备一个 PHP 环境PHP 7.4 以上即可不需要额外扩展。写一个index.php里面是上面那种带preg_match(/\s/, $cmd)的代码。开一个 Apache 或者 Nginx或者直接用 PHP 内置服务器php -S 127.0.0.1:8080。浏览器打开http://127.0.0.1:8080/?cmdid确认命令能正常执行。这里提醒一下用 Docker 跑靶场一定要把端口映射到本机的回环地址不要直接映射到0.0.0.0因为你只是在自己电脑上做实验没必要让局域网里其他人访问到。安全习惯从环境搭建就得养成。3.2 搞清楚空格被过滤后有哪些替代方案先做一个最简单的测试输入?cmdid看到uid33(www-data) gid33(www-data)说明环境正常。再输入?cmdcat /etc/passwd页面会显示no space。空格被正则\s挡住之后Shell 命令里的单词分隔符还可以用下面这些替代替代方法写法原理适用性${IFS}cat${IFS}/etc/passwdIFS 是 Shell 默认的分隔符变量展开后是空格/Tab/换行最常见通用性最好Tab 字符cat\t/etc/passwdTab 也属于空白但\s是否拦截取决于靶场正则写法有的靶场只拦截空格不拦截 Tab可以试用重定向符cat/etc/passwd把文件内容重定向到cat的 stdin中间不需要空格只能用于读取文件不能用于执行命令重定向符ls /tmp/x输出重定向时不需要空格用于写文件时的分隔不常用这里面最推荐的就是${IFS}。原因是它利用了 Shell 的变量展开机制——你输入的是${IFS}过滤层看到的是字母、美元符和花括号不包含空格但 Shell 在执行前会把${IFS}展开成空格于是cat${IFS}/etc/passwd就变成了cat /etc/passwd。这里特别重要的一点是${IFS}展开后不只是一个空格而是 IFS 变量的完整值。默认情况下 IFS 是空格、Tab、换行所以它可以拿来当单词分隔符而且因为这是 Shell 内置机制不需要引号不会被过滤规则里的空格关键字拦掉。3.3 命令分隔符没有分号怎么执行多条命令过滤规则只写了\s也就是只拦空白没拦分号、竖线、逻辑与或。所以我们在 Level 3 里其实可以做很多额外动作。这里把命令分隔符的几个选项列一下;换行分隔前一条执行完再执行后一条不管前一条是否成功。例如ls;whoami。逻辑与前一条成功才执行后一条。例如curl http://x id。||逻辑或前一条失败才执行后一条。例如cd /xx || id。|管道符把前一条命令的输出作为后一条命令的输入。例如cat /etc/passwd | head -5。反引号或者$()命令替换把里面的命令结果放在当前位置。例如echo \id。换行符%0a或者%0d%0a在 URL 编码场景下把%0a解码成换行Shell 会把换行当成命令分隔符。拿 Level 3 的代码来试?cmdid%0acat${IFS}/etc/passwd是可以执行的——因为%0a解码成换行preg_match(/\s/)会拦截空格、Tab 这些但不一定拦截URL 解码后的换行字符具体要看靶场的过滤逻辑是在解码前还是解码后做的。这里有个非常容易踩的坑过滤顺序和 URL 编码解码顺序的先后关系。很多靶场是先preg_match再执行命令而 HTTP 请求到 PHP 时 URL 解码已经完成所以%0a会被解码成真实的换行符这个换行符如果不在\s的匹配里就能绕过空格限制。但如果靶场是先把用户输入做了一次urldecode再过滤那%0a就可能被拦住需要再编码一次。所以我一直强调做题一定要先做探针测试搞清楚过滤规则作用在你输入字符串的哪个环节。3.4 一条完整的 Level 3 payload 是怎么构造出来的假设目标是读取/etc/passwd过滤条件是\s空白符那么构造思路是命令本体cat /etc/passwd。把中间空格替换为${IFS}cat${IFS}/etc/passwd。检查有没有其他被过滤的字符——正则只拦空白当前 payload 没有空白。直接提交?cmdcat${IFS}/etc/passwd。如果是想读取无法直接从参数拼接的目录比如想查看当前目录那ls -la变成ls${IFS}-la提交后如果没显示完整结果可以考虑加一个换行比如ls${IFS}-la%0acat${IFS}flag*。我在实际操作中发现一个细节有些 PHP 版本在system()执行时如果命令里有${IFS}这种写法是能正常展开的因为展开发生在 Shell 层面PHP 只是把字符串传给/bin/sh -c。但如果靶场用的是exec()或者shell_exec()行为可能略有差异所以在多个函数环境下都要测一遍别急着下结论说这个靶场不能用${IFS}。还有一个小技巧当你不确定 payload 能不能过过滤时先在本地把同样的过滤代码跑一遍用 PHP CLI 直接执行比对着靶场页面反复盲猜效率高得多。我自己就习惯在本地写一个test.php内容和靶场完全一致然后用命令行传参测试确认能过过滤了再去靶场页面提交。4. Level 4 实操字符被洗白怎么把命令重新拼起来4.1 先分析 Level 4 的过滤规则Level 4 的代码比 Level 3 狠在两点一是替换变成了一次性替换多个关键词二是没有用正则而是用str_replace把cat、flag、空格、;、、|全部替换成空字符串。这意味着几个关键事实你输入的任何cat都会变成空字符串直接写cat /etc/passwd提交最终被执行的是/etc/passwdShell 会试图执行passwd文件报错。flag也一样如果文件名里有flag直接写cat flag会被洗成cat什么也读不到。空格、分号、竖线、与号全部没了等于多重限制叠加。但这套过滤有个天生的弱点str_replace只替换一次且不递归。也就是说你输入cacat第一个cat被替换成空后剩下的还是cat但实际上我们从输入侧看到的是cacat这个字符串过滤层把中间那一段cat吃掉了留下了ccat还是cat取决于匹配位置。这里需要构造一个替换完之后字符串里刚刚好出现原来被过滤的关键字的经典绕法。举个例子str_replace(cat, , $cmd)输入cacat第一个cat匹配的是第 2-4 位替换成空后字符串变成cat于是最终执行的命令里出现了一个完整的cat。这就是双写绕过.为什么有效因为str_replace做的是单次遍历替换不是替换后重新从头检查。它扫到第一个匹配就处理掉处理完就继续往后不会因为你替换出来的结果里又有cat而再做一次。这个特性在很多老靶场里都出现过是个非常经典的知识点。4.2 双写绕过的实操假设 Level 4 要读取的是一个文件名含flag的文件比如/flag.txt过滤规则里有flag那我们提交cacat /flafla-g.txt之类就过于复杂。需要先验证双写是否有效。先在本地测试$cmd str_replace(cat, , cacat); echo $cmd;输出结果确实是cat。那str_replace(flag, , flafla-g)的结果就是flag。这证明双写在当前过滤规则下是可行的。因此读取/flag文件的 payload 可以写成cacat${IFS}/flafla-g.txt过滤过程cat变成空flag变成空空格已经不存在用${IFS}绕了提交字符串变成cat /flaflag.txt或者cat /flag.txt命令成功执行。但这里有一个非常关键的地方替换顺序会影响结果。代码里替换顺序是先替换cat再替换flag再替换空格再替换分号……如果你构造的 payload 里既有cat又有flag的双写得保证替换完之后不仅没有残留的多余字符而且最终命令是你想要的。实际操作时建议在本地用同样的代码过一遍确认输出字符串等于你真正想执行的命令再去打靶场。4.3 关键字被过滤时用通配符和变量拼接绕过除了双写还有其他几种构造看不到关键词但能执行对应命令的方法。通配符法Shell 支持*、?、[]等通配符做路径匹配。如果只过滤了字符串flag那么cat /fla*或者cat /f???都能匹配到/flag. 过滤层看你的输入是fla*不包含flag这个子串所以不触发替换而 Shell 在展开路径时会把fla*匹配为flag文件最终执行cat /flag。实测中比较稳妥的写法cacat${IFS}/fla*前提是靶场当前目录下只有一个以fla开头的文件如果有多个会报错或者输出一堆内容。通配符的副作用是匹配到多个文件时行为不可控所以能用精确路径就别用通配符能用通配符就尽量把匹配范围缩小。引号拼接法Shell 中cat和cat拼起来之后还是cat因为引号里的内容是空字符串拼接后不影响命令名。过滤层看到的是cat不包含cat子串所以能绕过。cat${IFS}/flag.txt这个方法的优点是通用性强缺点是一些靶场的过滤正则可能也会匹配引号或者系统启用了奇怪的自身防护所以需要测试。反斜杠法ca\t中的反斜杠在 Shell 里会转义t但如果是ca\t这种写法实际上反斜杠会让 Shell 把t当成普通字符形成cat。不过这个方法和 Tab 字符可能混淆要小心。更常见的写法是c\at或者ca\t, 但某些平台会有一点差异我在 Linux 里实测有效但在部分 Docker 环境里也可能被解析成别的所以推荐优先级低于引号拼接法。变量拼接法利用 Shell 的变量赋值特性构造一个字符串ac;bat;$a$b${IFS}/fla*更简洁的是用${PATH}或其他系统变量取值截取字符但比较难记适合进阶玩法。对于新人记住通配符和引号拼接两种基本足够了。4.4 多条命令被禁止时用管道和命令替换做组合Level 4 把|和都过滤了但%0a换行符没有被提到过滤规则里所以依然可以用换行来做命令分隔。比如cacat${IFS}/flafla-g.txt%0awhoami这样先读文件再执行命令两个动作互不干扰。如果换成管道符被过滤也可以用命令替换来间接组合echo$(cacat${IFS}/flafla-g.txt)$(...)里面的命令会先执行然后把结果放在echo后面。整个 payload 里不包含空格、不包含cat、不包含flag、不包含和|但照样能读取文件内容。这里有个经验遇到过滤条件很多时不要试图一次构造完美 payload。先把你的目标拆成最小动作比如读取某个文件、执行 whoami、打印当前目录然后针对每一个动作去解决过滤问题最后再把动作串起来。我刷 Level 4 的时候就是这种打法先解决cat的过滤再解决空格再解决文件名的flag每解决一个坑就拼一段最后整个 payload 跑通的时候非常有成就感。5. 实战遇到的问题与排查技巧5.1 最常见的三个坑坑一${IFS}在某些系统上不生效不是所有 Shell 都支持${IFS}的展开方式。如果靶场用的/bin/sh指向dash${IFS}依然有效但如果环境是 Windows 或者奇怪的沙箱可能不适用。遇到这种情况先提交?cmdecho${IFS}test看返回结果是不是一个空格加test。如果输出不是那说明这个系统不支持换用%09Tab或者重定向法。坑二过滤替换的顺序导致 payload 被破坏比如你写cacat${IFS}/flafla-g.txt如果cat替换发生在flag替换之前整体没问题但如果某个版本把flag替换放在cat之前那你的flafla-g会被先洗成fla-g后面的cat双写可能就出问题。所以在不同靶场里同一个 payload 的结果可能完全不一样必须以实际返回为准。坑三URL 编码问题在浏览器地址栏里直接输入%0a浏览器可能不会把它编码保留而请求库在发送时也会做一层 URL 编码导致你看到的 payload 和服务器收到的不一致。建议一律用 Burp Suite 手动发包用 Repeater 调试因为你能看到请求头里的原始字节。我自己刷这种靶场从来没在浏览器地址栏里试过最终 payload都是 Burp 里改完直接发。5.2 系统性的排查思路当 payload 不执行先别乱试。第一步看回显。如果页面返回报错信息仔细读报错内容它会告诉你 SQL 语法错误、文件不存在、命令找不到等等这些都是重要线索。第二步看响应时间。有的 payload 会卡住比如ping -c 1000 127.0.0.1这种说明命令确实被执行了只是阻塞在等待。通过响应时间可以判断命令是否执行成功。第三步看请求头。用 Burp Suite 观察服务器返回的 Set-Cookie、Server 字段等确认后端是 Apache 还是 Nginx是 PHP 还是 Java不同语言对命令执行的支持情况不一样。第四步用探针 payload 做排除法。目的探针 payload预期结果说明测试命令执行是否正常id返回 uid 信息如果这个都失败说明命令执行链路有问题过滤先放一边测试空格替代是否有效echo${IFS}test返回test如果能出test空格问题已解决测试cat双写是否有效cacat${IFS}/etc/passwd返回文件内容成功说明双写绕过有效测试换行分隔是否被过滤id%0awhoami连续输出两个结果如果能出两行说明换行可用测试通配符是否可行cacat${IFS}/fla*返回匹配到的文件内容如果报错信息提示匹配到多个文件说明通配符可用按这个顺序做排查基本 10 分钟内能定位卡点。5.3 我这几次操作里面的一些体会第一别迷信网上的 payload 大全。每个靶场过滤规则不一样网上 payload 大概率是给另一个环境准备的直接搬过来通常失败。真正有效的做法是理解过滤规则、理解 Shell 解析、理解目标行为然后自己一步步拼出来。这个过程看起来很慢但非常锻炼能力之后换一个靶场马上就能适应。第二本地复现靶场逻辑是最高效的学习方式。我刷 Level 3/4 的时候直接把靶场代码抄了一份到本地 PHP CLI 里跑改一个参数测一个结果几分钟就能试几十个 payload。等确认了过滤规则和绕过方向再回靶场页面验证。这样既不会把靶场环境搞坏又能快速验证假设。第三善用 Burp Suite 的 Intruder 来做批量测试。比如你要验证cat、cat、c\at、cat、ca\t这些写法哪个能被过滤直接写一个 payload 字典跑一轮 Intruder响应码和回显差异一目了然。虽然这一轮你可能会看到一堆 500 报错但你可以通过回显内容快速定位到通过的关键写法。这个思路在后面打别的靶场时同样适用。第四多动手理解 Shell 的展开机制。比如${IFS}为什么能展开成空格、双写为什么能绕过、通配符为什么能匹配文件名这些知识光看文章记不住一定要自己在一个真实的 Shell 环境里跑一跑看一下结果。我后来发现很多我以为很难的过滤规则只要你真正懂了 Shell 的执行顺序就发现绕法其实就那么几种翻来覆去都是基于过滤层看到的东西和 Shell 看到的东西不一样这一点。RCE-labs 的 Level 5/6 还有更复杂的环境比如命令被拼到exec里面、过滤函数用到正则循环、甚至结合了二次 URL 解码和 Base64 编码但这些都属于同一套底层原理的变种。Level 3/4 这一关打过去之后最大的收获不是背会了十几个 payload而是你开始用代码审计 过滤分析 Shell 执行原理的思维去看待一个注入点而不是到处搜索空格绕过 payload。最后送大家一个实战小技巧拿到任何 RCE 靶场先花 20 分钟把代码读一遍把过滤规则列成一个清单比如空格不能出现cat 不能出现分号不能出现flag 不能出现然后针对每一项去找对应的绕过方案最后再把这些方案组合起来。这个清单式打靶法我在刷很多靶场时都用效率远高于对着页面盲试。刷完这套题你会回来感谢那个愿意花时间读代码的自己。