☰
CTF命令注入绕过实战:Ping Ping Ping题目逐层拆解
2026/10/15 4:23:26 网站建设 项目流程

GXYCTF2019 这道 “Ping Ping Ping”,在某知名CTF练习平台上收录之后,几乎成了新手入门命令注入绕不过去的一道坎。题目名字看着挺萌,实际考的却是“文件执行漏洞”,更准确地说,是远程命令注入(RCE)。这类题目的套路非常典型:页面给一个输入框,让你填IP去ping,后端拿到参数直接拼进shell命令,然后因为各种过滤不严,攻击者就能把自己的命令一起执行了。

这道题适合谁?如果你是刚接触CTF Web方向、想搞懂命令注入绕过的新手,或者已经看过一些writeup但不知道“为什么这样写”的选手,这篇文章会很对路。我不会只丢一堆payload,而是把每一步探测、每一种绕过背后的逻辑拆开讲清楚,包括踩过的坑。读完你应该能明白:文件执行漏洞的本质是什么、过滤规则是怎么判断的、以及当空格和关键字都被ban的时候,还能用什么手段凑出一条命令来。

1. 题目定位与核心考点拆解:先搞懂“文件执行漏洞”到底是什么意思

1.1 为什么一个简单的ping功能会成为突破口

很多Web应用为了方便用户测试网络连通性,会提供一个在线ping的工具。输入一个IP,后端执行系统自带的ping命令,然后把回显结果打印到页面上。这个功能本身没什么问题,问题出在代码怎么写的。

如果后端代码是这样:

$ip = $_GET['ip']; echo shell_exec("ping -c 1 " . $ip);

那这个 $ip 参数就是一个任人宰割的入口。你输入127.0.0.1;ls,实际执行的命令就变成了:

ping -c 1 127.0.0.1;ls

分号本身不是IP地址的合法组成字符,但如果开发者没有做任何校验,它就会被原样拼进shell命令。shell在执行的时候会把它当作命令分隔符,先执行ping,再执行ls。这就像你把车钥匙交给代客泊车,正常他只该把车停到车位,但如果你从不交代边界,他完全可以开着你的车去任何地方。

CTF题目里经常把这类漏洞叫做“文件执行漏洞”,叫法上可能有点模糊,但它本质上就是远程命令执行。危害程度取决于能执行什么命令:轻则读文件、列目录,重则反弹shell、控制整个服务器。

1.2 拿到题目后的第一反应

从题目平台进入环境,页面一般会显示一个输入框,参数名通常是?ip=。第一件事不是瞎试,而是先建立一条探测基线:

  • 输入127.0.0.1,观察正常回显的样子;
  • 输入127.0.0.1;ls,看有没有触发过滤或报错;
  • 再换个连接符试试,比如|、||、&&,分别观察回显差异。

这一步的意义在于确认“注入点是否真的可执行”以及“过滤规则的边界在哪里”。有的题目直接就能;ls,有的则会把空格过滤掉,导致命令无法正常分隔。Ping Ping Ping 这道题就属于后者,而且过滤的还不止空格。

1.3 明确解题目标:读文件,而不是真的去ping

这种注入类题目的最终目标,99%是读取一个flag.php或flag文件,拿到flag字符串。所以核心思路可以拆成三条线:

  • 让注入的命令能够正常执行(解决连接符、空格问题);
  • 让要读取的文件名能正常传给命令(解决关键字过滤问题);
  • 让读取到的内容能正常回显到页面(解决输出方式问题)。

正常人第一反应是127.0.0.1;cat flag.php,但这道题用脚后跟想也知道不会让你这么顺利。空格和flag关键字大概率被过滤了,所以要走的是一条“绕过”链条。下面我从实际操作的角度,把每一步探测过程复盘出来。

2. 第一轮信息收集:试探、报错与回显判断

2.1 实测记录:输入正常参数与拼接参数

我当时打开环境,输入框默认让我填IP。先填个本地回环地址:

127.0.0.1

回显正常,能看到ping命令的四行统计输出。这说明后端确实执行了系统ping命令,而且结果会原样输出。

然后尝试注入:

127.0.0.1;ls

