1. 初探靶场:一个看似简单的Ping命令背后

大家好,我是老张,在安全圈摸爬滚打十来年,特别喜欢研究CTF(Capture The Flag)里的那些“弯弯绕”。今天咱们就来聊聊BUUCTF平台上的一道经典题目——[GXYCTF2019]Ping Ping Ping 1。这题名儿起得挺直白,就是让你Ping嘛,但真上手了你就会发现,它远不止“Ping”一下那么简单。这其实是一个典型的命令执行漏洞实战场景,核心考察的是我们在各种“枷锁”(过滤规则)下,如何灵活“跳舞”(绕过限制),最终拿到目标服务器上的flag。

咱们先来模拟一下新手刚接触这道题时的场景。你打开题目链接,映入眼帘的往往就是一个极其简洁的页面,上面可能就一行字,或者一个输入框,提示你输入一个IP地址。题目地址通常会有一个参数叫ip,比如/?ip=。很多朋友第一反应就是:这还不简单?输入个127.0.0.1或者8.8.8.8试试。一提交,页面返回了Ping命令的结果,显示目标主机是通的。这时候你可能觉得索然无味,这不就是个Ping功能测试页吗?

但CTF的乐趣(或者说坑)就在这儿。题目名字叫“Ping Ping Ping”,它明明白白告诉你这里能执行Ping,但暗地里是希望你利用这个执行点,去做更多事情。关键就在于那个ip参数,它被直接拼接进了系统命令里。题目后台的PHP代码逻辑大致是:获取你传入的ip值,然后拼接到ping -c 4 这个命令后面,最后通过像shell_exec()这样的函数去执行。你想啊,如果你输入的不仅仅是IP地址,而是一条完整的系统命令,会发生什么?这就是命令注入的根源。

所以,我们的第一步试探,绝不是满足于Ping通。我会习惯性地试试管道符。在Linux的Shell里,管道符 | 的作用是把前一个命令的输出,作为后一个命令的输入。我们在输入框里尝试构造这样的payload:?ip=127.0.0.1|ls。它的意图是,先执行ping -c 4 127.0.0.1,然后将其输出(其实我们并不关心)通过管道传给ls命令执行,ls会列出当前目录下的文件。如果页面返回的内容不再是单纯的Ping结果,而是出现了文件列表,比如flag.php和index.php,那么恭喜你,命令执行漏洞的大门就被你敲开了。这第一步,我们称之为漏洞验证,是实战中至关重要的一环,确认存在漏洞,我们才能进行后续的深入利用。

2. 直面过滤:解码源代码中的黑名单规则

当我们用?ip=127.0.0.1|ls成功列出目录后,目标很明确:读取flag.php文件的内容。很自然地,我们会尝试?ip=127.0.0.1|cat flag.php。但一提交,页面很可能返回一句“fxck your space!”。好了,第一个拦路虎出现了:空格被过滤了。

在命令执行漏洞的利用中,空格是分隔命令和参数的关键字符。过滤了空格,相当于给我们的命令“捆上了手脚”。但别急,这时候我们不能蛮干,得先搞清楚对方到底设置了哪些规则。最有效的方法,就是去读取题目本身的源代码,通常就是当前目录下的index.php。既然空格被过滤,我们就得用绕过空格的方法来读它。当时我试了用<符号代替空格,构造?ip=127.0.0.1|cat<index.php。有时候能成,但在这道题里,我遇到了“fxck your symbol!”。这说明,它不止过滤了空格,还过滤了一堆特殊符号。

这促使我们必须看到完整的过滤逻辑。我们可以尝试用$IFS$1来绕过空格(这个我们后面细说),构造?ip=127.0.0.1|cat$IFS$1index.php。如果成功,页面的源代码就会展示在我们面前。这串源代码就是我们的“藏宝图”,里面写明了所有机关陷阱。我把它贴出来,并逐行给大家拆解一下:

<?php
if(isset($_GET['ip'])){
  $ip = $_GET['ip'];
  if(preg_match("/\&|\/|\?|\*|\<|[\x{00}-\x{1f}]|\>|\'|\\"|\\|\(|\)|\[|\]|\{|\}/", $ip, $match)){
    echo 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("/bash/", $ip)){
    die("fxck your bash!");
  } else if(preg_match("/.*f.*l.*a.*g.*/", $ip)){
    die("fxck your flag!");
  }
  $a = shell_exec("ping -c 4 ".$ip);
  echo "<pre>";
  print_r($a);
}
?>

我来翻译一下这段代码的“防守策略”:

  1. 第一层(符号过滤):一个非常严格的正则表达式,过滤了&, /, ?, *, <, >, ‘, “, \, (, ), [, ], {, },还包括了十六进制从00到1F(即00到20)的控制字符。这基本上把很多常见的命令连接符和重定向符号都给禁了。比如我们想用|(管道),它没禁,运气不错。但想用&&、||、;(分号)?&被禁了。想用<>重定向?<和>都被禁了。这招挺狠。
  2. 第二层(空格过滤):简单粗暴,检测字符串里是否有空格,有就报错。
  3. 第三层(bash过滤):检测是否包含“bash”字符串。这是为了防止我们直接调用bash -c来执行更复杂的命令。
  4. 第四层(flag过滤):一个非常狡猾的正则/.*f.*l.*a.*g.*/。这意味着,只要你的输入里,f、l、a、g这四个字母按顺序出现,哪怕中间被任意字符隔开(.*代表任意数量任意字符),都会被拦截。你想直接写cat flag.php,门儿都没有。甚至你写cat fl ag.php(中间多个空格)或者cat f*l*a*g.php,都会被匹配到。

看明白这个,你是不是倒吸一口凉气?感觉路都被堵死了。但别灰心,搞安全的,玩的就是“道高一尺,魔高一丈”。这些过滤规则,恰恰告诉了我们哪些路不能走,以及,哪些路可能还没被堵死。比如,它没过滤反引号 ```,没过滤$符号,没过滤某些变量。这就是我们突破的关键。

3. 绕过艺术:实战中常用的几种“花招”

面对重重过滤,我们需要一个“花招”工具箱。下面我结合这道题,给大家详细拆解几种最常用、也最有效的绕过方法。这些方法不仅在CTF里有用,在一些实际的渗透测试或代码审计场景中,也可能遇到类似的防御,思路是相通的。

3.1 空格绕过:给命令“松绑”

空格被过滤了,但我们命令里的参数总得分开啊。这时候就需要找空格的“替身”。我实战中常用的有这么几个:

  • $IFS:这是Linux Shell中的一个特殊环境变量,叫做“Internal Field Separator”,内部字段分隔符,默认值就是空格、制表符、换行符。所以cat$IFSflag.php在Shell看来,就等同于cat flag.php。但有时候光用$IFS可能不行,因为它在某些上下文中会被直接解析为空。这时候可以加个数字,比如$IFS$1或$IFS$9,$1、$9是Shell的位置参数,这里我们没传递,它们就是空,但和$IFS连在一起,能确保$IFS被正确识别为一个变量名的一部分从而被解析。在这道题里,?ip=127.0.0.1|cat$IFS$1index.php 就是靠这个成功读到源码的。
  • < 和 > 重定向:cat<flag.php,意思是把flag.php文件的内容作为标准输入传给cat命令,cat会把它打印出来。这就不需要空格了。可惜这道题里<和>符号在第一关就被过滤了,此路不通。
  • 花括号{}扩展:{cat,flag.php},在Shell解析时,花括号会被展开,中间用逗号分隔,最终变成cat flag.php。但注意,这道题的花括号{和}也在第一关被过滤了。
  • 制表符%09:在URL中,空格可以编码为%20,制表符可以编码为%09。有时候前端过滤了空格字符,但没过滤其URL编码形式。我们可以尝试?ip=127.0.0.1|cat%09flag.php。不过这道题过滤了空格,但不确定是否对编码形式也做了检测,可以一试,但通常更底层的过滤会直接匹配字符本身。

我的经验是,$IFS系列是成功率最高的空格绕过方式之一,尤其是在Web题目中,因为它看起来就是一个普通的变量名,不容易被简单的字符串匹配规则拦截。

3.2 关键词绕过:让“flag”隐身

解决了空格,我们卡在了最恶心的一层过滤:/.*f.*l.*a.*g.*/。你不能在payload里直接出现“flag”这个连续字符串,甚至不能按顺序出现这四个字母。怎么办?两个核心思路:变量替换和编码混淆。

变量替换是这道题最巧妙的解法。我们注意到,PHP代码最后是用shell_exec(“ping -c 4 “.$ip)来执行的。我们的输入$ip被拼接进命令字符串。那么,我们能不能在$ip这个字符串里,动态地构造出“flag”这个词,但又让它不是直接出现的呢?当然可以,利用Shell的变量赋值。

我们构造这样的payload:?ip=127.0.0.1;a=g;cat$IFS$1fla$a.php。我们来拆解一下:

  1. 127.0.0.1:正常的Ping目标,用来通过基础检查。
  2. ;:命令分隔符。执行完Ping后,继续执行后面的命令。幸运的是,分号;没有被过滤!
  3. a=g:这是一个Shell命令,意思是定义一个变量a,其值为字符串g。
  4. cat$IFS$1fla$a.php:执行cat命令。$IFS$1是空格。fla$a.php是什么呢?Shell在解析时,会进行变量替换,将$a替换成它的值g。所以fla$a.php就变成了flag.php。

妙啊!在整个payload字符串中,f,l,a,g这四个字母从未按顺序直接出现。f,l,a出现在fla里,g是作为变量$a的值在命令执行时才被拼接上去的。正则匹配是在PHP层对原始输入字符串$ip进行的,它检测不到这种运行时才产生的组合,于是完美绕过。

编码混淆是另一种通用思路。既然直接写不行,我就把它编码一下。比如,我们可以对cat flag.php这个命令字符串进行base64编码,得到Y2F0IGZsYWcucGhw。然后想办法在Shell里解码并执行它。常用的方法是借助管道和base64命令:echo$IFS$1Y2F0IGZsYWcucGhw|base64$IFS$1-d|sh。意思是:echo输出编码后的字符串,通过管道传给base64命令解码(-d参数),再通过管道传给sh(Shell)去执行。这里我们用了sh,因为bash被过滤了。这个payload同样不包含连续的flag字符。不过在这道题里,你需要确保base64命令在目标系统上可用。

3.3 内联执行:反引号的魔力

这是一种更高级、也更需要理解的技巧,叫做内联执行。它利用的是反引号 ```(或$())的功能:Shell会先执行反引号内的命令,并将其输出结果替换到原命令中。

我们来看这个payload:?ip=127.0.0.1;cat$IFS$9ls“。它做了以下几步:

  1. 127.0.0.1;:照常Ping,然后分号分隔。
  2. `ls`:反引号内的ls命令会先被执行。假设当前目录下有flag.php和index.php,那么ls的输出就是flag.php index.php(中间有空格或换行)。
  3. 这个输出结果flag.php index.php会替换掉原命令中的`ls`部分。
  4. 于是,最终的命令变成了:cat flag.php index.php。
  5. cat命令会依次读取并打印这两个文件的内容。这样,我们虽然因为过滤不能直接写flag.php,但通过ls命令动态地获取了文件名,间接地读到了flag。

这个方法的精髓在于“借力打力”。它完全规避了在输入中直接出现敏感文件名的问题,而是让系统自己告诉我们文件名是什么。这需要你对Shell的命令替换有深刻的理解。注意这里用了$IFS$9作为空格,原理和前面一样。

4. 思维扩展:从这道题看命令执行的防御与绕过

通过这道题,我们其实可以学到很多关于安全攻防的本质。攻击者在不断寻找过滤规则的盲点,而防御者则在努力让这个盲点最小化。

从攻击者(绕过)视角看,我们需要掌握一个层次化的测试方法:

  1. 信息收集:首先确认漏洞存在(如用|ls),然后尽可能获取源码,明确过滤规则。这是所有后续操作的基础。
  2. 字符集测试:系统地测试哪些字符能用。分号;、管道|、反引号```、$、IFS、{、}、@、+等等。有时候一些看似无用的字符在特定组合下能产生奇效。
  3. 命令替代:bash被禁了,就用sh、dash、zsh,或者用更底层的/bin/sh。cat被禁了,就用tac(反向输出)、more、less、head、tail、nl,甚至grep、awk、sed这些文本处理工具来读文件。
  4. 编码与混淆:除了base64,还可以考虑hex编码、rot13、xxd等。或者利用通配符?和*来匹配文件名,比如cat fla*.php,但要注意这道题的正则可能会匹配到*f*l*a*g*。
  5. 利用环境变量:除了自己定义变量(a=g),还可以看看现有的环境变量里有没有可用的字符。比如${PATH:0:1}可能提取出/,${HOME:0:1}可能提取出/或~,用于路径拼接。

从防御者(开发)视角看,这道题的黑名单过滤方式其实存在很大隐患:

  • 黑名单永远不全:你很难穷举所有可能的绕过方式。像$IFS、内联执行这种,非安全专业的开发者很可能想不到。
  • 正则表达式的陷阱:题目中/.*f.*l.*a.*g.*/这个正则,看似严格,但用变量替换就轻松绕过。这告诉我们,单纯依赖字符串匹配来过滤关键词是非常脆弱的。
  • 正确的防御姿势应该是白名单+参数化。对于ip参数,应该严格用正则验证其是否为合法的IPv4或IPv6地址格式(白名单),而不是过滤非法字符(黑名单)。更根本的是,避免将用户输入直接拼接进命令字符串。如果必须执行命令,应使用安全的API,如escapeshellarg()或escapeshellcmd()对参数进行转义,或者使用更安全的、能指定参数数组的函数(如proc_open()配合正确的参数传递),从根本上杜绝命令注入的可能。

5. 实战复盘与经验之谈

回过头来看这道题,它就像是一个精心设计的迷宫,每一步过滤都引导你去思考一种新的绕过技巧。我最初做的时候,也卡在了flag过滤那里。当时试了通配符、试了编码,都因为各种细节问题没成功。直到静下心来,重新审视那句shell_exec(“ping -c 4 “.$ip),才猛然意识到,我的输入是被拼接进去的,那么我完全可以在拼接的过程中“做手脚”。变量替换的思路就是这样蹦出来的。

在实际的漏洞挖掘或渗透测试中,遇到命令执行点,我的排查思路通常是这样的:

  1. 盲测:先简单测试|id、;id、&&id等,看是否有回显。如果没有回显(盲注),可能需要尝试用curl或wget带出数据,或者用sleep命令通过时间延迟来判断。
  2. 探测过滤:如果发现有过滤,就系统性地测试命令分隔符、空格、常见符号、关键词。我会准备一个小字典,里面包含各种绕过姿势的payload,用工具批量测试,观察返回结果的差异。
  3. 上下文利用:仔细看漏洞点的上下文。是PHP的shell_exec、system、exec,还是Python的os.system、subprocess?是Linux环境还是Windows环境?这决定了可用的命令和语法。比如在Windows下,空格可以用%ProgramFiles%这样的环境变量来绕过,命令分隔符可能是&、|、&&。
  4. 步步为营:不要一开始就想着直接cat /flag或whoami。先从无害的命令开始,比如ls、pwd、uname -a,了解当前目录和环境。再慢慢探索目录结构,寻找敏感文件。
  5. 稳定利用:找到flag文件后,考虑如何稳定地读出内容。如果命令执行有回显,直接cat就行。如果是盲注,可能需要用base64编码后分块带出,或者用xxd转换成hex再传输。

这道题虽然是个CTF题目,但它浓缩了Web命令执行漏洞的精华。它告诉我们,面对安全防护,不能有直线思维。有时候,看似最直接的路径被堵死了,绕几个弯,换一种表达方式,就能柳暗花明。这种“绕过”的思维,不仅仅是技术,更是一种解决问题的创造性方式。多玩玩这样的题目,对于提升你在实际工作中的代码审计能力和安全攻防思维,绝对是大有裨益的。下次你再看到类似的功能点,脑子里自然会浮现出各种测试payload,这就是经验的积累。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