
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_seq1 ttl64 time0.038 ms ...从功能上看这就是一个在线ping检测的小工具。但这种工具如果后端直接拿用户输入拼进shell命令就很容易出问题。实测下来这个输入框的值是通过GET参数ip传过去的也就是说你可以直接在URL里构造请求http://目标地址/?ip127.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或者FlagLinux文件名区分大小写改了就读不到。要想想正则匹配的本质是什么。2.3 读index.php源码还原三重过滤既然直接猜过滤规则效率太低不如先读源码。怎么读用刚才已经验证有效的方法读index.php127.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}先变成cat和fla$a.php两个词然后变量$a再展开。如果你设ag最后得到的就是cat flag.php理论上可行。但我实测中更喜欢用$IFS$9因为花括号在不同shell下的行为有差异$IFS$9更通用、更稳。3.2 flag关键字用变量拼接骗过正则空格解决了再来对付flag过滤。第三层正则preg_match(/flag/, $ip)匹配的是HTTP请求里ip参数的原字符串也就是PHP拿到的那一串。它不会去执行shell命令也不知道变量展开后是什么。但shell会。bash在执行命令前会做变量展开把$变量名替换成对应值。利用这个信息差可以轻松绕过关键字过滤。思路是把flag拆成两段一段直接写在请求里另一段塞进变量然后拼起来。比如ag;cat$IFS$9fla$a.php请求串里出现的是fla$a.php没有连续的flag字样正则匹配不到。但shell执行时先执行ag给变量赋值然后cat$IFS$9fla$a.php展开$a的值是g拼到fla后面就变成了flag最终执行的命令是cat flag.php这就是这套绕过的核心原理正则检查的是请求文本shell执行的是展开后的命令这两个阶段之间存在一个“时间差”。payload组织成完整URLhttp://目标地址/?ip127.0.0.1;ag;cat$IFS$9fla$a.php执行顺序是ping -c 4 127.0.0.1;ag;cat$IFS$9fla$a.php分号分隔三条命令依次是ping回环地址、给变量a赋值、读flag.php文件。最后一条命令展开后就是cat flag.phpflag直接打印在页面里。类似地你也可以把flag拆成其他组合比如blag;cat$IFS$9f$b.php或者afl;bag;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://目标地址/?ip127.0.0.1;ag;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。第三个坑是变量拼接时一定要算清楚展开结果。比如我一开始图省事写aag;cat$IFS$9fl$a.php结果展开后是cat flag.php还是cat flg.php想清楚fl$a.php展开为flag.php得到flag.php没问题。但如果你写ag;cat$IFS$9fl$a.php展开后是flg.php就差一个字母。这类问题千万别靠脑子硬算先在本地shell里跑一下echo看展开结果echo fl$a.php看到输出flag.php再往题面上打。第四个坑是被过滤的符号可能不止你看到的那些。有的平台在题目外层又套了一层WAF导致本地能打的payload到远程被拦。遇到这种情况多看回显提示判断是题目自身的过滤还是平台附加的过滤再针对性调整。4.3 命令注入题通用排查清单刷多了命令注入题之后我总结了一套通用排查步骤遇到类似的题直接按这个顺序走能省很多时间步骤操作目的1先测分隔符;、、||、2测最简单的ls、whoami确认命令能执行3测空格是否被过滤直接带空格请求判断第一道过滤4测常用符号/、反引号、$()、?、*、引号摸清黑名单范围5如果目标站点有PHP源码优先读源码获取精确过滤规则6根据过滤规则选择绕过姿势空格用$IFS$9关键字用变量拼接等7最终验证并读取flag注意编码和注释这套清单不是我凭空想出来的是刷了十几道命令注入题以后总结出的共性路径。核心原则是先探测边界再读源码后造payload不要上来就瞎猜。最后再分享一个个人习惯。读源码这个操作在Web题里非常关键很多高手拿到题第一件事不是打flag而是想办法找源码、读配置、看路由。因为黑名单过滤规则这种东西靠黑盒试探往往要试很多次而直接读源码等于把答案摆在你面前。就像这道题如果不读index.php你可能还在猜到底过滤了哪些符号读了源码以后路径非常清晰过滤了符号但留下了$和;那就用这两个符号打出一条路来。