结果页面直接提示“不带这么玩的吧”之类的拦截信息,或者干脆没有任何输出。这一下就能确认:过滤存在,而且力度不小。接下来就要逐步缩小范围,搞清楚究竟过滤了哪些字符。

分别尝试:

  • 127.0.0.1|ls
  • 127.0.0.1||ls
  • 127.0.0.1&&ls
  • 127.0.0.1%0als

通过回显的差异判断哪些连接符可用。这里有个细节:如果用管道|,前面的ping输出不会显示,因为管道的语义是把前一条命令的stdout作为后一条命令的stdin,而后面的ls根本不会读取这些内容;如果用||,只有ping失败才会执行ls,而本地回环地址ping一定成功,所以也不会有输出;&&则相反,只有ping成功才执行ls。理解这些连接符的语义,比死记payload有用得多。

2.2 空格被过滤的判断条件

试到127.0.0.1;ls被拦,最需要确认的其实是空格是否被过滤。判断方法很简单:构造一个不含空格的命令注入看看,比如:

127.0.0.1;ls

这个被拦,不能说明是空格的问题,因为也可能禁了分号。换成不含空格的payload再试:

127.0.0.1;ls

如果这个也被拦,再换一个肯定不包含空格的版本:

127.0.0.1$IFS;ls

如果这个能出目录列表,那空格被过滤的可能性就非常高了。$IFS是shell里的一个特殊变量,叫内部字段分隔符,默认值是空格、制表符和换行。正则匹配的是字符串层面上的“空格”字符,$IFS在正则眼里只是四个普通字符,所以绕过了过滤;但shell在执行时,会把$IFS展开成真正的空格,于是命令就能正常分隔了。

这类“字符串层匹配”和“运行时解析”的错位,是命令注入绕过的最底层逻辑,也是后面所有绕过的根本原因。

2.3 另一个重要动作:读源码看过滤规则

当我发现;ls这种基础注入被拦后,我做的第二件事就是尝试读index.php源码。因为Ping Ping Ping这类题目一定会把过滤逻辑写在PHP源码里,与其在那儿猜过滤了什么东西,不如直接把源码读出来。

试一下:

127.0.0.1;cat$IFS$1index.php

实际执行的是ping -c 1 127.0.0.1;cat index.php。如果回显了PHP源码,里面通常会有类似这样的过滤代码:

if (preg_match("/\&|\/|\?|\*|\<|\>|\'|\"|=| |\x09|\x0a|flag/i", $ip)) { die("不带这样玩的吧"); }

当然不同平台的具体正则可能略有差异,但这道题常见版本里过滤的东西大致包括:&、/、?、*、<、>、'、"、=、空格、tab、换行,以及flag关键字(大小写不敏感)。把这些规则记牢,后面每一步绕过都是在跟这些字符做斗争。

3. 绕过三板斧:空格、关键字、命令构造方式

3.1 空格绕过的几种姿势,以及各自的适用场景

既然空格被过滤,那就得找替代品。常见方案有下面这几种。

绕过写法命令示例原理与适用场景
$IFScat$IFS/flag.php直接使用shell变量,注意变量名可能粘连
${IFS}cat${IFS}/flag.php花括号明确边界,更稳定
$IFS$1cat$IFS$1flag.php$1是空参数,用来隔断变量名
<cat<flag.php输入重定向代替空格,但要看<是否被过滤
%09cat%09/fla*URL编码的tab,有时正则没过滤tab
{IFS}{cat,flag.php}配合bash花括号展开,但这个并不是所有情况都有效

我实测下来最稳的是$IFS$1。为什么非要加$1?因为shell解析变量名时有“最长匹配”规则。你写$IFSflag.php,shell会尝试把IFSflag当成一个完整的变量名,而这个变量不存在,结果就变成空字符串,后面的.php成了文件名,命令自然就错了。写成$IFS$1flag.php时,$IFS先展开成空格,$1展开成空字符串,再把flag.php传进去,等于cat flag.php。这个细节很多人不知道,但踩过一次坑就能记住一辈子。

${IFS}也能达到同样效果,花括号本身就是边界分隔。我个人建议做题时优先试$IFS$1,不行再换$IFS、${IFS}、%09,不要在一棵树上吊死。

