命令注入绕过实战:GXYCTF2019 Ping Ping Ping 靶场解析
2026/9/24 19:24:59 网站建设 项目流程

BUUCTF上这道[GXYCTF2019]Ping Ping Ping1,几乎可以算命令注入入坑必刷的经典题。题目本身不复杂,但过滤规则设计得很有层次,空格、符号、关键字三重拦截,逼着你去理解Linux Shell到底是怎么解析命令的。这篇write up我把完整思路、绕过原理、踩坑记录全部写清楚,新手照着做能复现,老手也能当个速查。

1. 先把题目打开:一个测ping的小网页里藏着什么

1.1 题目外观与交互逻辑

打开BUUCTF给的题目地址,页面非常简单,一个输入框,一个执行按钮,旁边写着类似“Ping”的提示。你往里面填一个IP地址,比如127.0.0.1,点执行,页面就会返回一堆ping命令的输出,类似:

PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data. 64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.038 ms ...

从功能上看,这就是一个在线ping检测的小工具。但这种工具如果后端直接拿用户输入拼进shell命令,就很容易出问题。实测下来,这个输入框的值是通过GET参数ip传过去的,也就是说你可以直接在URL里构造请求:

http://目标地址/?ip=127.0.0.1

看到这个参数名,懂的人基本就有数了——后端很可能是这样写的:

shell_exec("ping -c 4 ".$_GET['ip']);

用户输入原封不动地拼到命令后面,这就是典型的命令注入入口。你输入的ip参数不只是“IP地址”,而是整个shell命令的一部分。能不能利用,取决于后端有没有过滤、过滤了什么。

1.2 ping命令本身是怎么回事

先补个基础。ping命令是基于ICMP协议的,原理很简单:向目标主机发送ICMP回显请求报文,目标主机收到后回复一个ICMP回显答复报文。本机通过有没有收到答复、答复的延迟和丢包率,来判断目标主机是否在线、网络质量如何。

日常排障时大家也经常用ping,比如我在排查Linux服务器连不上外网的问题时,第一件事就是ping一下www.baidu.com,看是DNS解析失败(报Name or service not known)还是网络不通(报Destination Host Unreachable)。这些报错本身就是信息。

在这道题里,我们并不关心ping本身能不能通,关心的是:命令拼接之后,能不能在ping后面再塞别的命令。Linux shell里多条命令可以用分号、管道、逻辑与或连接,所以只要后端没过滤分号,输入127.0.0.1;ls,实际执行的命令就是:

ping -c 4 127.0.0.1;ls

先ping一下,然后列出当前目录。这就是命令注入的第一步试探。

2. 从报错与回显里拿到第一手情报

2.1 第一发测试:用分号扛起命令注入

直接输入:

127.0.0.1;ls

页面回显里,在ping的输出后面,多了一串内容,是当前目录下的文件列表:

index.php flag.php

看到flag.php,说明注入可行,分号没有被过滤,而且命令是真实执行的。这就确定了第一个关键信息:可以利用分号做命令拼接。

不过先别急着直接cat flag.php。很多新手到这里就往前冲,结果被过滤规则教育了。我的习惯是先试试水,摸清楚过滤规则再动手,否则来回试错浪费时间。

输入127.0.0.1;cat flag.php,页面直接返回一段提示,大意是检测到了空格,被拦了。这段英文提示本身就是一个重要线索:题目不是简单地把输入拼进命令就完事,而是做了黑名单过滤,而且不同违禁内容会返回不同提示。

到这里,我推测过滤规则至少包含空格和flag关键字。但还有一个关键问题:是否存在其他符号过滤?比如斜杠、反引号、管道符、通配符?这些光靠猜是猜不完的,必须拿到源码才能精确知道。

2.2 试图直接读flag:空格被教育的现场

先确认一下空格确实被过滤了。构造:

127.0.0.1;cat flag.php

返回的提示说明空格被拦截。这是第一道坎。

那空格怎么绕过?常见的思路是用Linux里的环境变量$IFS。IFS是shell的内部字段分隔符,默认包含空格、制表符、换行。在命令中用$IFS可以替代空格。但直接写cat$IFSflag.php是不行的,因为shell解析变量名时会尽可能多地吃掉后面的字母数字下划线,$IFSflag.php会被当成变量名IFSflag.php,展开为空,命令就变成了cat.php,直接报错。所以要用$IFS$9或者$IFS$1这种写法,$9在交互式shell里通常展开为空字符串,它的作用只是把变量名边界切清楚,让$IFS独立展开成一个空格。

所以payload变成:

127.0.0.1;cat$IFS$9flag.php

但这样又被拦了。看提示,是flag关键字被检测到了。这说明第三层过滤是匹配flag字符串。

空格的坎过了,flag关键字的坎又出现了。这时候千万别乱试,比如把flag改成FLAG或者Flag,Linux文件名区分大小写,改了就读不到。要想想正则匹配的本质是什么。

