☰
Phar反序列化与Filter链:从文件包含到RCE的完整攻击链
2026/10/16 7:15:19 网站建设 项目流程

在给某内部系统做安全巡检的时候,我发现一个特别典型的现象:他们代码里没有任何一处显式的unserialize(),于是研发和老板都很放心,觉得“反序列化漏洞离我们很远”。可当我往一个图片预览接口的参数里塞进去一个phar://开头的路径时,系统直接读取并反序列化了文件中的对象元数据,配合一条常见的魔术方法链,最终拿到了目标服务器的命令执行权限。整个过程发生在一个表面上“只有文件读写”的功能模块里,没有反序列化入口、也没有明显的代码执行点。

这类组合攻击正是当前PHP环境中比较棘手的一类问题:攻击入口是文件包含/文件操作,中间靠Phar反序列化完成对象注入,再用php://filter链来解决源码读取、编码绕过和payload写入的问题。很多人分别听说过这三个名词,但不知道它们为什么常常一起出现、相互之间如何衔接。这篇文章我就按实际攻击链路展开,把Phar反序列化、Filter链、文件包含三者的原理、触发条件和防御做法一次讲透,适合做漏洞挖掘、代码审计、红蓝对抗或安全加固的读者。

1. 为什么Phar反序列化、Filter链、文件包含容易凑到一起

1.1 三个问题共享同一个“入口面”:PHP流包装器

PHP的很多文件操作不是只能读本地磁盘路径,它会把你传入的路径交给一组“流包装器”处理。phar://、php://、data://、zip://这些都是流包装器。对开发者来说,file_get_contents('phar://uploads/evil.jpg/test.txt')看起来是在读文件;对PHP引擎来说,它先走了phar包装器的解析逻辑,而该逻辑会从phar文件的元数据区取出一段序列化数据做unserialize()。

文件包含漏洞同样也是走流包装器:include($_GET['file'])里如果$_GET['file']是php://filter/read=convert.base64-encode/resource=config.php,include就会先把config.php内容做base64编码再交给PHP语法解析,由于内容已经被转义成纯文本,配置源码就漏了出去。Filter链本质上是php://协议上一个可自由组合的过滤器管道。

1.2 攻击者视角下的一次“最小衔接”

从攻击者角度看,这三个能力其实拼成一条路径:

  • 弱点A:某个参数被带进include/require/file_get_contents等文件函数,构成文件包含或任意文件读取;
  • 弱点B:目标环境下有一个可写的上传点,允许攻击者塞入一个“伪装的Phar文件”;
  • 弱点C:业务代码或某框架里存在一条可用的反序列化POP链(从__destruct/__wakeup/__toString等魔术方法进入危险函数)。

把A和C通过phar://连接,就是一条典型的“Phar反序列化触发链”;B用于准备trigger文件;Filter链则用于中途读源码、绕过编码限制或写入payload。

1.3 为什么很多系统对这三个攻击面毫无感知

因为防御方经常按“漏洞类型”而不是按“数据流向”去布防。代码里没有unserialize(),就认为没有反序列化风险;allow_url_include已经关闭,就认为文件包含翻不起浪;WAF里有“php://”的正则,就认为Filter链被堵死了。但真正的风险往往藏在业务逻辑里:文件上传后可能用is_image()检查文件头,finfo_file()、getimagesize()这些看似人畜无害的函数也会触发phar元数据反序列化;php://filter在很多include场景下不受allow_url_include=Off限制。

这就解释了为什么这三个东西经常出现在同一个漏洞故事里:它们不是三个孤立的“洞”,而是同一套文件路径处理机制的三个出口。防御时如果只堵一个出口,攻击者换一个包装器、换一个文件函数就能绕过去。

2. Phar反序列化:真正的引爆点是文件操作函数,而不是unserialize()

2.1 phar文件为什么自带“反序列化按钮”

