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|ls127.0.0.1||ls127.0.0.1&&ls127.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 空格绕过的几种姿势,以及各自的适用场景
既然空格被过滤,那就得找替代品。常见方案有下面这几种。
| 绕过写法 | 命令示例 | 原理与适用场景 |
|---|---|---|
$IFS | cat$IFS/flag.php | 直接使用shell变量,注意变量名可能粘连 |
${IFS} | cat${IFS}/flag.php | 花括号明确边界,更稳定 |
$IFS$1 | cat$IFS$1flag.php | $1是空参数,用来隔断变量名 |
< | cat<flag.php | 输入重定向代替空格,但要看<是否被过滤 |
%09 | cat%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.phpURL里传过去的是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|bashY2F0IGZsYWcucGhw是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.phpshell把\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换行符或 ` |
| 有回显但不显示flag | flag变量在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有用得多。