3.2 关键字被过滤:从反斜杠到变量拼接

空格解决之后,下一个拦路虎就是flag关键字过滤。如果直接cat flag.php被拦,说明正则是在字符串层追踪连续的flag字符,那么绕过思路就是让“正则看的时候不是flag,但shell解析的时候变成flag”。

第一种办法:反斜杠拆分。

127.0.0.1;cat$IFS$1fl\ag.php

URL里传过去的是fl\ag.php,正则匹配连续的flag,看到中间有个反斜杠,就匹配不上了;但shell在解析时,反斜杠的作用是转义下一个字符,也就是把\a转成普通字母a,最终传给文件系统的路径仍然是flag.php。这个payload在很多题目里都能直接成功。

第二种办法:变量拼接。

127.0.0.1;a=fl;b=ag;cat$IFS$a$b.php

先用a=fl、b=ag拆开存到变量里,再用$a$b拼成flag。正则分析字符串的时候看到的是变量名和赋值符号,不会去匹配“flag”这个连续串,而shell在执行时会把变量展开后再拼出完整参数。

第三种办法:通配符。

127.0.0.1;cat$IFS$1fla*

如果正则没有过滤*,这个也可以绕过。不过很多题目会把*和?一并过滤,Ping Ping Ping 的常见版本里*是直接ban掉的,所以通配符方案在这个题里大概率走不通。但遇到变体时,还是要优先试一试,成本极低。

第四种办法:base64编码执行。

127.0.0.1;echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|bash

Y2F0IGZsYWcucGhw是cat flag.php的base64编码,管道传给base64解码,再交给bash执行。这个思路很好,但依赖管道符号|和base64命令是否可用,还要注意正则有没有过滤|。作为备选方案,我通常会放在反斜杠和变量拼接后面再试。

3.3 命令连接符的选择:什么时候用分号和管道

这一步新手最容易迷惑。后端把输入拼成ping -c 1 $ip,那么;、|、||、&&执行出来的效果完全不一样:

  • ;是无条件分隔符,前面命令成功与否都会执行后面的命令;
  • |是管道符,把前一条的stdout交给后一条作为stdin;
  • ||是逻辑或,只有前一条失败才会执行后一条;
  • &&是逻辑与,前一条成功才执行后一条。

在CTF命令注入里,最常用的是;,因为它最“无脑”,不管ping成不成功都继续往后走。但要注意,如果题目过滤了分号,就得考虑换行符%0a或者管道符。Ping Ping Ping 的常见版本没有过滤分号,所以直接用分号最省事。

我见过有人用|然后抱怨“为什么看不到ls的结果”,原因就在于管道改变了输出的流向。回显只显示最后一条命令的stdout,而ls不读stdin,自然不会把它吞掉再吐出来。所以连接符不是越酷越好,回显什么、怎么回显,都要根据目标来选。

4. 完整解题链路:读源码、构造最终payload、拿flag

4.1 确认过滤规则:读index.php

如果前面第2章的尝试顺利,你已经能执行不含空格和关键字的命令了。接下来第一优先级的动作是读pages前端的PHP源码,搞清正则到底过滤了什么。

可用payload:

127.0.0.1;cat$IFS$1index.php

如果直接回显HTML和PHP源码,页面源码里能看到preg_match的过滤规则。如果直接输出的是二进制乱码,可能是php文件被执行了,这时考虑用cat配合编码方式,或者改用tac、nl、sed等命令,它们的作用是让文件内容以文本流的形式输出,不会触发PHP解析。

读index.php的好处非常多:你能知道你接下来要过的每一个字符是不是在过滤名单里,从而决定用哪种绕过组合。比如我知道了过滤名单里有/,我就不必再去尝试cat /flag这种路径,而是专注于当前目录下的flag.php。

4.2 尝试读取flag.php:关键payload的逐层进化

知道了过滤规则之后,读取flag的payload可以从简单到复杂逐层尝试。

第一层:直接读。

127.0.0.1;cat$IFS$1flag.php

被拦,因为里面有flag。

第二层:反斜杠拆分。

127.0.0.1;cat$IFS$1fl\ag.php