Phar(PHP Archive)是一个把PHP脚本、资源文件打包在一起的归档格式,设计初衷是做应用打包分发。Phar文件由四部分组成:stub(一段PHP代码,固定以__HALT_COMPILER();收尾)、manifest(清单,包含文件列表、属性、元数据等)、文件内容、签名。其中manifest里有一个metadata字段,它保存的是经过serialize()处理的一段数据。当PHP通过phar://包装器去访问一个Phar归档时,会解析manifest并把metadata做unserialize()。

关键在于:phar文件的扩展名完全不受限制。攻击者完全可以把一个正常的phar文件改名成avatar.jpg,甚至在stub最前面加一段GIF89a之类的字节,让getimagesize()都以为它是一张图片,但只要你的代码用文件函数访问phar://uploads/avatar.jpg/...这个路径,PHP就会去解析它并把metadata交给unserialize()。

2.2 哪些常用函数会“帮攻击者按下按钮”

下面这些函数在传入phar://路径时都会触发解析。我列一个实际验证过的常用清单:

函数分类具体函数
存在性检查file_exists、is_file、is_dir、is_link、stat、filesize
文件读取file_get_contents、fopen、fread、readfile、file、parse_ini_file、hash_file、md5_file、sha1_file
元数据/图像getimagesize、exif_read_data、exif_thumbnail、finfo_file
文件操作copy、rename、unlink、touch、chmod、chgrp、chown、fileowner、fileperms、filemtime、fileatime、filectime

要理解为什么这些函数都能触发:它们内部统一调用php_stream_open_wrapper_ex(),当一个路径以phar://开头时,它会加载Phar模块的stream handler,并在handler解析目录和文件信息时读取manifest。也就是说,你不是调用了unserialize()才会反序列化,而是涉及phar://的“任何一次文件系统操作”都在触发反序列化。

2.3 本地构造一个最小可运行的Phar PoC

生成一个带恶意metadata的phar很简单,但有两个前提:本机PHP的phar.readonly要设为Off,且目标环境不需要能生成phar(只要能解析就行)。示例代码:

<?php // 本地生成phar时要求 php.ini 里 phar.readonly = Off class DemoPayload { public $command = 'whoami'; public function __destruct() { system($this->command); } } $phar = new Phar('poc.phar'); $phar->startBuffering(); // 把phar伪装成图片,很多上传点只看前几个字节 $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->addFromString('ok.txt', 'test'); $phar->setMetadata(new DemoPayload()); $phar->stopBuffering();

然后在受害者环境里,只要存在以下任意一段代码,都会触发上面DemoPayload::__destruct():

file_exists('phar://uploads/poc.phar/ok.txt'); getimagesize('phar://uploads/poc.phar'); finfo_file($finfo, 'phar://uploads/poc.phar');

2.4 没有现成POP链怎么办

很多人问:metadata反序列化出来了,但没有利用链怎么办。实际场景里通常不依赖项目自己写的类,而是去翻目标用到的框架或组件包,找一条通用链。这些框架里总有几个类带有__construct/__destruct/__toString魔术方法,并且方法体里会调用可影响的对象方法,比如文件删除、文件写入、缓存操作等。只要能把可控参数导向某个能落地危险动作的方法,反序列化对象就被接成了一条链。这部分动作和传统unserialize()利用思路完全一样,区别只在于触发方式。

另外还有个细节:PHP版本会影响metadata的解析行为,部分版本对phar://路径上下文做了更严格的限制,我不建议只看网上的通用PoC,应该在目标版本上做一轮实测确认。

3. php://filter链:既能当“读码器”,也能当“解码器”

3.1 一段最常见的filter读源码用法

比如存在:

include $_GET['file'];

你直接传/etc/passwd会被include执行失败或输出空,因为include会尝试把内容当作PHP代码执行;直接传config.php什么都看不到,因为源码被PHP引擎消费了。这时用filter:

php://filter/read=convert.base64-encode/resource=config.php

include看到一个php://filter流,于是先把config.php的内容流经convert.base64-encode过滤器,再把编码后的字符串交给include去执行。base64的字符串里全是字母数字和=号,不是合法PHP代码,于是它会被当作纯文本原样输出,你拿到一串base64,解码就是源码。

