☰
命令注入绕过实战:从Ping Ping Ping到读取flag
2026/10/21 17:06:51 网站建设 项目流程

坐在电脑前复现某次CTF里的Ping Ping Ping时,我脑子里就一句话:Linux命令注入的过滤绕过,差不多全在这道题里了。页面简陋到只有一行输入框和按钮,你填个IP它帮你ping,看似人畜无害,实际这个输入点直接接进了shell,空格被ban、flag这俩字被ban、斜杠被ban,层层过滤叠起来,目标是让你读取服务器上的flag文件。命令注入是Web安全里的老面孔了,原理不复杂,但想在高过滤下把数据拿出来,需要的是对shell解析机制有实打实的理解。

这篇文章就借这道题,把命令注入从探测、拼接、绕过滤到最终拿到数据的完整链路拆开讲一遍。适合刚接触Web安全的初学者,也适合准备CTF比赛但总在命令注入题型上卡壳的人。先说清楚:所有payload都建议在自己搭的本地靶场或合法授权的环境里验证,真实业务系统千万别拿这些命令去试。读懂攻击手法,最终目的是知道怎么防得住。

1. 赛题背景与攻击面还原

1.1 这道题到底考什么

Ping Ping Ping这种题在CTF里非常经典,名字带三个Ping,本体就是一个远程主机连通性测试功能。系统给了一个输入框,提交参数后,后端大概率做了这样一件事:拿用户输入的IP拼到一个ping命令后面,用system()之类的方法交给shell执行。

ping -c 3 <用户输入>

问题是,system()会把整条字符串交给shell去解析,而shell的语法又非常灵活。正常用户提交一个IP,比如127.0.0.1,拼出来就是ping -c 3 127.0.0.1,没毛病。但如果提交的是127.0.0.1;id,拼出来的命令就变成了ping -c 3 127.0.0.1;id。分号前的ping照常执行,分号后的id也被shell当作新命令执行了,这就是命令注入的起点。

不少新手第一次接触命令注入,觉得不就是加个分号吗,有什么难的。但赛事方不会让你这么舒服,它会给输入加过滤。这道题我记得比较清楚的是,提交带有空格的请求会被拦,字符串里出现flag也会被拦,有时候连斜杠都在黑名单里。这一下就把上面的简单payload废掉了大半,逼着你去找shell语法层面的替代方案。

所以这道题考的不是"你知道命令注入这个概念",而是"你是否理解shell该如何解析一条命令"。你把cat flag.php这句话的每个组成部分拆开看:cat是命令、空格是参数分隔符、flag.php是文件名。黑名单过滤掉空格和flag字样,本质上是把这三要素拆散了。绕过思路就是你得用别的语法形式,让shell在执行前把它重新拼成一条等价命令。

1.2 后端代码错误示范

为了说清楚问题,我直接写一份典型的错误代码,这类代码在真实项目里也常能看到,只不过真实环境不一定这么直白地拼接。

<?php $ip = $_GET['ip']; // 模拟赛事方的黑名单过滤 if (preg_match('/flag| |\\//', $ip)) { die('no no no'); } system("ping -c 3 " . $ip); ?>

本地靶场怎么搭、是哪家赛事出的题,这些细节无关紧要,把这段代码放到PHP环境里跑起来,就是一个能动手验证的靶子。

关键在最后一行:用户输入被直接拼进system()。即使中间加了正则过滤,它也只是对原始输入做了黑名单检查,并未改变"用户输入一旦通过检查就会被shell原样拼接执行"这个事实。攻击者的目标就变成了一个:构造一个字符串,满足两个条件,一是正则检查时看不到黑名单词,二是shell解析时又能得到想要的效果。

我用个生活化的类比来解释这个现象。想象你在饭店收银台买单,收银员会把你说的每一句话念给后厨听,你正常说"打包一份饭",但你要是夹带一句"顺便把后厨门打开",后厨也会照做。过滤规则就好比收银员说"不能提到门和打开",你换成"开一下那个door"或者"使用暗号打开",只要收银员没拦住,后厨还是能听懂并执行。这本质上是同一件事:输入经过一道"语法翻译"后,仍然能到达最终的命令解释器。

2. 过滤规则分析与绕过思路推导

2.1 黑名单为什么挡不住注入

先别急着看payload,把黑名单机制本身想清楚,后面很多东西就通了。

黑名单的思路是:我事先知道攻击者可能用的敏感词,把这些词一个个找出来删掉或拒绝。听起来很直接,但有两个致命问题。第一,你不可能穷举所有敏感词,shell的语法空间非常大,删除flag还能用fl\ag、fl''ag这种写法绕过;第二,过滤发生在命令执行之前,而shell的变量展开、特殊字符解析发生在命令执行的那一瞬间,这两者之间存在一个时间差。