如果成功,页面会输出flag文件的内容,可能还会附带一段PHP源码。如果反斜杠被URL解析吞掉或者报错,可以试试URL编码后的%5c:

127.0.0.1;cat$IFS$1fl%5cag.php

第三层:变量拼接。

127.0.0.1;b=ag;cat$IFS$1fl$b.php

先把ag存进变量b,拼起来就是flag.php。注意分号的使用,多个命令之间都要用连接符串起来,如果担心;被过滤,可以用换行%0a代替。

第四层:换工具读取。

如果cat也被过滤了,那也别慌。cat只是众多文本查看工具之一,可以换tac、nl、more、less、head、tail、od等。比如:

127.0.0.1;tac$IFS$1fl\ag.php

甚至可以用php命令直接执行文件:

127.0.0.1;php$IFS$1fl\ag.php

不过这种看环境而定,优先用最朴素的方案。

4.3 最终payload的构造逻辑,以及为什么它能成功

如果上面第二层payload成功,你拿到的形式大概是这样的:

?ip=127.0.0.1;cat$IFS$1fl\ag.php

我们来拆解这条命令在服务器端到底发生了什么:

正则匹配发生在用户输入字符串上,它检查的是127.0.0.1;cat$IFS$1fl\ag.php这段完整字符串。正则里过滤了空格,所以$IFS$1不包含实际空格,安全通过;过滤了flag连续字符,而fl\ag.php里的flag被反斜杠拆成fl\ag,正则匹配不到,也安全通过。然后shell实际执行时:

ping -c 1 127.0.0.1;cat fl\ag.php

shell把\a转义为普通a,于是最终执行的是cat flag.php,命令和文件名都“复活”了。这就是绕过能成功的核心原因:正则判断的是静态字符串,shell解析的是经过转义和变量展开后的动态内容,两者之间存在信息不对称。

如果你读完源码后发现当前目录下有flag.php和index.php,但是flag.php被PHP解析成了空页面,那就说明问题可能出在输出环节。此时可以试试把源码读成文本而不是执行它,比如用反引号让命令结果先落在变量里再输出:

127.0.0.1;echo$IFS$1`tac$IFS$1fl\ag.php`

但这一步要看过滤规则是否允许反引号。更通用的方案是tac、nl这类命令配合当前回显机制,或者想办法让程序直接报错把源码泄露出来。总之,拿到flag的方式不只有一种,关键是理清“过滤在哪里、执行在哪里、输出在哪里”。

5. 高频翻车场景与快速定位方法

5.1 为什么我的payload一发就废:排查思路

做题过程中最让人抓狂的就是:明明看着writeup里的payload能用,我自己一发就是不行。我把常见翻车点整理了一下,基本可以照着排查。

