CTF的Web方向里,RCE(远程代码执行/远程命令执行)是那种你早晚要正面硬刚的题型。我刷CTFHUB这套题的时候最大的感受是:它把一个很容易让人绕晕的考点拆成了连续的关卡,从代码执行到命令注入再到各种过滤绕过,难度是平滑上升的。这篇文章就把CTFHUB RCE系列从环境准备到每类题的解题思路完整过一遍,适合刚接触Web安全、准备系统刷CTF题、或者想搞懂命令注入到底怎么绕过滤的读者。建议人手一份基础payload手册,结合实际题目反复练,比死记硬背有效得多。
1. RCE题型全貌与CTFHUB关卡脉络
1.1 什么是RCE漏洞,为什么CTF绕不开它
RCE是Remote Code/Command Execution的缩写,中文常称为远程代码执行或远程命令执行。这两者从攻击者的视角看非常接近,都是让目标服务器执行我们传入的攻击载荷,区别在于最终被利用的函数是代码执行函数(如eval、assert)还是命令执行函数(如system、exec、shell_exec、passthru)。代码执行是把一段脚本语言代码当作程序来执行,命令执行是调用操作系统shell来执行系统命令。
这个考点之所以地位高,原因很直白:一旦利用成功,攻击者相当于拿到了一个在服务器上的“遥控器”,能读文件、看源码、拿数据库信息、甚至进一步获取服务器权限。它是所有Web漏洞中后果最严重的一类之一。CTF出题人也特别喜欢在这个点上做文章,因为可利用面广、绕过思路多、现实场景中又频繁出现,一题能考出选手对语言特性、shell解析、正则过滤、URL编码等多方面的功底。
对着代码理解会更清晰。如果开发者写了类似下面这样的代码,就存在RCE:
<?php eval($_GET['code']); ?>用户传入code参数,这段内容就被eval当成PHP代码执行了。你传system('id');,服务器就把当前用户信息打回给你。真实业务中这类问题常在模板渲染、日志处理、后台配置等功能里出现,问题根源是“用户的输入被拼接到了代码/命令的上下文里”。
用一个生活化类比帮助记忆:把服务器想象成一家餐厅,用户在菜单之外输入的内容如果被直接当成了“做菜指令”,那输入的字符串就不只是数据,而是厨师手里的操作命令。RCE漏洞就是请求中的参数被直接当成了代码或命令执行。
1.2 CTFHUB的RCE系列是怎么设计的
CTFHUB是一个面向CTF选手的在线训练平台,Web方向的技能树里RCE单独成类,关卡设计有明显的递进关系。我刷下来感觉整个系列大概分几个阶段:
- 第一类:eval执行,没有任何过滤,考察对代码执行函数的基本调用。
- 第二类:命令注入,需要让payload里出现系统命令,难点变成“怎么把命令拼进参数”。
- 第三类:过滤空格、过滤cat、过滤目录分隔符等,每一关只给定一个明确的过滤条件。
- 第四类:综合过滤练习,多个过滤条件叠加在一起,需要把前面所有技巧组合起来。
这种设计很像游戏里的新手村任务,它把真实环境中会遇到的“输入过滤”问题单独拎出来一关关训练,比直接丢一道综合题要友好很多。我建议按顺序刷,不要跳关。因为每一关都是在上一关基础上加一个过滤条件,比如前面学会用${IFS}绕过空格,后面过滤cat的时候要自然把${IFS}融入进去。跳着刷容易在不该卡的地方卡住,而且卡住后不清楚是哪个环节的问题。
刷题策略上,每道题至少要在心里回答三个问题:这题过滤了什么?用什么符号能在shell里等价替代?如果替代手段也被过滤,还有没有更底层的思路?带着这三个问题去解题,收获会大很多。
2. 开刷之前的环境与工具准备
2.1 一套顺手的环境组合
刷CTFHUB这类在线靶场,不需要太重的本地环境,但几样趁手的工具能大幅提升效率:
- 浏览器:用你日常用的浏览器就行。推荐装一个HackBar之类的辅助插件,方便切换GET/POST、快速修改请求参数,这样不用每次手拼URL。不过这不是必须的,浏览器地址栏加右键“复制为cURL”也能凑合。
- Burp Suite:主要用于重放请求、URL编码、观察响应细节。有些payload含有特殊字符,在地址栏直接输入会被浏览器自动转换,用Burp的Repeater模块就不会有这个问题。
- curl命令行:快速验证输出很好用,尤其适合在本地终端里先跑一遍payload确认语法,再发到靶场。
做题流程一般是这样:点开题目,平台会给一个动态环境地址,访问后进入靶机页面;题目会提示通过哪个参数提交输入;构造payload,观察响应;拿到flag后提交到平台验证。这里有个细节:CTF平台的环境通常有时效性,开启后尽量连续做完,别放着过夜,不然环境过期后flag会变。
如果看到题目没有任何输入框,不要蒙。通常是直接在URL上构造GET参数,或在请求体里构造POST参数,具体看题目描述。CTFHUB的题目描述一般比较直白,比如“请通过cmd参数实现命令执行”,照着做就行。
2.2 PHP危险函数与命令拼接基础速查
RCE题目基本绕不开PHP,先把两个方向的危险函数牢记,后面做题会快很多。
| 类型 | 函数 | 说明 |
|---|---|---|
| 代码执行 | eval() | 把字符串当作PHP代码执行 |
| 代码执行 | assert() | PHP 5/7早期版本能执行代码,新版默认不执行字符串 |
| 代码执行 | create_function() | 创建匿名函数,字符串拼接时可能出现代码注入 |
| 命令执行 | system() | 执行系统命令并直接输出结果 |
| 命令执行 | exec() | 执行系统命令,仅返回最后一行 |
| 命令执行 | shell_exec() | 执行系统命令,返回完整输出 |
| 命令执行 | passthru() | 执行系统命令并直接输出原始结果 |
命令注入要掌握的基础是命令分隔符。不同符号在shell里含义不一样:
;:连续执行多条命令,前面是否报错不影响后面。|:管道符,把前面命令的输出作为后面命令的输入。||:只有当前面命令失败时才执行后面命令。&&:当前面命令成功时才执行后面命令。%0a:URL编码的换行符,在某些场景下等效于回车,用来截断命令。
注入时优先用分号和换行符,因为它们不受前面命令退出状态的影响,输出也更稳定。用管道符时要小心,有时候只会显示最后一条命令的输出,容易造成“命令好像没执行”的错觉。
另外,shell里的空格过滤是个高频话题,这里先记住三个代替空格的办法:${IFS}变量、Tab键(URL编码%09)、输入重定向符<。IFS是shell的内部字段分隔符,默认包含空格、制表符、换行,所以cat${IFS}flag.php就等价于cat flag.php。这个技巧后面会反复用。
还有一点值得提前说:在bash和sh里,${IFS}的写法比$IFS兼容性更好,因为$IFS后面紧跟字符时可能被shell解析成变量拼接。后面刷过滤空格的关卡时,两种写法都可以试。
3. 逐个击破:CTFHUB RCE典型关卡实操解析
3.1 eval执行:代码执行的最直接形态
第一类题目通常不给过滤,源码逻辑大概长这样:
<?php if (isset($_REQUEST['cmd'])) { eval($_REQUEST['cmd']); } ?>代码用$_REQUEST同时接收GET和POST参数,参数名是cmd。这意味着我们传入的任何PHP代码,都会被eval当成PHP代码执行。
先发一个最基础的探测payload:
/?cmd=system('ls');这里提醒一下引号的选择:外层是URL查询字符串,内层payload里PHP字符串我用单引号包起来,不要用双引号。双引号在多层拼接时容易触发转义问题,而且PHP里双引号会解析变量,容易产生意外。
执行后,页面会返回当前目录的文件列表,通常能看到类似flag_xxxx.php的文件。接下来直接读取:
/?cmd=system('cat flag_xxxx.php');关键技巧:eval里可以直接调用system函数来执行系统命令,不需要额外构造复杂的PHP代码。如果system不行,还可以尝试passthru、shell_exec、highlight_file等函数。比如highlight_file('flag_xxxx.php');能直接把源码高亮显示,也是一个很好用的PHP原生函数。
这一类题目最常见的翻车点是忘了加分号。PHP语句必须用分号结束,缺了会直接报语法错误。另一个坑是只想着执行phpinfo();来证明代码执行,但题目需要的是flag,不是证明。先列目录、再读文件,才是正路。
如果当前目录没有直接看到flag文件,可以扩大搜索范围:
/?cmd=system('find / -name "flag*" 2>/dev/null');2>/dev/null把权限错误的提示过滤掉,输出更干净。这类搜索命令在真实环境里排查文件时也很常用。
3.2 命令注入之过滤空格:绕过空格的三板斧
这一关开始进入过滤场景,代码逻辑类似:
<?php $cmd = $_GET['cmd']; if (preg_match("/ /", $cmd)) { die("no no no"); } system($cmd); ?>直接把cat flag.php传进去会被拦,因为里面包含空格。要把空格替换成shell能识别的等价字符。这里有三板斧:
第一,${IFS}。这是最经典的方式:
/?cmd=cat${IFS}flag.php${IFS}解析为空格后,shell看到的就是cat flag.php。我在本地bash里验证过,这个写法稳定可用。
第二,$IFS$9。有些环境里$IFS后面跟数字9不会输出额外内容,也能实现同样的效果:
/?cmd=cat$IFS$9flag.php第三,Tab键。Tab在shell里也是合法的空白分隔符,URL编码是%09:
/?cmd=cat%09flag.php如果Tab也被过滤,还可以用输入重定向符<:
/?cmd=cat<flag.php这里<的作用是把文件内容作为标准输入传给cat,省略了空格。我第一次遇到这种写法时觉得挺巧妙,后来发现它不仅能绕过空格过滤,还能在某些过滤目录分隔符的场景里发挥作用。
实际刷题时,建议先ls再看文件名。列目录的命令也要注意空格:
/?cmd=ls${IFS}如果当前目录没东西,可以加绝对路径:
/?cmd=ls${IFS}/注意,这一关过滤的是空格,斜杠没有被过滤,所以绝对路径是可以用的。我认识的一些朋友卡在“为什么ls${IFS}没输出”上,往往就是没注意当前目录是空的,换成根目录或直接pwd先看路径就能解决。
3.3 过滤敏感命令:读取文件的替代思路
这类题目的过滤条件通常是cat、/等敏感内容,代码逻辑类似:
<?php $cmd = $_GET['cmd']; if (preg_match("/cat|\//", $cmd)) { die("no no no"); } system($cmd); ?>过滤了cat,不代表不能读文件。Linux下能读文件的命令非常多,先记住一个结论:cat被过滤后,优先用tac和nl。
tac:反向输出文件内容,cat反过来写。flag如果在文件末尾,tac正好能一眼看到。nl:带行号输出,无论flag在第几行都能看到。more、less:分页查看,虽然交互体验一般,但用来读flag没问题。head -n 100、tail -n 100:从头或末尾读指定行数。od、xxd:二进制/十六进制查看,适合文件内容被转义或含特殊字符的场景。
所以一个有效payload是:
/?cmd=tac${IFS}flag_1.php如果连flag这个关键词都被过滤了,普通路径写法也会暴露关键词,这时候可以用通配符。?匹配单个字符,*匹配任意多个字符:
/?cmd=tac${IFS}fla?.php /?cmd=tac${IFS}*第一种写法用?占住flag里的g,正则匹配不到完整单词flag,shell在展开通配符时却能匹配到真实文件。第二种写法更暴力,直接匹配当前目录所有文件,能读就全读出来。这类通配符思路是绕过关键字过滤的万能钥匙,后面面对综合过滤也一样有效。
还有一种思路是拼接命令名,比如:
/?cmd=c''at${IFS}flag.php /?cmd=c"at"${IFS}flag.php /?cmd=c\at${IFS}flag.php正则匹配的是连续字符串cat,但两个引号之间是空字符串,shell解析时会自动拼接成cat。注意单引号里包空字符串的写法有几个环境不支持反斜杠转义中途插入,实测下来双引号拼接的兼容性最好。
如果过滤了/,相对路径可能就不可用了,因为文件名本身就是路径的一部分。这时候可以先改变当前目录,再读取:
/?cmd=cd${IFS}..;ls /?cmd=cd${IFS}..;cat${IFS}flag.phpcd ..让当前目录切换到上级目录,后面的命令里就不需要斜杠了。这个方法同样能应对部分过滤目录分隔符的关卡。
3.4 综合过滤:把前面的技巧串起来
综合过滤关卡会同时限制空格、cat、flag、目录分隔符等,看起来好像快没路可走了。实际上,前面所有招数配合起来就能解。目标还是读取当前目录下类似flag_xxxx.php的文件。
完整步骤演示一遍:
第一步,列出当前目录下的文件:
/?cmd=ls${IFS}如果ls被过滤,可以用echo *,shell会直接把当前目录下的文件名打印出来:
/?cmd=echo${IFS}*第二步,读取文件。cat被过滤就用nl或tac,flag关键字被过滤就用通配符:
/?cmd=nl${IFS}fla?.php如果这一步被拦截,说明nl也被加入黑名单了,换成tac、more、head逐个试。不要怕多试几次,CTF刷题就是一个“试探-反馈-调整”的循环。
第三,如果这些常规读取命令全被过滤,还有一发通用思路:base64编码加管道执行。原理是让服务器执行一个命令,把一段base64字符串解码后交给shell执行,从而绕开对命令原文的关键字过滤。payload形式:
/?cmd=echo${IFS}Y2F0IGZsYWdfMS5waHA=|base64${IFS}-d|bash其中Y2F0IGZsYWdfMS5waHA=是cat flag_1.php的base64编码。服务器执行时会先输出那段编码字符串,再用base64解码,最后交给bash执行,实际效果就是执行cat flag_1.php。前提是过滤规则没有针对base64命令本身,也没有禁止管道符。这个思路在真实渗透和CTF中都很常用,值得背下来。
如果连base64关键词都被过滤,可以先用base64的反向命令rev,或者用php -r 'system(...);'直接构造PHP代码执行,只要服务器上有这些解释器,总能找到执行路径。
综合过滤关卡的要点是别慌,逐个确认过滤规则,把可用字符列出来,再拼凑出有效payload。很多情况下一次请求就能解开,但心态不好时容易在某个细节上反复卡住。
4. 刷题过程中最容易踩的坑与排查技巧
4.1 高频翻车点
我自己刷这几关时踩过不少坑,总结下来主要集中在几个地方。
第一个坑是命令分隔符选错。有人习惯用&拼接命令,但&在shell里是后台执行,命令执行了但输出不显示,看起来就像没生效。还有人用|,但|是管道,cat flag.php | bash会把前面命令的输出作为后面命令的输入,本质完全变了。注入多用分号;优先于其他分隔符,因为分号就是把两条命令排着执行,互不干扰。
第二个坑是URL编码问题。直接往地址栏里塞#、%、&、?这些字符,浏览器和服务器会做出不同解析。#在URL里代表锚点,后面的内容根本不会发送到服务器,要写成%23。空格如果不用${IFS}代替,也需要写成%20。遇到奇怪的执行结果时,先检查是不是没有编码。
第三个坑是引号的选择。PHP代码和shell命令里的引号语义不同,单引号内的$IFS不会展开成变量,双引号内的变量会被解析。在URL查询串中,双引号有时还会被浏览器自动处理,所以我的习惯是payload里优先用单引号,需要变量展开时才用双引号。如果你在Burp里测,还可以直接对全payload做一次URL编码,再发请求。
第四个坑是不知道命令输出被截断。exec()函数只返回最后一行输出,system()才完整输出。如果题目环境用exec接收命令,你执行ls;cat flag.php可能只看到最后一行,误以为前一条命令没执行。这种时候把payload拆开单独试,或者把多条命令写成一行用echo做标记,就能定位问题。
第五个坑是小写字母被过滤的特殊场景。有些进阶题目会过滤掉所有小写字母,这时候{$IFS}和大部分命令都没法用了。绕过思路是用大写字母替换某些不敏感字符,或者利用shell通配符匹配/bin下的命令。CTFHUB的RCE系列常规关卡不会这么极端,但万一遇到类似情况,知道有这条路就行。
4.2 一份过滤与绕过速查表
刷题时反复用到的技巧,整理成一张速查表,放到手边随时翻:
| 过滤内容 | 绕过思路 | 示例 |
|---|---|---|
| 空格 | ${IFS}变量、$IFS$9、Tab(%09)、输入重定向符< | cat${IFS}flag.php |
| cat | tac、nl、more、less、head、tail、od、xxd | nl flag.php |
| flag关键字 | 通配符?/*、字符串拼接、大小写绕过 | tac fla?.php |
目录分隔符/ | cd ..切换目录后用相对路径、切换工作目录 | cd ..;cat flag.php |
| 分号或管道被过滤 | 换行符%0a、命令结束符%00、注释符# | ls%0acat flag.php |
| 多个条件叠加 | 组合上面的手段,或使用base64管道 | echo Y2F0IGZsYWdfMS5waHA=|base64 -d|bash |
这张表看起来简单,但刷题时90%的卡壳都能在表里找到方向。你不需要把所有命令全背下来,真正关键的是遇到过滤时知道“还有别的办法”,然后去尝试。
4.3 出题思路与防御视角
把CTF题目换成真实业务视角看看,这类漏洞之所以存在,根源往往是开发者在不该调用危险函数的地方调用了危险函数。
防御层面有很多事可以做:
- 业务代码里尽量不要使用
eval、assert、create_function这类代码执行函数。PHP 7以后assert默认不再执行字符串代码,但历史代码里依然存在风险。 - 如果业务必须执行系统命令,对参数做严格白名单校验,而不是黑名单过滤。黑名单永远能被绕过,因为Linux命令和shell符号的组合实在太多了。
- 使用
escapeshellarg()或escapeshellcmd()函数转义用户输入,它们能在参数层面阻止大部分注入。 - 服务器配置里用
disable_functions禁用system、exec、shell_exec、passthru等函数,降低被利用后的影响面。 - 对历史遗留的
preg_replace的/e修饰符、回调函数配合点保持警惕,它们也是代码执行的经典入口。
从学习角度说,刷完CTFHUB的RCE系列,“正则过滤、shell执行、URL编码、变量展开”这些基础不会再陌生。这个系列练的是手法组合能力:一种方式被堵死,能不能快速想到替代方案,并且写出在目标环境里能跑通的payload。这个能力做后续的Pwn、反序列化、命令注入组合题都会受益。
最后分享一个我刷这系列题的小习惯:每个payload我都会先在本地bash里跑一遍再发到靶场。这样能很快区分问题是payload本身写错,还是靶场的过滤规则导致。尤其是${IFS}这类技巧,本地验证一次之后,后面遇到空格过滤就再也不会犹豫了。祝刷题顺利。