BUUCTF:[XDCTF 2015]filemanager
题目地址:
https://buuoj.cn/challenges#[XDCTF%202015]filemanager
题目首页给出了源码:
https://github.com/CTFTraining/xdctf_2015_filemanager
而在这道题中也存在源码泄露:
这道题主要还是考察代码审计和二次注入:
网站结构源码解析:
数据库结构:
xdctf.sql
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;
DROP DATABASE IF EXISTS `xdctf`;
CREATE DATABASE xdctf; #创建xdctf数据库
USE xdctf;
DROP TABLE IF EXISTS `file`;
#创建file表
CREATE TABLE `file` (
`fid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`filename` varchar(256) NOT NULL,
`oldname` varchar(256) DEFAULT NULL,
`view` int(11) DEFAULT NULL,
`extension` varchar(32) DEFAULT NULL, #存储后缀名
PRIMARY KEY (`fid`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
SET FOREIGN_KEY_CHECKS = 1;
文件包含文件, 数据库连接及输入过滤:
common.inc.php:
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午7:58
*/
$DATABASE = array(
"host" => "127.0.0.1",
"username" => "root",
"password" => "root",
"dbname" => "xdctf",
);
$db = new mysqli($DATABASE['host'], $DATABASE['username'], $DATABASE['password'], $DATABASE['dbname']);
$req = array();
foreach (array($_GET, $_POST, $_COOKIE) as $global_var) {
foreach ($global_var as $key => $value) {
is_string($value) && $req[$key] = addslashes($value);
}
}
define("UPLOAD_DIR", "upload/");
function redirect($location) {
header("Location: {$location}");
exit;
}
在common.inc.php中可以看到全局对用户输入的参数进行了转义,在这里进行数据库连接和遍历数组,如用户输入的参数是GET传参,比如:?demo=123;则valu接受的就是123。然后在将其过滤存储到$req这个数组中。这样常规注入少了大部分。遍观代码,输入处没有任何反转义、反解压、数字型等特殊情况,基本可以确定这里不存在直接的注入漏洞。
再看到上传处代码:upload.php
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午8:45
*/
#payload:upload_file_name=',extension='',filename='x.jpg.jpg
require_once "common.inc.php";
if ($_FILES) {
$file = $_FILES["upfile"];
if ($file["error"] == UPLOAD_ERR_OK) {#如果$_FILES["upfile"]["error"]等于UPLOAD_ERR_OK的话,这说明文件上传成功
$name = basename($file["name"]);#返回路径中的文件名部分
$path_parts = pathinfo($name);#以数组的形式返回关于文件路径的信息
#print_r(pathinfo("',extension='',filename='x.jpg.jpg"));
/*Array
(
[dirname] => .
[basename] => ',extension='',filename='x.jpg.jpg
[extension] => jpg
[filename] => ',extension='',filename='x.jpg
)*/
if (!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
exit("error extension");
}
$path_parts["extension"] = "." . $path_parts["extension"];
$name = $path_parts["filename"] . $path_parts["extension"];
// $path_parts["filename"] = $db->quote($path_parts["filename"]);
// Fix
$path_parts['filename'] = addslashes($path_parts['filename']);
$sql = "select * from `file` where `filename`='{$path_parts['filename']}' and `extension`='{$path_parts['extension']}'";
$fetch = $db->query($sql);
if ($fetch->num_rows > 0) {
exit("file is exists");
}
if (move_uploaded_file($file["tmp_name"], UPLOAD_DIR . $name)) {
$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";
#$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '\',extension=\'\',filename=\'x.jpg', 0, '.jpg')";
#单引号被过滤,插入过滤数据,'也插入到数据库,导致后面 '再次插入更新语句执 行,导致二次注入
$re = $db->query($sql);
if (!$re) {
print_r($db->error);
exit;
}
$url = "/" . UPLOAD_DIR . $name;
echo "Your file is upload, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
} else {
exit("upload error");
}
} else {
print_r(error_get_last());
exit;
}
}
这里简单解释下:首先是对文件名路径等信息通过pathinfo函数进行数组形式分割,再对后缀名进行白名单比较,如果不在白名单内则退出:后缀名错误;
接着再进行数据库查询操作,查询数据库中是否有上传的文件名及其对应后缀,如果有则提示文件已存在,在这里是在查询之前进行addslashes进行转义,所以也不存在注入。
接着再将其文件名和后缀名分别插入到数据库中。
再上传功能中对所有输入进行转义并对后缀进行白名单校验,所以此处也不存在注入。
我们在看到重命名功能:rename.php
<?php
/**
* Created by PhpStorm.
* User: phithon
* Date: 15/10/14
* Time: 下午9:39
*/
require_once "common.inc.php";
if (isset($req['oldname']) && isset($req['newname'])) {
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");
if ($result->num_rows > 0) {
$result = $result->fetch_assoc();
} else {
exit("old file doesn't exists!");
}
if ($result) {
$req['newname'] = basename($req['newname']);
$re = $db->query("update `file` set `filename`='{$req['newname']}', `oldname`='{$result['filename']}' where `fid`={$result['fid']}");
#关键点:
#$re = $db->query("update `file` set `filename`='x.jpg', `oldname`=' ',extension='',filename='x.jpg ' where `fid`={$result['fid']}");
#filename字段被更新为x.jpg,存储后缀名字段extension也设置为空。
if (!$re) {
print_r($db->error);
exit;
}
$oldname = UPLOAD_DIR . $result["filename"] . $result["extension"];
$newname = UPLOAD_DIR . $req["newname"] . $result["extension"];
if (file_exists($oldname)) {
rename($oldname, $newname);
}
$url = "/" . $newname;
echo "Your file is rename, url:
<a href=\"{$url}\" target='_blank'>{$url}</a><br/>
<a href=\"/\">go back</a>";
}
}
?>
<!DOCTYPE html>
<html>
<head>
<title>file manage</title>
<base href="/">
<meta charset="utf-8" />
</head>
<h3>Rename</h3>
<body>
<form method="post">
<p>
<span>old filename(exclude extension):</span>
<input type="text" name="oldname">
</p>
<p>
<span>new filename(exclude extension):</span>
<input type="text" name="newname">
</p>
<p>
<input type="submit" value="rename">
</p>
</form>
</body>
</html>
先验证用户是否输入旧文件名(即需要修改的文件名),和输入新文件名。
接着对数据库进行查询,查询在上传功能upload.php中在数据库插入的文件名中是否有用户输入的旧文件名,如果存在,即调用update语句,将查询到存在的文件名更新到数据库oldname字段中,导致文件名$result[‘filename’]再次入库,结果造成二次注入。
在这里验证oldname文件是否存在,是在数据库中去查询filename字段,而不是直接去查询文件系统的文件名验证。而filename字段又是我们可控的。而newname参数又是用户控制输入的,也就是我们也能控制。
但是在upload.php上传文件是对文件后缀进行白名单验证,所以这里不能直接上传恶意文件。
而在rename.php中,重命名的后缀是直接在数据库中提取出上传文件时插入的文件名后缀
而在上传文件时,我们是可以对后缀进行控制的,也就是说,我们可以通过upload.php插入时构造语句,然后在rename.php中update数据库更新进行二次注入将extension字段的值改为空,同时也可以控制filename的值,那么等于说我能控制rename函数的两个参数的值
注意在这里有个坑,这里改名的时候检查了文件是否存在:if(file_exists($oldname)) 我虽然通过注入修改了filename的值,但我upload目录下上传的文件名是没有改的。 因为利用注入时将extension改为空了,那么实际上数据库中的filename总比文件系统中真实的文件名少一个后缀。 那么这里的file_exists就验证不过。这里可以通过再次上传一个新文件,这个文件名就等于数据库里的filename的值就即可绕过。 所以最后整个getshell的流程,实际上是一个二次注入+二次操作getshell。
具体操作:
1.构造文件名,选择文件上传:
’,extension=’’,filename='x.jpg.jpg
(在右边的.jpg作用是为了绕白名单后缀,左边的.jpg是文件名,为了闭合将filename改为x.jpg)
数据库插入(引号也被插入数据库):
2.接着进行重命名操作:
注意这里是填写修改文件名,不包括后缀!
rename.php大概执行过程如下:
数据库:
3.接着在传入一个真正包含webshell的x.jpg
4.重命名getshell
重命名成功,将jpg文件修改为php文件