以空格过滤为例。后端正则检查你提交的原始字符,如果里面有空格就拒绝。但你提交$IFS的时候,正则检查看到的是美元符号和三个字母,没有任何空格。等到命令真正交给shell执行,shell会把$IFS这个环境变量展开成它保存的分隔符,默认就是空格。于是黑名单检查的那一秒没看到空格,shell执行的那一秒空格出现了,这就是时间差带来的绕过。

这块逻辑我建议画个时间线去理解,虽然不能画流程图,但你在脑子里过一下就行了:

  1. 服务端拿到原始输入字符串
  2. 正则黑名单检查原始字符串
  3. 通过检查后,字符串被拼进命令
  4. shell拿到最终命令开始解析
  5. shell把环境变量、通配符、引号等语法结构展开
  6. 展开后的结果才是真正执行的命令

绕过目标就是让步骤2看到的字符串和步骤6实际执行的命令不一致。这里没有魔法,靠的是shell自身的语法特性。

2.2 绕行路线图

读flag文件,最终还是要执行类似cat flag.php的命令。从这个目标出发,可以把障碍和对应绕过方式列成一张表,实际做题时也基本是照着这个路线往下走。

障碍赛题常见过滤形式常用绕过手法原理简述
空格被过滤正则检查空白符,或替换空格为空$IFS、${IFS}、$IFS$1、tab替换利用环境变量展开或字符替换生成分隔符
flag关键字被过滤正则检查单词flag并拒绝fl\ag、fl''ag、变量拼接、通配符fla??让正则匹配不到完整字符串,shell解析后合成为flag
斜杠被过滤正则检查/字符$HOME拼接、cd目录后用相对路径、通配符避免直接在payload里出现/符号
多个命令需要执行未过滤分号;、|、&&、换行符%0a用shell的命令分隔符串起多条命令
输出中有关键字被二次过滤输出内容替换flag为空tac flag.php | base64编码后再输出,避免关键字出现在响应里

表格列出的只是主干,实际做题时还会遇到排列组合的情况,比如既过滤空格又过滤flag,那就把两种手法串在同一个payload里。这条路的本质是:"让黑名单检查器看到的是一串无害字符,让shell最终解析出来的是我们想要的恶意命令"。

3. 三种关键绕过手法的实操拆解

3.1 摆脱空格限制:$IFS与环境变量

先处理最常见的空格过滤。你要在shell里执行cat flag.php,cat和flag.php之间必须有分隔符。正常是空格,但空格被ban了,就得找shell里其他能充当单词分隔符的东西。

IFS(Internal Field Separator)是shell内置的环境变量,用来指定单词划分的边界符号,默认值包含空格、制表符和换行。所以$IFS在shell展开后就是一组空白字符,正好能拿来当分隔符用。直接写cat$IFSflag.php看起来是可以的,但在真实shell里十有八九会失败,原因是变量名边界问题。

$IFSflag.php会被shell解析为变量名IFSflag的值加上.php,而不是IFS变量的值后紧跟着flag.php,因为这个变量名还没结束。正确写法有几种:

cat${IFS}flag.php cat$IFS$1flag.php cat$IFS$9flag.php

${IFS}用花括号明确变量名边界,$IFS$1则是让第二个变量$1(位置参数,为空)紧跟在IFS后面,把变量名截断。我这里只说bash/sh的常见行为,其他shell大同小异,关键是你得意识到边界问题。这个坑我踩过不止一次,第一次写cat$IFSflag.php卡了半天,后来才反应过来变量名被连读了。

再补充一个技巧:如果连$符号也被过滤了,空格还是有替代方案的。最经典的是利用输入重定向,cat<flag.php,它用重定向符号<把文件内容喂给cat,命令和文件名之间不需要空格。也能用{cat,flag.php}这种花括号展开写法,逗号充当分隔符。不过花括号展开的细节受shell版本和语法影响比较大,实战时不如$IFS稳定。

3.2 突破flag关键字:转义、拼接与通配符

空格绕过去了,下一个坎是flag字样。后端如果正则匹配flag这个完整单词,那提交flag.php就会被直接掐掉。这时候思路是把flag拆开,让正则匹配不到完整词,但shell解析后又能组合回flag。

关于这一块,我前三板斧是反斜杠、引号拼接和变量拼接:

cat${IFS}fl\ag.php cat${IFS}fl''ag.php a=fl;b=ag.php;cat$IFS$a$b

第一行里的反斜杠在shell中会转义下一个字符,fl\ag会被解析成flag,正则看到的是带反斜杠的字符串,正好绕开完整关键词匹配。第二行的两个单引号中间是空字符串,shell解析时会把fl、空串、ag.php紧紧贴在一起,最终拼成flag.php。第三行是最“脏”的打法,先用变量存碎片,执行时再用变量展开拼接出完整文件名,同样能骗过正则。