filter链的“链”字就体现在这里:过滤器可以按顺序拼接,用管道符|连接:

php://filter/read=convert.base64-encode|string.rot13/resource=config.php

读到的内容先rot13再base64,绕过了某些WAF里对base64关键字的简单匹配。

3.2 filter链怎么当“解码器”参与写入

如果目标代码是这样的:

$content = $_POST['data']; file_put_contents($path, $content . "<?php exit();?>");

你发过去的payload后面会追加一个exit标签,导致整个文件无法被作为PHP执行。解决思路不是去掉exit,而是让整个写入内容先经过一层base64解码。只要$path被指定为php://filter/write=convert.base64-decode/resource=shell.php,并且你上传的$content是一个合法的base64串加上一些凑64倍数用的垃圾字符,那么最终落盘的文件里就不存在原先的exit标签,只剩下解码后的PHP payload。

注意写入方向的过滤器参数名是write而不是read,很多人栽到这里;另外.号、/号、+号在base64解码时都会被忽略,所以垃圾字符怎么塞进主要触发点在于凑足64的整数倍。

3.3 字符集过滤器与WAF绕坑

PHP提供convert.iconv.*过滤器,可以自由切换字符集。在一些必须让字符串以特定编码落盘的场景,iconv能帮你把UTF-8内容转成UTF-16/UCS-2等。很多WAF规则会检查请求体里是否存在<?php,但你把它编成UTF-16后规则直接失效,而PHP文件本身会按BOM或配置识别编码并执行。

这类操作在CTF里很常见,在真实系统上也一样有效,因为PHP的file_put_contents和include都会先经过流过滤器再落盘。从防御角度看,最费劲的也是这里:filter链可组合的种类太多,单纯匹配convert.base64的规则很容易被绕过。

3.4 再次强调:filter链和Phar可以互相配合

Filter链不只是“读源码/绕WAF”的工具,它还能作为一个稳定的前置步骤:先通过filter读源码摸清业务过滤规则和底层框架版本,再决定构造哪条POP链、phar文件用什么stub去伪装。整个攻击过程中,filter链承担的是“侦察和编码加工”的角色。

4. 文件包含漏洞如何一步步升级成RCE

4.1 两个需要留意的基础点:allow_url_include与伪协议

文件包含漏洞本身大部分时候只是“信息泄露”级别。要升级成RCE,要借助三种路径:

  1. 引入远程代码(需要allow_url_include=On):http://、data://text/plain;base64,...、php://input;
  2. 引入可控制的本地文件(日志、session、临时文件);
  3. 配合Phar反序列化完成对象注入。

注意:allow_url_include=Off只关闭了http://、ftp://等真正的URL包装器,不代表php://filter不能用,所以在LFI里filter读码的场景依旧广泛存在。php://input在include中通常需要allow_url_include=On,这点和filter不一样。

4.2 日志文件包含、session文件包含的通用思路

日志包含的原理想必大家都清楚:如果你能包含/var/log/nginx/access.log,攻击者在User-Agent头里构造一段<?php ... ?>,日志就会记录下这段内容,include的时候日志内容被当作PHP执行。目录路径往往需要先通过LFI去探测。限制也很明显:日志可能被清理、内容过长会撑爆include、默认日志格式会包含大量干扰字符导致语法错误。所以我更推荐的做法是发一个只含<?php ... ?>的UA,让日志行尽量干净。

session文件包含是另一个稳定路子。PHP默认把session文件放在/var/lib/php/sessions/sess_<session_id>或临时目录中,内容形如username|s:5:"admin";。若应用把用户可控数据写进session,就可以包含session文件。配合session.upload_progress:上传文件时PHP会把进度信息写入session文件,即使没有正常登录session也能利用。

4.3 完整组合示例:LFI + filter + phar 的一次实战走向