现象可能原因解决方案
命令执行了但报not found命令本身被过滤或不存在换成tac/nl/php等替代工具
用$IFS但文件名解析错误shell把变量名解析成IFSxxx改成$IFS$1或${IFS}
反斜杠被过滤或吞掉服务器URL解析或正则额外过滤\尝试URL编码%5c,或换变量拼接
回显是空页面PHP文件被当成代码执行了改用tac或nl读取源码
分号被过滤后端过滤了;换%0a换行符或 `
有回显但不显示flagflag变量在PHP源码里想办法读取源码而不是执行它

排查的原则很简单:先确认过滤名单,再确认连接符是否可用,最后确认命令和文件名是否被展开成预期值。一般流程是,把一个payload切分成若干段,逐段替换测试,比如先测空格绕过是否成功,再测关键字绕过是否成功,最后测回显方式。

5.2 做题时记录payload和回显的重要性

这个建议听起来老生常谈,但实际非常有用。每尝试一个payload,就把“输入内容、回显内容、当时推断”三样记下来。因为比赛里的环境和本地环境可能不同,正则的具体过滤项也可能有变体,记录能帮你快速看到规律。比如你会发现flag被过滤,但fl\ag能过;然后你再观察fla\g是否也能过,从而推断正则到底是匹配连续字符还是匹配包含关系。

我自己的习惯是维护一份“命令注入绕过字典”,按分类记录:

  • 空格替代:$IFS、$IFS$1、${IFS}、%09、<
  • 关键字拆分:fl\ag、fl''ag、fla*、a=fl;b=ag
  • 命令执行方式:;、|、||、&&、%0a
  • 读取命令替换:cat、tac、nl、more、less、head、tail

这套字典遇到新题目可以直接套用,不用每次都重新试。

5.3 不同题目的过滤变体怎么应对

Ping Ping Ping 是命令注入里非常典型的一类,但它的变体很多。有些平台会在正则里加过滤tac、nl、fl\ag或者其他命令,导致上面某些payload失效。

应对变体时,我一般优先走这几种通用深层绕过:

  • 利用bash的ANSI-C quoting构造命令字符。比如cat可以写成$'\x63\x61\x74',其中\x63、\x61、\x74分别是c、a、t的十六进制编码,正则看到的是一堆$、\、数字和字母,不是完整命令。

  • 利用printf构造命令。$(printf '\143\141\164')会让shell执行printf输出的cat,再配合文件名变量,可以绕掉命令关键字过滤。

  • 利用已有的环境变量拼接命令。有些情况下系统里已经存在含关键字符的变量,用${PATH:0:1}这类子串截取方式拼出命令字符,不过这种方式比较重,一般作为最后手段。

这些扩展手段不需要每道题都用,但了解它们的原理,能让你在遇到变体时不至于完全没思路。

6. 从一道题延伸到整个命令注入体系:防御视角与后续学习方向

6.1 作为攻击者:构建自己的解题方法论

做命令注入题,最高效的不是背payload,而是建立一套判断树:

  • 参数点在哪,会不会进入shell命令;
  • 过滤规则是什么,哪些字符在上游被静态ban掉;
  • 能不能用变量展开、转义、编码等方式让静态规则和动态解析错位;
  • 命令执行结果能不能回显,不能回显时怎么用延时、DNS外带等方式判断。

把这个判断树运用熟练后,遇到任何注入类题目都能快速定位。Ping Ping Ping 的解题过程就是这套方法论的一个缩影:先探测,再读源码,剥洋葱一样一层层绕过过滤,最终落到payload上。

6.2 作为开发者:如何避免这种漏洞

反过来想,如果我们是开发者,怎么写才能避免命令注入?最稳的做法是永远不要直接拼接shell命令。Python的subprocess用参数列表、PHP的exec用参数数组方式、Java的ProcessBuilder,它们都能避免shell解析,也就没有命令注入的土壤。

如果业务场景必须走shell,至少要严格校验输入格式。一个IP参数就只允许数字、点和冒号(IPv6),用白名单校验而不是黑名单过滤。黑名单最大的问题在于永远有绕法,今天过滤了空格明天还有tab,过滤了flag还有反斜杠和变量拼接。白名单则把输入限制在一个绝对安全的字符集里,从根上堵死注入。

过滤类的函数、禁用危险函数这种思路只是降损,不是止血。真正治病要对输入做白名单校验,或者避免拼接。这个认知放在真实代码审计里同样适用。

6.3 后续还能往哪些方向练

如果这道题你已经能做出来,建议接着往上练几个方向:

  • 无回显的命令注入(盲注):通过延时sleep、DNS请求等方法判断命令是否执行;
  • 命令注入配合反弹shell:在出网环境下,用bash或nc反弹一个交互shell回来,这一步需要理解文件描述符和网络连接的原理;
  • 其他RCE入口:文件上传、模板注入、反序列化等漏洞最终也可能演变成命令执行,把这些链路串起来,才算真正掌握了Web安全的“命令执行”维度。

Ping Ping Ping 这道题本身并不难,但它把“命令注入绕过”最核心的思路浓缩进去了。如果把它吃透,你会发现后面很多Web题目里的命令执行考点,本质上都在重复同样的逻辑。

回到我自己的做题体验:一道题最值钱的往往不是最后那个flag,而是中间那些“为什么这样试、为什么不行”的思考过程。Ping Ping Ping 我只刷过一次,但$IFS$1和fl\ag这两个绕过点,在之后很多题目里都救过我。建议你也一样,每次做题把payload和回显记录下来,慢慢沉淀成自己的绕过手册,这比收藏一堆writeup有用得多。

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

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

立即咨询