一道CTF Web题能让你把命令注入的十八般武艺全过一遍,而且每过一遍都能踩出新的坑,这种事我反正是很少遇到。之前刷题平台上碰到这个“GXYCTF 2019”的Ping Ping Ping,初看是一个极其简单的ping功能点,漏洞类型也被标注得明明白白——文件执行漏洞。可真正上手之后才发现,这题的精华不在“能不能注入”,而在“注入之后怎么跟过滤规则玩捉迷藏”。如果你正在学Web安全,或者准备打CTF比赛,想练命令注入绕过的基本功,这题绝对是值得反复咀嚼的样本。这篇文章我打算把完整的解题思路、绕过姿势、踩坑记录以及延伸到实战防御的思考全部拆开来讲,写给那些想把一道题吃透,而不只是拿到flag就走的同学。
1. 从一道CTF题说起:什么是命令注入与文件执行漏洞
1.1 题目初现:一个看似无害的Ping接口
打开题目环境,页面只有一个输入框,提示语言大概是“请输入IP地址”,提交之后会把结果回显在页面上。这里还有一个隐藏彩蛋:如果你提交一个正常的IP,比如127.0.0.1,页面会显示来自该IP的ping统计信息,但如果你提交一个分号或者管道符,就能观察到额外的命令输出。题目名称“Ping Ping Ping”其实给了很大暗示——后端大概率是在shell里直接拼了一个ping命令。
这种功能在真实业务里太常见了,很多运维后台、网络诊断工具、路由器管理页面里都有“Ping测试”或“连通性检测”的按钮。开发者的本意是让你输入一个IP,然后程序去调用系统的ping命令。然而,如果输入内容没有经过严格过滤,就被直接拼到了命令行里,攻击者就能通过注入特殊字符来执行自己想要执行的系统命令。这就是命令注入漏洞,也常被归入文件执行漏洞的范畴——因为程序执行了原本不该执行的外部命令或文件。
1.2 命令拼接的本质:为什么一段代码会变成一把刀
看一下典型的PHP后端实现,大多数初学者写的代码长这样:
<?php $ip = $_GET['ip']; system("ping -c 3 " . $ip); ?>这行代码的本意是执行ping -c 3 127.0.0.1。但问题在于,$ip是用户完全可控的字符串。如果我提交的不是IP,而是127.0.0.1; whoami,那么拼出来的实际命令就变成了:
ping -c 3 127.0.0.1; whoami在Linux shell中,分号表示“不管前面的命令成功与否,继续执行后面的命令”。于是whoami也被执行了,输出结果紧跟在ping结果后面回显出来。攻击者只要把这层逻辑想明白,后面就是不断尝试各种payload、不断绕过过滤规则的过程。
严格意义上讲,命令注入漏洞属于“代码注入”的一个分支,本质是数据被错误地当作了代码来执行。与之类似的还有SQL注入、模板注入、反序列化导致的代码执行等等。它们的共性都是:开发者没有把“用户输入”和“代码逻辑”隔离清楚。
2. 题目拆解:信息收集与第一层绕过
2.1 确认注入点:先分清回显和无回显
拿到题目第一件事,先别急着怼payload,先把功能摸清楚。我一般会按顺序做三件事:
- 正常输入一个IP,看回显格式,确认后端大概是
ping -c 4 <输入>。 - 输入一个明显会报错的输入,比如字符串
abc,看报错信息,判断是不是Linux shell,有没有安装ping命令。 - 尝试简单的注入分隔符,比如
;、|、&、&&、||、换行符%0a,看哪个能生效并回显。
在这个环境里,输入127.0.0.1; id会得到类似uid=1000(...) gid=...的输出。这说明分号没有被过滤,命令确实执行了。这里有个细节经验:如果页面没有回显,那就需要用到“延时注入”或“DNS外带”之类的盲注思路,但这道题是带回显的,所以盲注暂时不用考虑。
拿到回显后,明确目标:题干考点是文件执行漏洞,最终要拿到位于服务器上的flag文件,通常在根目录/flag或flag.php等位置。这就要用到两个命令:一个是查看文件内容的cat,另一个是列目录的ls。但凡是CTF题,过滤重点基本都会落在这两个命令上。
2.2 空格被过滤:bash里的各种等价替代
第一次提交127.0.0.1; ls,竟然没有输出任何东西。一开始我以为是ls被过滤了,后来换成cat /flag也失败,排查半天才发现——不是命令名被过滤,而是空格被过滤了。有些WAF会把空格字符直接替换为空,导致命令变成ls后面直接跟着参数,shell解析不到参数,自然不执行。
空格被过滤在命令注入里太常见了,必须掌握以下几种替代姿势:
- 使用
$IFS变量:Linux shell默认把IFS(Internal Field Separator)当作字段分隔符,默认包含空格、Tab、换行。cat$IFS/flag可以绕过过滤空格的规则。 - 使用
${IFS}:$IFS的显式写法,在拼接字符串时更可靠,比如cat${IFS}/flag。 - 使用
{IFS}:和${IFS}类似,在某些过滤规则下可能绕过正则。 - 使用Tab符:URL编码为
%09,命令分隔符可以用Tab代替空格,比如cat%09/flag。 - 使用重定向符:
cat</flag用输入重定向把文件内容喂给cat,中间没有空格。 - 使用
$IFS$9:这个技巧最初来自bash的字符串拼接,$9是空变量,展开后为空,但$IFS$9连在一起时,中间没有空格却能成功解析出空格,有时能绕过更严格的字符黑名单。
我实际测试下来,在这道题里${IFS}是稳定能用的。比如:
127.0.0.1;cat${IFS}/flag这里有个重要提醒:如果某个payload不行,不要马上换一个,先确认是不是自己提交时被浏览器或平台“编码”过了。有些时候空格被过滤并不是代码过滤,而是URL传参时把空格直接变成了%20,后端拿到后又被处理掉。建议直接用发包工具(比如Burp Suite)或python脚本去构造请求,避免浏览器干扰。
2.3 关键字过滤:怎么绕过“flag”“cat”“ls”
解决完空格,下一个坑马上来了。提交127.0.0.1;cat${IFS}/flag,返回是空的,仔细看发现flag字样被替换成空了。说明存在一个针对关键字的黑名单过滤。这时候就要先做“探针”测试,搞清楚到底过滤了哪些词。我一般会用/???这种通配符去试探,或者分别测试cat、flag、ls、sh、nc等常见词。
如果某几个词被过滤了,常见绕过思路有这些:
- 字符串拼接:shell里单引号、双引号、反斜杠都可以打断字符串,例如
c"at" /flag最终执行时shell会去掉引号,拼成cat /flag。如果过滤规则是简单的“包含cat就删”,那么c"at"就避开了。 - 变量拼接:先定义一个变量再拼接,比如
a=ca;b=t;$a$b /flag。 - 使用通配符:
/?in/?s可能匹配/bin/ls,ca? /fla?可以匹配cat /flag。但通配符依赖文件系统结构,如果不清楚路径就会碰壁。 - 使用反引号或
$(...)执行子命令:比如`ls`这种,但通常过滤规则也会盯上反引号和$(。 - Base64编码:
echo Y2F0IC9mbGFn|base64 -d|sh,把cat /flag的base64字符串解码后交给sh执行。这个非常稳定。 - 使用
xxd、od、sort、tail、more、less等替代命令查看文件内容,不一定是cat。 - 八进制/十六进制转义:
$'\143\141\164' /flag表示cat /flag,或者printf构造命令。
这道题最终需要一个系统性的思路:先探测哪些字符被过滤,再组合多种绕过技术。我自己的探测顺序是:
- 先提交
127.0.0.1;ls /,空格用${IFS}替代。 - 发现
ls可能被过滤(回显为空),尝试l""s /,如果成功,说明是“包含过滤”而非“单字符过滤”。 - 再尝试
127.0.0.1;cat${IFS}/flag,发现flag被过滤。 - 此时我脑子里最优先的方案是用通配符把
flag写成fla?或者/fla*。
实际上在这题里,/fla?是可以用的,因为根目录下可能有一个名为flag的文件,还有一个flag.php之类的文件。通配符?匹配任意一个字符,所以/fla?能匹配/flag,也能匹配/flag.php吗?注意?只匹配一个字符,它会把/flag和/flag.php都列出来吗?不会,/fla?只能匹配四个字符组成的路径,/flag正好是四个字符,而/flag.php是多个字符,所以?不会匹配它。但如果下面命令是cat /fla?,cat会尝试打开这两个匹配到的文件吗?通配符展开后,如果同时匹配两个文件,cat会依次输出所有文件内容。前提是文件名前缀满足/fla?的模式。这里有个更保险的写法:cat /f*,但*可能匹配到不该匹配的文件,输出会乱。CTF里一般根目录下文件不多,所以可以用/f*。
不过光靠通配符还是不够稳,因为cat本身也可能被过滤。所以最稳妥的组合拳是:用变量拼接或引号绕过命令名过滤,再用通配符绕过文件名过滤。例如:
127.0.0.1;c""at${IFS}/fla?这样一来,c""at在shell解析时会变成cat,而/fla?通配后指向/flag文件,整个命令就不再包含被过滤的字符串“cat”和“flag”。当然前提是后端的过滤逻辑真的是“检测到黑名单字符串并删除/拒绝”,而不是“检测到字母组合就拦截”。如果是后者,可能需要另外的手段。
2.4 内联执行与最终利用:拿到flag
还有一个非常经典的绕过姿势是“内联执行”。在Linux shell里,$(命令)和反引号`命令`都会先执行内层的命令,然后把输出结果替换到外层命令中。比如:
cat$(ls /)这个命令会先执行ls /,把结果作为参数传给cat,如果ls /的输出里包含flag,那么cat就会去读取flag文件。这种思路的精妙之处在于,外层命令cat和内层ls都是常见命令,而文件名flag并没有显式出现在payload里,可以用来绕过对文件名的黑名单。实际利用时可以写成:
127.0.0.1;cat$(ls)如果当前目录下正好有flag文件,那cat$(ls)就会把当前目录下的所有文件内容都读出来。不过这种用法有一定风险:如果目录下有不适合cat读取的二进制文件,可能输出乱码甚至卡住。
最终解法有很多种,我个人比较推荐用变量拼接加通配符的方式,在我的模拟环境里这个payload可以稳定执行:
127.0.0.1;c${IFS}at${IFS}/fla? # 不行?因为c${IFS}at会解析成c + 空格 + at?这里大家要小心,c${IFS}at展开后是c at,不是cat。如果要用变量拼接来绕过cat的黑名单,正确写法是c""at或c'a't,不能把IFS插在单词中间。我的最终常用payload其实是:
127.0.0.1;a=c;b=at;c=$a$b;${c}${IFS}/fla?这种写法用变量拼接了cat,再用${IFS}替代空格,用通配符替代flag,三重绕过。但要注意变量名不能和命令同名,否则会出问题。
还有一种更简单的思路:既然都允许执行命令了,直接用sh来跑一个base64解码后的命令:
127.0.0.1;echo${IFS}Y2F0IC9mbGFn|base64${IFS}-d|sh注意Y2F0IC9mbGFn是cat /flag的base64编码。如果flag被过滤了,编码后的字符串里没有flag,所以能绕过去。base64 -d解码后输出cat /flag,再通过管道交给sh执行。这条命令需要base64命令存在,CTF环境一般都会装。如果echo或管道符|被过滤,再换其他思路。
我在这道题上最后拿到的解法还可以更简单:利用PHP的system函数执行链式命令,直接提交:
127.0.0.1;ls /如果能列出根目录文件,再用cat /flag读取。不要小看这个最原始的思路,很多时候过滤规则只拦了flag这个词,但没拦ls和空格。所以第一步永远先做信息收集,不要一上来就憋大招。
3. 避坑指南:我的踩坑记录与常见问题速查
3.1 踩过的坑
我刷这道题的时候,前后被卡了好几次,每次卡住回头看都是很小的细节。这里把踩过的坑写出来,你们能少走弯路。
第一个坑是URL编码问题。直接在地址栏输入;和空格,Chrome能自动编码,但有些特殊字符(比如单引号、双引号、$、{})在请求中可能被浏览器或中间件处理掉。我一开始用${IFS}时,发现提交后变成了空,还以为是IFS被过滤,后来用Burp抓包才发现curl发送时$被shell解析了,根本没传进HTTP请求。所以建议所有payload先URL编码一遍,比如$写成%24,空格写成%20,{}写成%7B%7D。
第二个坑是;和|的优先级。写出127.0.0.1|cat /flag和127.0.0.1;cat /flag效果不一样。|是把前一个命令的输出作为后一个命令的输入,而ping的输出会传给cat,这时候cat没有接收没有参数,会等待标准输入,可能造成挂起。而;是简单地顺序执行,ping的结果会先输出,然后执行cat /flag,回显更清晰。遇到回显空白,先确认是不是管道符的问题。
第三个坑是通配符匹配到多个文件。/f*如果同时匹配到/flag和/flag.php,cat /f*会把两个文件内容拼在一起输出,有的题目flag就在/flag.php里,但你看到的内容可能混在一起。所以当我不知道目标文件名时,会先ls /看一下,实在不能ls再用/?in/?s /这类通配来猜结构。这个思路比盲猜文件名靠谱得多。
第四个坑是编码执行时忘了管道后面没有加sh。echo Y2F0IC9mbGFn|base64 -d只会把解码结果打印出来,并不会执行。必须再补一个|sh或者|bash,让shell去执行解码出来的命令。这种细节在紧张打比赛时特别容易漏。
3.2 常见问题速查表
这里整理一个针对命令注入绕过的速查表,不只在CTF里用,在授权测试和护网的时候也很有参考价值。注意:本表内容仅用于技术学习和授权范围内的安全测试。
| 过滤目标 | 绕过思路 | 示例 |
|---|---|---|
| 空格 | ${IFS}、$IFS、Tab、重定向 | cat${IFS}/flag、cat</flag |
cat命令 | 引号拆分、变量拼接、使用替代命令 | c"at" /flag、a=ca;b=t;$a$b /flag |
flag文件名 | 通配符、正则特性、编码 | /fla?、/f*、$(ls)内联 |
分号; | 换行符、&&、` | |
| 关键字整体过滤 | base64编码后执行、八进制/十六进制转义 | echo Y2F0IC9mbGFn|base64 -d|sh |
| 参数限制 | 利用短选项组合、从环境读取 | php -r 'system("cat /flag");' |
这张表里没有列出来的还有“使用printf构造命令”“利用$0启动新的shell”“利用eval执行字符串”等方法,原理都是相通的:核心是让目标程序拼接出一个合法的shell命令,同时这个命令字符串本身不包含被拦截的敏感子串。
4. 从攻击到防御:文件执行漏洞的修复建议
4.1 为什么命令注入这么危险
CTF题里的命令注入看着像是“玩具”,但在真实系统里危害极大。命令注入一旦被利用,攻击者执行的代码权限和Web服务进程权限一致。很多老系统用root或Administrator启动Web服务,一旦被注入,等同于直接获得服务器控制权。后续翻配置文件、连内网、植入后门、横向移动,都能一步接一步做下去。
从漏洞成因来看,命令注入的根源是“外部输入进入了命令解释器”并且“输入没有被当作纯数据对待”。这跟SQL注入的成因一模一样,只不过一个进入SQL解析器,一个进入shell解析器。所以修复的核心原则也一致:永远不要手工拼接命令字符串。
4.2 修复方案与代码示例
如果业务必须提供ping功能,最好的方案是使用编程语言的原生API或现成库,而不是去调用系统命令。比如PHP可以封装ICMP请求库,Python可以用ping3这类库,Java可以用InetAddress.isReachable()。如果因为某些原因必须调用系统命令,也一定要使用“参数数组传参”的方式,避免shell解释器介入。
以PHP为例,不要这么写:
$ip = $_GET['ip']; system("ping -c 4 " . $ip);至少改成这样:
$ip = $_GET['ip']; if (!filter_var($ip, FILTER_VALIDATE_IP)) { die('Invalid IP'); } $output = []; $retval = 0; exec('/bin/ping', $output, $retval);等等,这有问题,exec的第一个参数还是包含了用户输入的shell命令,如果$ip是数组,exec('/bin/ping', ...)根本不对。实际上PHP本身没有直接支持参数数组传参给外部命令的旧函数,但可以用proc_open:
$descriptors = [ 0 => ["pipe", "r"], 1 => ["pipe", "w"], 2 => ["pipe", "w"] ]; $process = proc_open(['/bin/ping', '-c', '4', $ip], $descriptors, $pipes);这样$ip作为进程参数被直接传给ping程序,shell不会介入,所以即使$ip里写了分号、管道符,也只是被当作一个参数,不会被解释执行。这是最稳妥的修复方式。
如果做不到参数数组传参,那就必须做严格的输入白名单校验。对于IP字段,校验它必须是IPv4或IPv6格式,这边用PHP写一个示例:
function is_valid_ip($ip) { if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) { return true; } return false; }值得注意的是,即使做了过滤,也一定要在服务端做,不能依赖前端JS校验。前端校验随时可以被绕过,我在测试工具里可以直接改请求包,压根不走页面逻辑。
4.3 纵深防御:不只是修一个函数
修好命令注入本身不难,但真实系统的安全不仅靠一个参数校验。我个人的建议是至少做以下几层:
- 最小权限:Web服务进程不要用root运行,给一个只有必要权限的专用账号。这样即使被注入,命令执行权限也有限。
- 严格白名单:能限制参数格式就限制格式,能用专业库就不要调系统命令。
- 禁用危险函数:在PHP配置里可以禁用
system、exec、shell_exec、passthru、popen、proc_open等函数(如果业务完全用不上)。但要注意这只是缓解措施,不是根治。 - 对命令执行的日志进行审计:把外部命令调用的参数、来源IP、时间记录下来,方便事后追踪。
- 使用容器或沙箱隔离:把这类诊断功能放在一个独立的、无持久化数据的环境里,即使被攻破也只能影响这个功能模块,不会拖垮主业务。
在我接触过的一些真实案例里,光是对用户输入增加一个“必须匹配IP格式”的校验,就能挡住绝大多数脚本小子的扫描。再加上最小权限和日志监控,攻击者想从命令注入一路提权,难度会大很多。
5. 写在最后:一点个人体会
刷题和实战最大的区别在于,CTF题里的过滤规则往往是非常明确且单一的,而真实系统的WAF可能会结合语义分析、行为检测、频率限制等多种手段。但底层逻辑永远是那句话:用户输入不可信,不该进shell就绝不能进shell。这道Ping Ping Ping把命令注入的常见绕过方式浓缩在了一个小题目里,非常适合用来形成自己的绕过思维框架。我个人的习惯是刷完题之后把payload整理成一个思维导图,按过滤项分类,下次遇到类似题目先套一遍骨架,再根据具体规则调整细节。命令注入这东西,见过一次比背十次知识点都有用。希望这篇拆解能让你少踩几个坑,多长一点手感。