2.3 读index.php源码,还原三重过滤

既然直接猜过滤规则效率太低,不如先读源码。怎么读?用刚才已经验证有效的方法,读index.php

127.0.0.1;cat$IFS$9index.php

这个payload里没有空格、没有flag字样,能通过所有过滤。页面回显里会把index.php的源码打出来。源码核心逻辑大概是这样的:

<?php if(isset($_GET['ip'])){ $ip = $_GET['ip']; if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{20}]|\>|\'|\"|\\|\(|\)|\`|\|/", $ip, $match)){ die("fxck your symbol!"); } else if(preg_match("/ /", $ip)){ die("fxck your space!"); } else if(preg_match("/flag/", $ip)){ die("fxck your flag!"); } else { $a = shell_exec("ping -c 4 ".$ip); echo "<div class=\"result\">"; print_r($a); echo "</div>"; } } ?>

拿到源码再看,过滤规则就一目了然了。逐条拆:

第一层正则过滤了很多特殊符号:&/?*<、空白字符(ASCII 0x00到0x20,包含空格制表符换行)、>、单引号、双引号、反斜杠、左右括号、反引号、管道符|

第二层单独检测空格。

第三层单独检测flag字符串。

注意第一层的[\x{00}-\x{20}]其实已经包含空格了,所以严格来说带空格的payload会先触发第一层的拦截。但不同版本的题目源码顺序可能不一样,有的版本先查空格再查符号,所以网上很多write up记录的是“空格被拦”“flag被拦”这种分开的提示。不管提示长什么样,核心结论是:空格、斜杠、管道符、反引号、通配符、引号、括号全被过滤了,flag字符串也被过滤了。

3. 绕过方案的原理拆解

3.1 空格替代的两种正确姿势与原理

先解决空格。第一层和第三层过滤都绕不开一个核心问题:shell解析命令时,命令和参数之间的空格是必需的。那能不能不用空格?

先说$IFS方案。IFS是内部字段分隔符,默认值是空格、制表符、换行。在bash里执行命令时,未加引号的变量展开结果会被做分词处理,IFS就是分词的依据。也就是说,$IFS在命令行里展开后会被当作一个空格来分割单词。

前面说过,直接用$IFS拼文件名会有变量名边界问题,要用$IFS$9隔开。$9是位置参数,在脚本里表示第9个参数,但在交互式shell里通常为空,所以$IFS$9展开结果就是“空格+空”,效果等同于一个空格。同理可以用$IFS$1$IFS$2,甚至$IFS$x,只要$x变量是空的不存在就行。

另一种姿势是花括号展开。Linux bash里{cat,flag.php}会被展开成cat flag.php,花括号里用逗号分隔多个词,展开后词与词之间自动带空格。写法:

127.0.0.1;{cat,flag.php}

但这里有个问题:flag.php里有flag字符串,会被第三层过滤拦掉。能不能用变量拼接花括号?比如{cat,fla$a.php},这里有个坑,bash的展开顺序是花括号展开在前、变量展开在后,所以{cat,fla$a.php}先变成catfla$a.php两个词,然后变量$a再展开。如果你设a=g,最后得到的就是cat flag.php,理论上可行。但我实测中更喜欢用$IFS$9,因为花括号在不同shell下的行为有差异,$IFS$9更通用、更稳。

3.2 flag关键字:用变量拼接骗过正则

空格解决了,再来对付flag过滤。第三层正则preg_match("/flag/", $ip)匹配的是HTTP请求里ip参数的原字符串,也就是PHP拿到的那一串。它不会去执行shell命令,也不知道变量展开后是什么。

但shell会。bash在执行命令前会做变量展开,把$变量名替换成对应值。利用这个信息差,可以轻松绕过关键字过滤。

思路是把flag拆成两段,一段直接写在请求里,另一段塞进变量,然后拼起来。比如:

a=g;cat$IFS$9fla$a.php

请求串里出现的是fla$a.php,没有连续的flag字样,正则匹配不到。但shell执行时,先执行a=g给变量赋值,然后cat$IFS$9fla$a.php展开,$a的值是g,拼到fla后面就变成了flag,最终执行的命令是:

cat flag.php

这就是这套绕过的核心原理:正则检查的是请求文本,shell执行的是展开后的命令,这两个阶段之间存在一个“时间差”。

payload组织成完整URL:

http://目标地址/?ip=127.0.0.1;a=g;cat$IFS$9fla$a.php

执行顺序是:

ping -c 4 127.0.0.1;a=g;cat$IFS$9fla$a.php

分号分隔三条命令,依次是ping回环地址、给变量a赋值、读flag.php文件。最后一条命令展开后就是cat flag.php,flag直接打印在页面里。

类似地,你也可以把flag拆成其他组合,比如:

b=lag;cat$IFS$9f$b.php

或者:

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

