
老实说我一开始在RCE-labs前几关过得很顺。命令注入嘛不就是拼接分号、管道符、与符号这三板斧页面把输入原样扔给系统命令闭合掉前缀就能拿到执行结果。直到Level 5和Level 6启动代码里直接把这些符号和空格全部过滤掉我第一反应是这题还能怎么做。如果你现在也卡在这两关别急着去翻题解先跟我一起把代码读一遍你会发现所谓的高难度只是障眼法真正的考点是shell解析本身。这里说的RCE-labs是一个专门给新手练远程命令执行RCE的靶场从简单到复杂一共多关前几关练基础分隔符后面练过滤绕过几乎把命令执行漏洞的常见考法都覆盖了。这篇文章就把Level 5、Level 6的完整通关思路、背后的shell解析原理以及我踩过的那些坑一次性讲清楚适合刚学完Web安全基础、知道HTTP请求和PHP语法但还没动手练过靶场的同学参考。1. 为什么新人学Web安全要先啃RCE这块硬骨头1.1 RCE到底是什么业务里在哪里出现RCE全称Remote Code Execution远程代码执行但在打靶时通常被更细化为命令执行Command Execution。两者有区别一个是让目标执行任意PHP代码一个是让目标执行任意系统命令。这个漏洞的本质是代码里把外部输入直接拼进了系统命令之后执行。典型场景比如网站后台的Ping检测功能你输入一个IP服务器拿到后拼成系统ping命令去执行如果开发者图省事直接用了字符串拼接那就等于把一个命令行终端交到了用户手里。新人容易把这部分当纯攻击技巧看其实它是所有Web漏洞里最接近本质控制的一种。一旦确认命令执行点通常意味着直接拿到了服务器上命令行的一部分能力。也正因为这样RCE-labs这类题目在新手路线图里位置很靠前——它考的不是复杂的逻辑漏洞而是你对自己常用工具链和系统命令的理解程度。我在某实验室的模拟项目里见过一个线上案例一个老旧管理后台的连通性检测功能就是把用户输入的地址直接拼给shell结果整个内网被摸了个遍。业务功能越不起眼越容易藏这种问题。1.2 新人为什么绕不开它我见过不少新人跳过靶场直接去看真实目标的漏洞报告结果面对一堆POC代码完全看不懂因为里面充满了各种绕过技巧和编码变换。而RCE-labs的好处在这时候就体现出来了它把漏洞的完整链路收敛在一个极小的demo里level1到level4基本只考分隔符level5、level6开始上过滤逼着你理解字符在被拼接进shell之前经历了什么。有了这个基础后面再看命令执行的真实CVE你才有能力去分析补丁到底堵住了哪里。这里顺便说一句路线选择。如果你不是专门做漏洞研究的只想知道怎么防RCE打靶依然值得做。因为只有亲手构造过一次绕过你才会真正理解为什么推荐用白名单而不是黑名单为什么检查输入格式比替换危险字符更可靠。防御的前提是理解攻击路径而不是背几条开发规范就完事。2. 靶场准备与Level 5关卡拆解2.1 先把RCE-labs跑起来RCE-labs本身需要PHP环境。如果你有Docker直接用项目仓库里带的容器配置启动最省事一条命令就能把整个靶场拉起来。如果你不想用Docker手边有PHP 7以上环境也可以直接把源码丢到Web根目录访问即可。需要注意的一点是Linux环境下做练习最好因为Linux shell和PHP的命令执行函数配合更典型换行符、环境变量等考点在Windows下会失效。我第一次就是拿Windows搭的结果整体体验很别扭后面换到Linux容器才顺畅起来。访问靶场时建议用浏览器的无痕窗口排除浏览器插件对请求的干扰尤其是那些会自动解码URL的插件后面会坑你。2.2 Level 5源码里藏着的过滤逻辑打开界面以后先把页面拿到的源码简单读一遍。Level 5大致长这样?php $cmd $_GET[cmd]; $cmd preg_replace(/[;|]/, , $cmd); echo pre; system(ping -c 1 . $cmd); echo /pre;核心逻辑就两行接收cmd参数把分号、竖线、与符号统一替换成空字符串然后拼进ping命令执行。后端的Ping功能给了回显这对新手来说等于难度打折了——你能直接看到命令是否生效。要注意这里用的是preg_replace作用是删除而不是拦截。页面本身不会报forbidden而是非常客气地把危险字符删掉再执行剩下的命令。这意味着它不会明确告诉你哪里被拦了只会默默改变你的payload让你在调试时产生大量困惑。我第一次就花了不少时间才确认分号真的被删干净了而不是payload写错了。2.3 从直球失败到换行符绕过一句话payload的完整推导先试最直觉的payload?cmd;id结果发现id前面的分号被删掉了ping收到的是ping -c 1 id系统提示命令找不到。再试|id、id结局一样。这时候如果只盯着这几个符号思路就断了。真正关键的一步是问自己在Linux shell里除了分号和管道符什么字符能分隔两条命令答案是换行符。敲命令的时候你按回车才执行这个回车在shell解析层面就是一个命令分隔符。而换行符发送过来时我们可以在请求里把它编码成%0a。于是payload变成了?cmd127.0.0.1%0aid服务器收到后解出换行符实际拼出来的是ping -c 1 127.0.0.1 id第一行ping正常执行第二行id被执行结果是当前用户名和权限信息。Level 5就这样通了。后来我翻PHP手册才更加确认system和shell_exec在底层都是调用shell来跑字符串shell自然会把换行符当作命令边界所以这个绕过其实不是漏洞链而是shell自身的合法行为。你要做的只是找到过滤规则没覆盖到的合法分隔符。这里有个细节还要提醒用Burp发请求时要先确认%0a没有被自动解码再手动解码两遍有些版本会帮你decode有些不会搞不好payload会多一层编码问题。新手可以先用浏览器地址栏直接测成功率反而高。我当时用浏览器实测一次就通了反而是在Burp里折腾了好一会儿。3. Level 6空格过滤之后的二次绕过3.1 这次的过滤升级点在哪里到了Level 6页面提示现在命令里不能有空格了。同样先看代码?php $cmd $_GET[cmd]; $cmd str_replace(array( , , ), , $cmd); echo pre; system(echo . $cmd); echo /pre;这次连空格、单引号、双引号都删掉了。想想看之前用的127.0.0.1%0aid到这里会变成什么空格没了如果把换行符后面的id和前面的IP拼在一起系统命令里127.0.0.1%0aid会被当成一个整体面对echo 127.0.0.1id显然不是我们想要的。题目给的回显点是echo意味着我们只需要让echo输出命令执行的结果。这时候脑子里要建立一个新的目标构造一段没有空格、没有引号却能让shell执行命令的字符串。这里我建议你先动手试几个版本比如127.0.0.1%0a$(id)看看会发生什么再往后读。3.2 Tab、${IFS}与Linux命令解析的冷知识Linux shell里区分命令和参数、参数和参数的是空白字符而空白字符并不只有空格一种。Tab键制表符也是合法分隔符在URL里对应%09。所以最朴素的绕过就是拿Tab代替空格?cmd127.0.0.1%0a%09id不过这种写法有个局限如果题目过滤得再狠一点把Tab也换了就又要换方案。另一个更常用的替代品是${IFS}。IFS是shell里的内部字段分隔符Internal Field Separator默认值就是空格、Tab、换行。关键点在于${IFS}作为一个整体会先被shell展开成一个空白字符而它本身不包含空格所以能穿过过滤。于是可以这么写?cmd127.0.0.1%0a${IFS}id拼出来的结果相当于echo 127.0.0.1 id由于这次没有分号过滤如果不想用换行也可以写成127.0.0.1;${IFS}id。我在实测中发现两种写法都可行但换行配合IFS的写法更能体现本关意图也更容易泛化到更多场景。3.3 最终完整通关payload实测可行的payload是?cmd127.0.0.1%0a${IFS}id如果你接下来想执行cat /etc/passwd这种带斜杠、带空格的复合命令可以写成?cmd127.0.0.1%0a${IFS}cat${IFS}/etc/passwd任何地方的空格都用${IFS}顶替命令中的/没有被过滤所以不用管。这一关最后的考点总结起来就一句话过滤空格不代表过滤了所有空白符shell的字段分隔靠的是一组字符而不是单纯的空格键。打到这里我更加确定RCE-labs的设计是一层层踩在上一个解法上做升级的。level5你以为换行符就是终点level6立刻告诉你换行符不够用还得叠加IFS。这种递进式的关卡对新人建立绕过思维特别有帮助。4. 打完这两关该建立起来的防御意识4.1 PHP里常见的命令执行入口我在前面两章讲的全是进攻视角接下来必须换个身份说说防御。因为靶场再好玩最终落到实际工作里我们都希望自己写的代码不是下一个RCE入口。PHP这边常见的命令执行函数有这么几个system、exec、shell_exec、passthru、popen、proc_open还有反引号运算符。它们的共同点是都会把传入的字符串交给shell去解析。区别主要在于回显方式system直接输出结果exec和shell_exec返回结果字符串passthru适合输出二进制流。很多开发者在业务里用这些函数处理用户传参时根本没意识到拼字符串这个动作本身有多危险。我列一个表方便对照函数是否有回显常见用法system直接输出简单执行并打印结果exec最后一行的输出拿字符串结果进一步处理shell_exec所有输出收集完整命令输出passthru直接输出原始流执行二进制程序反引号所有输出和shell_exec等价要注意PHP本身还提供了escapeshellarg和escapeshellcmd这类转义函数但它们并不能作为万能防御。escapeshellarg会把参数包上一层单引号这东西在拼接场景里有时反而会被用来做参数注入所以不能盲用。4.2 业务代码的三种防御做法第一种是白名单。比如Ping检测功能就让用户只能从预定义的几个主机IP里选不给任意输入的机会。这是最稳的但也最容易被业务方嫌体验差。第二种是参数化。PHP里没有直接对应参数化系统命令的万能封装但你可以把可变部分从命令字符串里剥离出去。比如先固定命令模板用户输入只作为单独的参数数组传给proc_open而不是拼进字符串。这思路和SQL预处理预防注入是同一个原理只是落地更麻烦因为shell参数传递不像数据库驱动那样统一。第三种是输入校验。如果业务确实需要接受主机名/IP那就用严格的格式校验比如用filter_var($input, FILTER_VALIDATE_IP)或者正则校验IP格式只有校验通过后才拼命令。这个方案比黑名单过滤可靠得多。我在某模拟项目的后台功能里就见过一个反面教材开发者用正则删掉了所有字母和数字以外的字符结果没多久就被人用通配符绕过了。4.3 为什么过滤方案永远治标不治本Level 5、Level 6本身就在告诉你一件事任何黑名单过滤都能被绕过。删掉分号和管道符出现了换行符删掉空格和引号出现了IFS。往后还可以有各种奇奇怪怪的变体比如通配符、花括号展开、环境变量拼接等等。黑名单就像不断打补丁的墙而shell本身就提供了一个巨大工具箱你不可能认识所有工具。所以如果你在真实项目里维护一段旧代码临时加过滤可以理解但长期方案一定是改成白名单或参数化。这也是我打完这两关以后最大的心态转变从我要构造更多payload变成了原来开发时一个不经意的拼接就埋了雷。打靶不是让你去外面折腾别人的系统而是让你在自己的环境里把系统底层的执行机制搞明白进而在写代码时知道哪些地方必须设防。5. 新人打RCE-labs最容易踩的坑与自查清单5.1 我踩过的几个带真实感的坑第一个坑是Windows环境判断失误。Level 5用换行符绕不过去我一度以为是代码理解错了后来才发现Windows下ping命令的语法、底层的cmd.exe解析都和我预期不一样导致整个调试过程很混乱。建议直接用Linux容器或虚拟机Windows只用来发HTTP请求。第二个坑是URL编码乱套。在Burp里输入payload时有些插件会自动解码有些不会导致实际到达后端的字符串多了一层或少了一层人眼看不出的编码。遇到类似情况先在后端加一个临时日志记录原始接收值把所有字符的十六进制打出来一眼就知道问题在哪。我为了排查这问题顺手把整个靶场代码复制了一份加了中间日志才算看清整个过程。第三个坑是只盯着回显。Level 5有回显给了很大便利但很多真实场景是没有回显的。这时不要慌可以借助延时、外带请求等手段判断命令是否真的被执行。靶场练熟以后建议找个有源码的类似环境把隔离回显关掉再试一遍不然容易形成依赖。第四个坑是忽略了shell解释顺序。有人会把${IFS}直接放在变量开头结果shell先做变量展开再做命令查找导致整个命令格式错误。要记住展开动作发生在命令解析之前所以${IFS}whoami最终会变成whoami前面多了一个看不见的空白命令查找时是忽略的但拼接在中间时就不能乱放。5.2 一套可复用的过关自查顺序最后总结一个我可以直接抄的checklist先读代码明确过滤规则到底删掉了哪些字符而不是凭经验乱猜。判断输入点拼接在命令的哪个位置是中间、后面还是整体参数。优先尝试标准分隔符分号、竖线、与符号、换行。如果空格被过滤立刻切换Tab或IFS并确认请求里有没有二次编码。构造命令时先执行一个简单的id或whoami确认命令确实被执行再考虑读文件等下一步操作。回显异常时不要只调payload也要检查后端环境、字符编码、请求方式等外部因素。这套顺序在我后来处理其他类似靶场时也很好用核心就是把猜测换成验证。每次卡关都先问一句我的假设是建立在哪条规则上的如果这条规则本身错了那payload再花哨也没用。打完Level 5和Level 6之后我最大的收获倒不是多了几个payload而是养成了一个习惯每次拿到一个PHP命令执行点先不是急着凑黑名单绕过而是把它当作一次shell解析实验。你把输入放在哪个位置、shell怎么切分、环境里有什么可用的变量这些底层问题想清楚了类似的关卡换多少种过滤你都不怕。RCE-labs确实适合新人但前提是你别只背答案而是真的把每一关的过滤规则和绕过原理读进去。带着这套理解再往后闯关你会觉得后面几关反而没那么难了。