1. 程式人生 > 實用技巧 >攻防世界-web-filemanager(原始碼洩漏、二次注入)

攻防世界-web-filemanager(原始碼洩漏、二次注入)

題目來源:XDCTF 2015
題目描述:暫無

進入介面

這題看似檔案上傳,其實主要是利用sql注入修改指定檔案在資料庫中的字尾名為空。

首先,/www.tar.gz 下載原始碼

原始碼審計

資料庫的欄位結構為

SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

DROP DATABASE IF EXISTS `xdctf`;
CREATE DATABASE xdctf;
USE xdctf;

DROP TABLE IF EXISTS `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

  • 對傳入的引數進行了addslashes()轉義
  • 資料庫連線和遍歷陣列
  • 基本沒有直接的注入漏洞
<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午7:58
 */

$DATABASE = array(

    "host" => "127.0.0.1",
    "username" => "root",
    "password" => "ayshbdfuybwayfgby",
    "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

upload

  • 白名單限制了字尾名
  • 查詢檔名是否存在,進行了addslashes()轉義
  • 也不存在直接注入漏洞
<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午8:45
 */

require_once "common.inc.php";

if ($_FILES) {
    $file = $_FILES["upfile"]; //PHP $_FILES 是一個預定義的陣列,用來獲取通過 POST 方法上傳檔案的相關資訊(比如name、type、tmp_name、error、size等)。如果為單個檔案上傳,那麼 $_FILES 為二維陣列;如果為多個檔案上傳,那麼 $_FILES 為三維陣列。
    if ($file["error"] == UPLOAD_ERR_OK) { //PHP檔案上傳完畢之後,返回了UPLOAD_ERR_OK錯誤程式碼,這表示檔案上傳成功
        $name = basename($file["name"]); //basename() 函式返回路徑中的檔名部分。
        $path_parts = pathinfo($name); //pathinfo() 返回一個關聯陣列包含有 path 的資訊。包括以下的陣列元素:[dirname]: 目錄路徑、[basename]: 檔名、[extension]: 檔案字尾名、[filename]: 不包含字尾的檔名

        //上傳副檔名的白名單
        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']); //檔名轉義

        //根據上傳檔案的檔名和副檔名判斷資料庫中該檔案是否exist
        $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");
        }

        //move_uploaded_file() 函式將上傳的檔案移動到新位置。若成功,則返回 true,否則返回 false。這裡將上傳的檔案移動到upload/下
        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']}')";
            $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;
    }
}
upload.php

delete

  • 就是刪除,沒什麼好講
<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午9:39
 */

require_once "common.inc.php";

if(isset($req['filename'])) {
    $result = $db->query("select * from `file` where `filename`='{$req['filename']}'");
    if ($result->num_rows>0){
        $result = $result->fetch_assoc();
    }

    $filename = UPLOAD_DIR . $result["filename"] . $result["extension"];
    if ($result && file_exists($filename)) {
        $db->query('delete from `file` where `fid`=' . $result["fid"]);
        unlink($filename);
        redirect("/");
    }
}
?>
<!DOCTYPE html>
<html>
<head>
    <title>file manage</title>
    <base href="/">
    <meta charset="utf-8" />
</head>
<h3>Delete file</h3>
<body>
    <form method="post">
        <p>
            <span>delete filename(exclude extension):</span>
            <input type="text" name="filename">
        </p>
        <p>
            <input type="submit" value="delete">
        </p>
    </form>
</body>
</html>
delete.php

rename

  • filename=$req['oldname']是從資料庫查詢輸入的oldname是否在於filename欄位,然後進行update修改
  • oldname={$result['filename']}將之前從資料庫中查詢出的filename更新到oldname當中,再次入庫造成二次注入
  • 可以通過sql注入,影響其extension為空,再修改檔案時加上.php字尾
  • 繞過file_exists()只需要再次上傳一個與資料庫當中filename的值相同的檔名即可
<?php
/**
 * Created by PhpStorm.
 * User: phithon
 * Date: 15/10/14
 * Time: 下午9:39
 */

require_once "common.inc.php";

//isset函式是檢測變數是否設定
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) {

        //basename() 函式返回路徑中的包含字尾的檔名部分。
        $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->error);
            exit;
        }
        //更新伺服器上的舊檔名為:資料庫中的新檔名(包含字尾)+資料庫中的舊副檔名
        //更新伺服器上的新檔名為:客戶端輸入的新檔名(包含字尾)+資料庫中的舊副檔名
        //因此,在這裡,我們可以先把資料庫中的舊副檔名置為空,然後rename時,把新檔名設定為test.php
        $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>

最重要的就是標紅的這5行。 Oldname和newname,有幾個特點:

字尾相同,都是$result['extension'] oldname的檔名來自資料庫,newname的檔名來自使用者輸入 首先後綴相同這個特點,就導致getshell似乎難以完成,如果要getshell那麼一定要將“非.php”字尾的檔案重新命名成“.php”的檔案。字尾相同怎麼重新命名? 除非字尾為空! 所以我們的update型注入就開始派上用場了。通過update型注入,我們可以將資料庫中extension欄位的值改為空,同時也可以控制filename的值,那麼等於說我能控制rename函式的兩個引數的值,這樣getshell就近在咫尺了。

但還有個坑,這裡改名的時候檢查了檔案是否存在:if(file_exists($oldname))我雖然通過注入修改了filename的值,但我upload目錄下上傳的檔名是沒有改的。 因為我利用注入將extension改為空了,那麼實際上資料庫中的filename總比檔案系統中真是的檔名少一個字尾。 那麼這裡的file_exists就驗證不過。怎麼辦? 簡單啊,再次上傳一個新檔案,這個檔名就等於資料庫裡的filename的值就好了。

所以最後整個getshell的流程,實際上是一個二次注入 + 二次操作getshell。

解題過程

根據原始碼審計結果,我們知道,程式碼中由於白名單限制,所以無法上傳惡意檔案,由於版本限制也不能用%00截斷,但有一個rename功能,只能修改檔名,但可以通過sql注入,影響其extension為空,再修改檔案時加上.php字尾。

先上傳一個用來sql注入的空檔案,檔名為 ',extension='.txt

修改檔名

新的檔名為test.txt.txt,但是資料庫中經過update語句

update `file` set `filename`='test.txt', `oldname`='',extension='' where `fid`={$result['fid']}

實際上,新檔案的filename為test.txt,extension為空

再上傳一個和上面newname檔名相同的木馬檔案

rename

下面直接菜刀連線即可。

參考:

https://blog.csdn.net/weixin_44604541/article/details/108917121

https://blog.csdn.net/weixin_43610673/article/details/107503183

攻防世界官方writeup