原理一模一样,选哪种纯看个人习惯。

3.3 为什么其他常见姿势全军覆没

这道题设计得挺有意思,它几乎把命令注入里的常规绕过方式全堵死了,只留了一条路。我把常见姿势和死因列一下:

  • 反引号执行:\`ls\`,反引号被第一层过滤,用不了。
  • $()命令替换:$(ls),左右括号被第一层过滤,用不了。
  • 管道符:|被第一层过滤,用不了,所以base64编码后管道给bash解码这条路直接断了。
  • 通配符:?*都被第一层过滤,用不了,所以cat?fla?.php这种写法也不行。
  • 引号包裹:单引号双引号都被过滤,cat' 'flag.php这种拼接也不行。
  • 反斜杠转义:反斜杠被过滤,ca\t都做不了。
  • 绝对路径:/被过滤,cat /flag这种操作根本走不通。
  • 直接空格:第二层过滤拦得死死的。
  • 直接写flag:第三层过滤拦得死死的。

所以你会发现,剩下还能用的符号里,分号;、美元符号$、变量赋值、$IFS全都没被过滤。题目设计者就是故意留下这条狭窄的通道,让你理解shell变量展开机制。这也正是这道题的教学价值所在。

4. 完整打一遍:最终payload与踩坑记录

4.1 最终payload与回显分析

最终完整的请求是:

http://目标地址/?ip=127.0.0.1;a=g;cat$IFS$9fla$a.php

页面回显大致分两块:前面是ping回环地址的正常输出,后面就是flag.php的源码内容。flag一般就在源码里,形式可能是:

<?php $flag = "flag{xxxxxxxxxxxxxxxxxxxx}"; ?>

或者直接以HTML注释形式藏在flag.php里。因为flag.php是PHP文件,直接浏览器访问会被服务器解释执行,反而看不到里面的$flag变量赋值代码,这就是为什么必须通过命令注入读源码而不是直接访问它。

另外补充一个细节,有些版本里flag.php源码中flag是经过编码的,比如base64或者十六进制,拿到后还需要解一下码。如果遇到这种情况,别慌,先看源码,按编码方式逆回去就行。

4.2 我在复现时遇到的几个坑

第一个坑是浏览器或Hackbar自动编码。直接在地址栏输入$IFS$9时,有些浏览器或代理工具会把$号或其他符号转成URL编码,导致后端收到的数据变了。用Burp Suite或者Hackbar这类工具发请求时,要保证原始payload不被二次编码。

第二个坑是$IFS$9在不同shell环境下的兼容性。题目环境用的通常是bash或sh,$IFS$9在大多数情况下都能工作,但如果你在本机用zsh测试,行为可能有点差别,比如$9在某些位置参数模式下会被解释成脚本参数。建议以题目环境为准,本地测试只用bash。

第三个坑是变量拼接时一定要算清楚展开结果。比如我一开始图省事写a=ag;cat$IFS$9fl$a.php,结果展开后是cat flag.php还是cat flg.php?想清楚:fl$a.php展开为fl+ag+.php,得到flag.php,没问题。但如果你写a=g;cat$IFS$9fl$a.php,展开后是flg.php,就差一个字母。这类问题千万别靠脑子硬算,先在本地shell里跑一下echo看展开结果:

echo fl$a.php

看到输出flag.php再往题面上打。

第四个坑是被过滤的符号可能不止你看到的那些。有的平台在题目外层又套了一层WAF,导致本地能打的payload到远程被拦。遇到这种情况,多看回显提示,判断是题目自身的过滤还是平台附加的过滤,再针对性调整。

4.3 命令注入题通用排查清单

刷多了命令注入题之后,我总结了一套通用排查步骤,遇到类似的题直接按这个顺序走,能省很多时间:

步骤操作目的
1先测分隔符:;、`||&&`
2测最简单的lswhoami确认命令能执行
3测空格是否被过滤:直接带空格请求判断第一道过滤
4测常用符号:/、反引号、$()?*、引号摸清黑名单范围
5如果目标站点有PHP源码,优先读源码获取精确过滤规则
6根据过滤规则选择绕过姿势空格用$IFS$9,关键字用变量拼接等
7最终验证并读取flag注意编码和注释

这套清单不是我凭空想出来的,是刷了十几道命令注入题以后总结出的共性路径。核心原则是:先探测边界,再读源码,后造payload,不要上来就瞎猜。

最后再分享一个个人习惯。读源码这个操作在Web题里非常关键,很多高手拿到题第一件事不是打flag,而是想办法找源码、读配置、看路由。因为黑名单过滤规则这种东西,靠黑盒试探往往要试很多次,而直接读源码等于把答案摆在你面前。就像这道题,如果不读index.php,你可能还在猜到底过滤了哪些符号,读了源码以后,路径非常清晰:过滤了符号,但留下了$;,那就用这两个符号打出一条路来。

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

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

立即咨询