如果题目没过滤通配符,还有更省事的玩法,直接用?匹配单个字符、用*匹配任意数量字符:

cat${IFS}fla??.??? cat${IFS}fl*

?匹配一个任意字符,fla??能匹配flag。用通配符有个风险:如果目录下有多个匹配文件,cat会把它们一起读出来,输出比较乱。所以最好先ls看一下目录里到底有什么文件,心里有数了再下手。

还有一种常见处理方式是把输出编码后再显示。假设读文件时服务端还会对响应里的flag关键字做处理,你在payload里加个base64:

cat${IFS}fla\g.php|base64

这样返回的是一串base64文本,再本地解码就能看到原始文件内容。这道题我没记错的话,flag文件本身可能是个PHP脚本,直接读会连同PHP代码一起出来,用编码输出反而更干净。

3.3 组合payload的构造思路

单个绕过手法学会之后,重点就在组合。我的构造习惯是先明确目标命令,再一步步反推需要的字符。

目标是执行:

cat flag.php

障碍是空格和flag字样。空格换成${IFS}:

cat${IFS}flag.php

flag关键字被ban,加反斜杠拆开:

cat${IFS}fla\g.php

这个payload基本就能过很多版本的黑名单了。如果服务端还过滤了斜杠,情况会再复杂一点,因为flag.php里没有斜杠,但路径可能涉及目录。通常的做法是借助已有环境变量拼接出斜杠,比如$HOME展开后是/home/xxx,再用${HOME:0:1}取第一个字符得到/。不过多数入门题的flag文件就在当前目录,相对路径读取反而绕开了斜杠问题。

我习惯把payload拆成"命令分隔符 + 目标命令 + 绕过片段"三段来组织。比如:

127.0.0.1;cat${IFS}fla\g.php

第一段127.0.0.1让前面的ping正常执行,分号是命令分隔符,之后就是被各种技巧武装过的读文件命令。提交到页面如果回显出了flag内容,说明整条链路已经打通。

4. 完整实战过程实录

4.1 搭个本地靶场

纸上谈兵没意思,我建议你本地搭个一模一样的靶场。我复现时用的代码就是前面那份PHP文件,保存为ping.php,然后用PHP内置服务器启动:

php -S 127.0.0.1:8080

这个环境完全隔离,怎么折腾都不影响真实网络。搭建的意义在于你可以放心地测试每个payload,逐步观察服务端回显,把"到底哪一段被过滤了"看得清清楚楚。

4.2 交互式探测注入点

我通常先用最简单的方式判断有没有注入点,比如直接提交ip=127.0.0.1;id。如果页面在正常ping结果后面多了一行用户组信息,说明分号没被过滤、命令拼接成立,注入点确认存在。

紧接着试空格。正常提交127.0.0.1;ls,如果返回no no no这样的提示,说明过滤已经生效。这时候我会缩小范围,先试ls是否在黑名单里,再试空格是否被ban。区分的方法很简单,用${IFS}替换空格再提交一次,比如127.0.0.1;ls${IFS}。如果这次能回显目录列表,说明黑名单拦的就是空格,而不是命令本身。

不同题目的过滤策略差别很大,有的拦空格但不拦tab,有的连分号都干掉了。所以这种逐个字符的探测比盲目提交完整payload要靠谱得多。我在本地复现时,会把每次请求和回显记录下来,看着列表一目了然。

4.3 从列目录到拿flag

拿到目录列表后,正常显示会出现flag.php或者index.php之类的文件。看到flag.php就不要直接提交cat flag.php了,空格和flag关键字两个坎摆在那。

我先走$IFS路线:

127.0.0.1;cat${IFS}flag.php

如果被拦,说明flag也进了黑名单,再加反斜杠拆开:

127.0.0.1;cat${IFS}fla\g.php

这一步通常就能看到flag内容。如果还不行,就再叠加变量拼接或者通配符方案。在我本地靶场里,最终能稳定出flag的payload有几个:

127.0.0.1;cat${IFS}fla\g.php 127.0.0.1;cat${IFS}fla''g.php 127.0.0.1;cat${IFS}fla??.???

提交后页面返回的内容里包含了类似flag{...}这样的一段占位文本,题目就算解完了。整个过程其实并不复杂,核心就是不断试探过滤边界,再用对应手法补齐执行的完整性。

5. 常见问题排查与避坑速查

5.1 变量展开与过滤时机的坑

最常出的问题就是$IFS写到一半被变量名边界坑了。cat$IFSflag.php提交上去为什么不好使,因为shell并不认为IFS变量结束了。排查这类问题,可以先用echo测试变量展开情况,比如提交127.0.0.1;echo${IFS}xxx,看输出结果是否正确显示空格,能帮你确认你的shell语法有没有写对。

