
最近带几个零基础的朋友玩本地Web靶场第一关就卡了不少人。他们不是不会输入命令而是卡在环境“启动”上有人端口没对有人根目录指错了还有人压根没安装PHP。所以我想把RCE-labs这套东西从clone到跑起来再到level0、level1通关完整拆开讲一遍。这篇内容不讲虚的直接照做就能跑。1. 先搞清楚RCE-labs到底是让你练什么1.1 什么是RCE漏洞RCE这个缩写在Web安全里基本属于“重量级”漏洞。它指的是远程代码执行或者叫远程命令执行也就是攻击者输入的字符串被服务器当作代码或命令的一部分去执行了。很多人第一次看到这个词觉得很高深其实落到代码层面就那么几行。PHP里常见的有system()、exec()、shell_exec()、passthru()只要用户输入能影响这些函数的参数就很可能形成RCE。我打个比方。你去一家餐厅取餐店员拿大喇叭喊“请张三到前台取餐”。这个张三就是用户输入喇叭就是命令执行函数。正常情况下你报上张三的名字喊的就是张三。但如果店员不检查你报的名字你偏要说“请所有顾客都到前台来领餐”喇叭也会照样喊出去整个餐厅就乱套了。放在Web应用里这就是一次命令注入。RCE-labs这种靶场就是把这件事拆成不同等级让新人先在一个无保护的环境里看到命令是怎么执行起来的再一点点加上过滤逼你去找绕过方法。它最大的优点是代码短、依赖少读完源码以后你不会有“是不是因为我不会用工具”的挫败感而是真的能想明白漏洞为什么存在。1.2 level0 和 level1 分别想训练什么level0是整个靶场的第一关。它的代码就是一个简单的system($_GET[cmd])。你传什么服务器就执行什么。这关的目标不是让你死记硬背一个利用语句而是让你理解三件事第一用户输入从哪里进入后端第二系统命令是怎么被拼接的第三为什么一个看起来无害的参数能控制一台服务器。level1接着level0往下走。代码变成先拼一个固定前缀ping -c 1再拼用户输入。同时加了一个黑名单过滤掉分号、管道、反引号等常见命令分隔符。这一关想训练的是“绕过滤规则”的意识。很多新人上来就试;ls或者|cat /etc/passwd发现被拦截就卡住了但实际上绕过方式非常多后面我会演示其中一种。这两个关卡组合在一起其实是在给新人铺一条很缓的坡。level0让你建立信心level1让你知道RCE不是只会一个payload就能通关它背后是shell语法、URL编码、代码逻辑共同作用的结果。1.3 为什么“启动”反而是新人最大的坎我见过好几个朋友拿到类似靶场以后卡在最前面一个小时不是原理没看懂而是环境没跑起来。要么打开页面是404要么PHP命令找不到要么端口被占用要么输入了命令但页面空白。这些问题看起来小但对新人来说非常劝退。所以这篇内容我会把“启动”拆得很细。你不需要懂Docker不需要装数据库只需要一台装了PHP的电脑用一条php -S命令就能跑起来。先把环境搞明白后面打level0和level1才有意义。2. 启动前的准备环境、目录、命令2.1 环境清单与安装方式RCE-labs是PHP写的所以最核心的依赖就是PHP。建议PHP 7.4以上实际上PHP 5.6也能跑但现代版本更规范。不需要数据库不需要Composer不需要Nginx用PHP自带的开发服务器就够了。各系统安装方式不同。Linux里多半直接用发行版包管理器sudo apt install php-cli装完用php -v验证。macOS可以用brew install php。Windows麻烦一点从PHP官方下载zip包解压到比如C:\php然后把C:\php加进系统的PATH环境变量再新开一个终端窗口执行php -v。如果安装对了终端会显示PHP版本号。为什么我不推荐新人用Docker因为Docker本身又是一个新概念对新人来说等于在“学漏洞”之前先“学容器”门槛反而抬高。只有当你需要和目标靶场环境完全一致或者不想污染本机环境时才值得用Docker。这里先清楚自己的目标你是来练RCE的不是来学容器的那就别给自己增加额外负担。2.2 项目目录结构拿到压缩包解压以后你会看到里面有一个www目录这才是Web站点根目录。不要直接在项目根目录启动服务否则访问路径会带上子目录容易乱。标准结构大致是rce-labs/ ├── www/ │ ├── config.php │ ├── level0.php │ └── level1.php └── readme.mdconfig.php用来放flag也是我们最后要读的目标。level0.php和level1.php是关卡页面源码加起来可能不到20行。你完全可以打开文件读一遍了解漏洞产生的原因。很多新手不敢看源码觉得那是“高手”才做的事实际上这就是一个普通文本文件用记事本或VSCode打开就行。2.3 启动命令与验证方式在项目根目录执行php -S 127.0.0.1:8080 -t www-S表示启动PHP内置服务器127.0.0.1:8080是监听地址和端口-t指定Web根目录为www。终端出现类似PHP 7.4.3 Development Server started说明启动成功。服务会一直运行按CtrlC可以停止。验证服务是否正常浏览器访问http://127.0.0.1:8080/level0.php如果看到输入框或提示就说明OK。更稳的验证是打开另一个终端执行curl -I http://127.0.0.1:8080/level0.php返回状态码200就是成功。如果端口被占用把8080改成8081例如php -S 127.0.0.1:8081 -t www然后访问新地址。这里有一个习惯建议监听地址只写127.0.0.1不要写0.0.0.0。因为0.0.0.0会把服务暴露到局域网其他设备也能访问。虽然是学习用的但少给自己惹麻烦总是好的。2.4 启动后立刻要做的小检查服务起来后先访问一次首页然后回到启动服务的终端。你会看到一条访问日志比如[Sun Jul 20 14:10:00 2025] 127.0.0.1:54321 [200]: GET /level0.php这个日志非常有用。当你后面构造payload发现没反应时可以先看这行日志确认请求到底有没有打进来、状态码是多少。很多人漏掉这一步结果排查了半天其实请求根本没有到达后端。所谓“启动”不仅是把服务跑起来还要知道它有没有正常工作。3. level0实战让一个system()听你使唤3.1 先读源码再动手打开www/level0.php内容大概率是这样?php $cmd $_GET[cmd] ?? ; system($cmd); ?第一行从GET请求参数cmd里取值第二行直接扔给system()。system()是一个PHP内置函数把传入的字符串交给系统shell执行然后把输出直接返回给浏览器。它和你在终端里敲命令几乎没有区别。所以当你在浏览器里访问http://127.0.0.1:8080/level0.php?cmdls服务端就执行了ls当前目录下所有文件会出现在页面上。如果你看到包含config.php的文件列表恭喜第一个RCE已经打通了。3.2 信息收集先看看我是谁拿到命令执行权限后第一件事不是急着读flag而是先确认当前环境和权限。这不是浪费时间而是真实测试中的标准操作。可以依次试试cmdid查看当前用户权限很多靶场里PHP进程权限并不高。cmdpwd查看当前工作目录。cmdls -la查看所有文件包括隐藏文件。cmduname -a查看系统内核版本这个在真实环境中会被用来判断有没有可利用的本地提权漏洞。这些命令本身没有恶意它只是让你熟悉“拿到命令执行后怎么确认环境”。在靶场里这更是标准操作因为不同环境里文件路径、权限可能完全不同先收集信息能避免你瞎猜。3.3 读config.php拿到flag看到文件列表里有config.php后执行http://127.0.0.1:8080/level0.php?cmdcat%20config.php%20是空格的URL编码形式。写成catconfig.php也可以因为URL里的加号会被解析成空格。如果你不放心直接用curlcurl http://127.0.0.1:8080/level0.php?cmdcat%20config.php这时页面要么直接输出flag{...}这样的字符串要么输出PHP源码。如果浏览器里看着不对右键“查看网页源代码”或者看curl的原始输出。3.4 一个容易被忽略的点URL编码在URL参数里、|、;、空格、引号这些字符都可能被浏览器或服务器特殊处理。所以只要命令里出现这些“保留字符”最好都转成URL编码。比如想执行ls -la /etc | grep passwdURL要写成?cmdls%20-la%20%2Fetc%20%7C%20grep%20passwd其中|的编码是%7C/的编码是%2F。实际打靶时也许不编码也能用但在自动化脚本里不编码就很容易导致payload被截断。从level0开始养成编码意识后面能少踩很多坑。3.5 这关给出的三个核心收获第一你看清了system()是个“无脑执行者”它不关心字符串是谁拼出来的。第二你掌握了GET参数控制函数参数的完整链条。第三你学会了看原始输出和URL编码。这些基础没打好后面看再多高级绕过技巧都是空中楼阁。4. level1实战黑名单面前换行符站了起来4.1 先看看level1的“过滤规则”打开www/level1.php内容大概是?php $ip $_GET[ip] ?? ; if (preg_match(/[;|]/, $ip)) { die(forbidden); } system(ping -c 1 . $ip); ?页面模拟的是“ping工具”输入IP后端拼一个固定的ping -c 1命令再执行。这里的过滤规则是如果用户输入里包含分号、与符号、管道符或反引号就直接终止输出forbidden。这是很典型的黑名单防御。试一下常见payload?ip127.0.0.1;ls页面会显示forbidden。这时很多人会想不能用分号怎么在ping后面再执行一条命令答案是换一个shell认识、但正则没列出来的分隔符。4.2 为什么换行符能绕过Shell用来分隔命令的符号不只是分号换行符也一样。ping -c 1 127.0.0.1这是一个完整命令行如果后面接一个换行符再写cat config.php那么shell看到的就是两行命令ping -c 1 127.0.0.1 cat config.php执行完第一行会继续执行第二行。背后的原因在于system()在执行时会把字符串原样交给/bin/sh解析而换行符在shell语法里同样代表一条命令的结束。正则/[;|]/只匹配分号、与符号、管道符和反引号没有包含换行符\n。所以我们可以用URL编码的%0a 把换行符传进去绕过黑名单。4.3 完整payload与执行过程访问http://127.0.0.1:8080/level1.php?ip127.0.0.1%0acat%20config.php这里有个小细节%0a后面的空格直接写在URL里也可能被服务端正常解析但为了更通用最好把空格也编码成%20。上述请求里%0a是换行符%20是空格实际传给系统的字符串就是ping -c 1 127.0.0.1 cat config.php执行后页面上会先显示ping的输出然后显示cat的结果。如果ping的输出很长往下滚动找。用curl看最清晰curl http://127.0.0.1:8080/level1.php?ip127.0.0.1%0acat%20config.php4.4 再举几个同类绕过思路换行符不是唯一选择。如果黑名单加了换行符可以试试用制表符%09代替空格比如?ip127.0.0.1%0als%09-la。用${IFS}代替空格${IFS}在shell里就是默认的分隔符不需要空格。用$()做命令替换虽然本关可能用不上但后面很多关卡会考。真实的过滤场景比这复杂得多经常是黑名单加白名单组合。但核心思路始终一样先搞清楚过滤到底拦了什么再想系统里还有什么符号能实现同样的语义。这也是RCE-labs想锻炼的思维能力。4.5 防御者视角这个正则为什么不够黑名单永远不是安全边界。因为shell语法非常丰富总有你想不到的符号能拼出命令。与其猜攻击者会用哪种分隔符不如直接禁止整个输入进入系统命令。放在这个案例里最简单可靠的修复是校验IPif (filter_var($ip, FILTER_VALIDATE_IP) false) { die(invalid ip); } system(ping -c 1 . $ip);IP只有一种合法格式校验通过后就不可能夹带命令。这个思路叫“白名单校验”它比黑名单拦截要可靠得多。这也是为什么很多安全测试报告都建议开发者“能不用系统命令就不用必须用的时候就做严格的输入校验”。5. 打靶路上最常见的四个坑5.1 服务起不来或页面404这个坑八成出在命令路径上。如果你启动时用了php -S 127.0.0.1:8080 -t www但当前目录不在项目根目录那么-t www可能指不到任何地方。终端会提示文件找不到浏览器就会404。先执行pwd看看当前目录再用ls确认里面是不是有www文件夹。另一个常见情况是PHP没装或者装错版本。先执行php -v如果提示“命令找不到”那就是PATH问题。Windows用户记得修改环境变量后要重开终端否则新加的路径不会立即生效。5.2 输入payload后页面空白如果请求地址对了但页面空白优先确认参数名。level0用的是cmdlevel1用的是ip。很多人把cmd带到level1里测试当然没有反应。然后确认是否被过滤了如果页面显示forbidden说明正则拦截生效。还有一种情况是命令本身出错。比如level1里写?ip127.0.0.1%0azzzshell会提示command not found页面也会把这个错误信息回显出来。这不一定是漏洞失效而是命令不存在。先试几个最基础的命令比如ls和id能排除这类问题。5.3 curl和浏览器看到的输出不一样浏览器有时候会把返回内容里的、当成HTML标签解析导致你看不到原始字符串。比如cat config.php返回的内容里有一句?php $flag... ?浏览器可能不显示中间的变量。这不是漏洞的问题是显示问题。解决方式很简单在浏览器右键“查看页面源代码”或者直接用curl拿原始字节。5.4 换行符被转义或过滤在level1里如果你在浏览器地址栏手动输入%0a有些浏览器可能不会原样传给服务端而是尝试解析或拦截。这种情况先用curl验证curl http://127.0.0.1:8080/level1.php?ip127.0.0.1%0acat%20config.php如果curl能通浏览器不通基本可以确定是浏览器或输入法的问题。用curl发请求是打靶的必备技能早点学会能避开很多“假故障”。6. 从靶场回到现实RCE是怎么发生的又该怎么防6.1 RCE在真实应用里的三个高发入口第一次看完level0和level1你可能会觉得这漏洞太傻谁会这么写代码现实是系统命令执行需求非常常见稍不注意就会留下口子。常见高发入口有几个。一是网络诊断工具比如页面提供一个“ping一下某个IP”的功能多半就是直接拼接命令。二是文件处理比如音视频转换、压缩解压文件名参数如果没有过滤也能带出命令。三是自动化运维后台脚本里面经常要调外部命令参数来源复杂一个没校验就是RCE。在这些场景里RCE的利用不需要多高深一个特殊符号就够了。6.2 最靠谱的修复方式在代码层面防御RCE要分几步走。第一尽量减少执行系统命令的入口能用PHP函数解决的不用shell。第二确实要执行时避免直接把用户输入拼接进命令字符串。第三用白名单校验输入比如IP就用IP校验器文件路径就用真实路径匹配。第四如果必须经过shell也可以考虑escapeshellarg()对参数做转义但它不是万能所以前三条优先级更高。把这个靶场两个level的代码拿回去改一下你会更直观地看到“为什么修复比添加过滤更有效”。把黑名单换成IP白名单再打level1试试还有没有绕过就会明白“根除”和“拦截”的区别。6.3 通关之后还能往哪里扩展level0和level1只是入门。往后你可以继续刷RCE-labs的后续关卡也可以自己改规则做变体练习。比如说把level1的过滤改成同时过滤空格和换行看你会不会用${IFS}解决把system换成shell_exec看看输出会不会有变化把GET参数改成POST参数再想想怎么从表单传递payload。RCE这条路走下去还有命令执行无回显、带外数据通道、命令注入结合其他漏洞等方向。学习打靶不一定非要成为极客但多动手、多改代码、多记录踩坑才是进步最快的方式。7. 一周练习节奏与自查清单7.1 前三天怎么安排第一天只做两件事装好PHP启动服务把level0的ls、cat、id跑通。不要急着看高级绕过。第二天再进level1先读源码理解过滤规则再把换行符绕过跑通。第三天的任务比较关键把源码里的过滤规则改一改比如加一个换行符到黑名单里然后尝试用别的方式绕过。这个“改规则再绕过”的过程比连续刷十个关卡都有用。7.2 通关前先过这份自查表检查项是否掌握能用php -S启动靶场并解释每个参数含义能通过system()执行ls、cat、id命令知道为什么URL里的空格要编码成%20能在level1用换行符绕过黑名单能解释为什么黑名单过滤不彻底能读懂源码里preg_match和system的作用如果你能全部打钩这两个level对你来说就算真正通关了。不是看到flag就算结束而是要看懂漏洞链条。7.3 本地练习的安全底线打靶场要守住一条底线只能在授权的本地或测试环境中操作绝对不能拿这些payload去打别人的线上系统。RCE是真实世界中杀伤力极高的漏洞用来学习时它能帮你建立攻击者视角但如果用错地方后果非常严重。练习时保持自律把环境限制在本机或明确授权的靶场里就好。我个人带新人时最爱让他们做的事是复制一份源码自己加一条过滤规则再去绕过。这比连续刷十个靶场都管用。启动环境这件事也一样多用几次curl多看一眼终端日志后面再打任何靶场都会顺手很多。