坐在电脑前复现某次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、flag这种写法绕过第二过滤发生在命令执行之前而shell的变量展开、特殊字符解析发生在命令执行的那一瞬间这两者之间存在一个时间差。以空格过滤为例。后端正则检查你提交的原始字符如果里面有空格就拒绝。但你提交$IFS的时候正则检查看到的是美元符号和三个字母没有任何空格。等到命令真正交给shell执行shell会把$IFS这个环境变量展开成它保存的分隔符默认就是空格。于是黑名单检查的那一秒没看到空格shell执行的那一秒空格出现了这就是时间差带来的绕过。这块逻辑我建议画个时间线去理解虽然不能画流程图但你在脑子里过一下就行了服务端拿到原始输入字符串正则黑名单检查原始字符串通过检查后字符串被拼进命令shell拿到最终命令开始解析shell把环境变量、通配符、引号等语法结构展开展开后的结果才是真正执行的命令绕过目标就是让步骤2看到的字符串和步骤6实际执行的命令不一致。这里没有魔法靠的是shell自身的语法特性。2.2 绕行路线图读flag文件最终还是要执行类似cat flag.php的命令。从这个目标出发可以把障碍和对应绕过方式列成一张表实际做题时也基本是照着这个路线往下走。障碍赛题常见过滤形式常用绕过手法原理简述空格被过滤正则检查空白符或替换空格为空$IFS、${IFS}、$IFS$1、tab替换利用环境变量展开或字符替换生成分隔符flag关键字被过滤正则检查单词flag并拒绝fl\ag、flag、变量拼接、通配符fla??让正则匹配不到完整字符串shell解析后合成为flag斜杠被过滤正则检查/字符$HOME拼接、cd目录后用相对路径、通配符避免直接在payload里出现/符号多个命令需要执行未过滤分号;、|、、换行符%0a用shell的命令分隔符串起多条命令输出中有关键字被二次过滤输出内容替换flag为空tac flag.php | base64编码后再输出避免关键字出现在响应里表格列出的只是主干实际做题时还会遇到排列组合的情况比如既过滤空格又过滤flag那就把两种手法串在同一个payload里。这条路的本质是让黑名单检查器看到的是一串无害字符让shell最终解析出来的是我们想要的恶意命令。3. 三种关键绕过手法的实操拆解3.1 摆脱空格限制$IFS与环境变量先处理最常见的空格过滤。你要在shell里执行cat flag.phpcat和flag.php之间必须有分隔符。正常是空格但空格被ban了就得找shell里其他能充当单词分隔符的东西。IFSInternal 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卡了半天后来才反应过来变量名被连读了。再补充一个技巧如果连$符号也被过滤了空格还是有替代方案的。最经典的是利用输入重定向catflag.php它用重定向符号把文件内容喂给cat命令和文件名之间不需要空格。也能用{cat,flag.php}这种花括号展开写法逗号充当分隔符。不过花括号展开的细节受shell版本和语法影响比较大实战时不如$IFS稳定。3.2 突破flag关键字转义、拼接与通配符空格绕过去了下一个坎是flag字样。后端如果正则匹配flag这个完整单词那提交flag.php就会被直接掐掉。这时候思路是把flag拆开让正则匹配不到完整词但shell解析后又能组合回flag。关于这一块我前三板斧是反斜杠、引号拼接和变量拼接cat${IFS}fl\ag.php cat${IFS}flag.php afl;bag.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里加个base64cat${IFS}fla\g.php|base64这样返回的是一串base64文本再本地解码就能看到原始文件内容。这道题我没记错的话flag文件本身可能是个PHP脚本直接读会连同PHP代码一起出来用编码输出反而更干净。3.3 组合payload的构造思路单个绕过手法学会之后重点就在组合。我的构造习惯是先明确目标命令再一步步反推需要的字符。目标是执行cat flag.php障碍是空格和flag字样。空格换成${IFS}cat${IFS}flag.phpflag关键字被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 交互式探测注入点我通常先用最简单的方式判断有没有注入点比如直接提交ip127.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}flag.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$1payload在本地通、靶场不通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环境里观察表现很多时候只是语法细节的事。后续有机会的话我打算再写一篇关于命令注入盲注场景的实战笔记那种情况下没有回显拿数据的方式又会完全换一套打法。