另一个容易忽略的坑是shell版本差异。题目环境可能是sh、bash、dash中的任意一种,某些高级扩展语法(比如花括号展开)在精简版shell里不一定支持。如果你在本地bash上调试成功,但靶场上不生效,优先怀疑shell差异,换成最基础的反斜杠或引号拼接手法再试。

5.2 URL编码的坑

初学者总爱问"空格的URL编码不是%20吗,直接编码不就行了"。关键看服务端的解码顺序。如果服务端先做urldecode再进入过滤函数,那%20解码后就是空格,照样被拦。如果过滤在前、解码在后,那%20能侥幸绕过去,但这种依赖先后顺序的玩法很不稳定,不建议作为主方案。

tab字符的URL编码是%09,有些黑名单正则用\s匹配一切空白符,tab也在其中,所以同样可能被拦。真正稳定的做法还是从命令构造角度绕,依赖的是shell自己展开出来的分隔符,而不是提交原始空白字符,那才是正路。

5.3 常见问题速查表

把我在实操过程中遇到的问题整理成一张表,方便你排查。

现象可能原因排查方向
提交;id后没有回显命令分隔符被过滤或输出不回显试|、%0a、&&;确认是否有回显机制
cat${IFS}flag.php被拦flag关键字在该题黑名单中改用反斜杠、引号拼接、通配符
$IFS写法报错或输出不对变量名边界问题改用${IFS}或$IFS$1
payload在本地通、靶场不通shell版本或平台差异换成最基础的语法再试
读了flag.php但响应里没内容文件路径不对,或输出内容被关键字过滤先ls确认路径,或把输出base64编码
空格用%20没绕过服务端先解码再过滤放弃URL编码姿势,改用${IFS}
注入点无回显结果不直接打印改用sleep、写文件、外部请求等盲注思路

6. 从攻击视角看防御:黑名单为什么是死路

6.1 绕过手段暴露的过滤缺陷

复盘这道题的时候,我对防御的体会其实比攻击更深。黑名单过滤看起来是"堵住已知漏洞",但实际维护成本非常高。你今天拦了flag,明天攻击者用通配符绕;你补上通配符,人家换变量拼接;你连$都拦了,他还能用重定向和引号拼接。这就像在街边修修补补一个漏水的管道,永远不知道下一次从哪个缝隙喷水。

根本问题在于:黑名单试图"识别所有恶意输入",但攻击者的输入空间是几乎无限的。任何一点没考虑到,就会变成绕过点。正则规则越堆越复杂,复杂的正则本身又有新的解析漏洞,形成恶性循环。我在防御侧最想强调的一句话是:不要试图记住所有攻击方式,而是从源头掐断命令拼接这条路。

6.2 白名单校验与安全调用是正解

拿这道题的场景来说,后端本来只需要接收一个IP地址,那就只允许输入符合IP格式的字符串,任何其他字符直接拒绝。用正则白名单做第一道闸:

<?php $ip = $_GET['ip']; // 只允许数字和点组成的IPv4地址 if (preg_match('/^(\d{1,3}\.){3}\d{1,3}$/', $ip)) { system("ping -c 3 " . $ip); } else { die('invalid ip'); } ?>

这样分号、字母、路径分隔符全部被挡在门外,黑名单那套花式绕过根本轮不到上场。不过说实话,白名单校验虽然安全,但system()本身还是不够优雅。更推荐的做法是压根不走shell,直接用编程语言提供的系统API去完成ping操作,或者用底层socket发送ICMP报文,这样用户输入根本不进入shell解释器,命令注入的风险面被直接清空。

另外还要提一个容易被忽略的点:Web服务的运行权限。就算代码写得再安全,把服务跑在root账户下也是在自己给自己挖坑。一旦某个漏洞没挡住,最低权限运行能极大限制攻击者的后续操作。分隔权限、容器隔离、最小化依赖,这些在纵深防御里同样重要。

这道题我前前后后复现过好几遍,每一次都还会踩一点新坑。我个人最大的体会是:命令注入的绕过技巧看着多,但核心思想始终一致,就是想办法让你提交的字符串在过滤检查时"隐身",在shell执行时"现身"。把这些技巧在靶场里一个个跑通,比背一百条现成payload都有用,因为你真正理解了shell的解析逻辑,遇到再奇怪的过滤规则也能现场推演出来。

如果在复现时发现某个payload在你这台机器上不生效,先别急着怀疑题目的过滤规则,可以试试把它放到不同shell环境里观察表现,很多时候只是语法细节的事。后续有机会的话,我打算再写一篇关于命令注入盲注场景的实战笔记,那种情况下没有回显,拿数据的方式又会完全换一套打法。

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

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

立即咨询