模拟某后台的“图片预览”接口:$_GET['img']直接拼接进file_get_contents('uploads/' . $img),输出按图片类型回显;另一个“模板展示”存在include参数漏洞。实际走向:

  1. 用filter读码:php://filter/read=convert.base64-encode/resource=demo.php回显源码,确认业务并没有对img做协议过滤;
  2. 上传一个伪装为GIF的恶意phar文件到uploads/(通过头像上传点);
  3. 构造phar://uploads/avatar.jpg/test.txt让file_get_contents触发metadata反序列化;
  4. 反序列化POP链执行命令。

真实场景中包括服务器路径、是否可写、框架有无POP链都要逐一验证。但核心逻辑是清晰的:文件包含/文件操作入口提供可控路径,phar提供反序列化触发点,POP链提供代码执行,filter链提供情报与编码支持。

4.4 为什么不要迷信“我们关闭了远程文件包含”

很多系统以为allow_url_include=Off就安全了,但上面的组合攻击中,根本没用http://或data://,用的是phar://,而phar://不受allow_url_include控制。所以“关闭远程包含”只解决一部分问题,真正的防线是“所有文件路径参数都必须严格校验”。

5. 防御侧怎么做:配置、代码、上传与日志四个抓手

5.1 PHP侧配置清单

配置项建议说明
allow_url_includeOff默认也是Off,但有的集成环境会开
phar.readonlyOn主要阻止在服务器上直接生成phar
open_basedir站点根+session+tmp限定文件函数可访问范围
disable_functions按需禁用命令执行函数纵深防御,不能完全依赖
session.save_path独立目录避免与web目录混用
phar扩展业务不需要就移除直接消除Phar反序列化触发面

5.2 代码侧最有效的两个校验点

一是白名单。include参数不走拼接,而是拿参数值查配置里的映射表。例如:

$allowed = [ 'home' => '/app/pages/home.php', 'about' => '/app/pages/about.php', ]; if (isset($allowed[$_GET['page']])) { include $allowed[$_GET['page']]; }

这是最彻底的做法。

二是路径规范化后校验真实路径前缀。对用户传来的文件路径先做realpath(),再用str_starts_with()或DIRECTORY_SEPARATOR比较是否落在允许目录下。同时要拦截协议头:phar://、php://、zip://、data://等。注意大小写、URL解码、双重编码。建议操作顺序:先做输入解码,再去协议头,再realpath(),最后做前缀校验。

5.3 上传场景与Phar防利用

上传点往往是整个链条的触发器。反Phar思路有三个层面:

  • 存储前做Phar特征扫描:检查文件头部是否包含<?php __HALT_COMPILER();,即使前面有GIF89a也能命中;
  • 上传后重命名、随机文件名,并以图片扩展名落盘;同时web目录禁止执行PHP(例如Nginx location匹配uploads目录内不解析PHP脚本);
  • 业务代码不要拿用户可控的文件名直接调文件函数,应该把文件ID作为参数,由服务端拼接实际路径。

5.4 日志检测特征与应急思路

给一份可直接落地的告警特征表:

异常特征建议动作
请求URL中含phar://告警并重点排查文件操作接口
请求URL中含php://filter告警并排查源码是否已泄露
请求URL中含data://、expect://等告警并检查allow_url_include
UA中含<?php/base64_decode(告警并排查日志是否被用作攻击入口
上传文件含__HALT_COMPILER直接阻断并删除

应急流程:先暂停疑似接口;查access日志中带上述特征的请求;对上传目录做全量grep,重点找__HALT_COMPILER、eval(、base64_decode(、system(等敏感字符串;确认后清理文件、重启PHP-FPM;最后针对接口补上白名单校验并复测。

最后分享一个我自己的经验:防御Phar反序列化,真正性价比最高的一步不是写正则拦phar://,而是把上传文件的头几个KB做一次Phar特征扫描,同时业务代码把文件路径参数改成白名单ID。只要文件系统函数没法被用户输入直接控制,Phar反序列化这个“隐蔽按钮”就按不下去。filter链和文件包含也同理——它们看起来是三个洞,本质上是一个问题:信任了不该信任的路径字符串。理解这一点,比背任何规则库都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询