某CMS 20180827版本 前臺getshell
前言
2018年9月21日,Destoon官方釋出安全更新,修復了由使用者“索馬利亞的海賊”反饋的一個漏洞。
漏洞分析
根據更新訊息可知漏洞發生在頭像上傳處。Destoon中處理頭像上傳的是 module/member/avatar.inc.php 檔案。在會員中心處上傳頭像時抓包,部分內容如下:
對應著avatar.inc.php程式碼如下:
<?php defined('IN_DESTOON') or exit('Access Denied'); login(); require DT_ROOT.'/module/'.$module.'/common.inc.php'; require DT_ROOT.'/include/post.func.php'; $avatar = useravatar($_userid, 'large', 0, 2); switch($action) { case 'upload': if(!$_FILES['file']['size']) { if($DT_PC) dheader('?action=html&reload='.$DT_TIME); exit('{"error":1,"message":"Error FILE"}'); } require DT_ROOT.'/include/upload.class.php'; $ext = file_ext($_FILES['file']['name']); $name = 'avatar'.$_userid.'.'.$ext; $file = DT_ROOT.'/file/temp/'.$name; if(is_file($file)) file_del($file); $upload = new upload($_FILES, 'file/temp/', $name, 'jpg|jpeg|gif|png'); $upload->adduserid = false; if($upload->save()) { ... } else { ... } break;
這裡通過 $_FILES['file']
依次獲取了上傳副檔名 $ext
、儲存臨時檔名 $name
、儲存臨時檔案完整路徑 $file
變數。之後通過 new upload();
創立一個upload物件,等到 $upload->save()
時再將檔案真正寫入。
upload
物件建構函式如下,include/upload.class.php:25:
<?php class upload { function __construct($_file, $savepath, $savename = '', $fileformat = '') { global $DT, $_userid; foreach($_file as $file) { $this->file = $file['tmp_name']; $this->file_name = $file['name']; $this->file_size = $file['size']; $this->file_type = $file['type']; $this->file_error = $file['error']; } $this->userid = $_userid; $this->ext = file_ext($this->file_name); $this->fileformat = $fileformat ? $fileformat : $DT['uploadtype']; $this->maxsize = $DT['uploadsize'] ? $DT['uploadsize']*1024 : 2048*1024; $this->savepath = $savepath; $this->savename = $savename; } }
這裡通過 foreach($_file as $file)
來遍歷初始化各項引數。而 savepath
、 savename
則是通過 __construct($_file, $savepath, $savename = '', $fileformat = '')
直接傳入引數指定。
因此考慮上傳了兩個檔案,第一個檔名是 1.php
,第二個檔案是 1.jpg
,只要構造合理的表單上傳(參考: ofollow,noindex">https://www.cnblogs.com/DeanChopper/p/4673577.html),則在avatar.inc.php中
$ext = file_ext($_FILES['file']['name']); // `$ext`即為`php` $name = 'avatar'.$_userid.'.'.$ext; // $name 為 'avatar'.$_userid.'.'php' $file = DT_ROOT.'/file/temp/'.$name; // $file 即為 xx/xx/xx/xx.php
而在 upload
類中,由於多個檔案上傳, $this->file
、 $this->file_name
、 $this->file_type
將foreach在第二次迴圈中被置為jpg檔案。測試如下:
回到 avatar.inc.php
,當進行檔案儲存時呼叫 $upload->save()
,include/upload.class.php:50:
<?php class upload { function save() { include load('include.lang'); if($this->file_error) return $this->_('Error(21)'.$L['upload_failed'].' ('.$L['upload_error_'.$this->file_error].')'); if($this->maxsize > 0 && $this->file_size > $this->maxsize) return $this->_('Error(22)'.$L['upload_size_limit'].' ('.intval($this->maxsize/1024).'Kb)'); if(!$this->is_allow()) return $this->_('Error(23)'.$L['upload_not_allow']); $this->set_savepath($this->savepath); $this->set_savename($this->savename); if(!is_writable(DT_ROOT.'/'.$this->savepath)) return $this->_('Error(24)'.$L['upload_unwritable']); if(!is_uploaded_file($this->file)) return $this->_('Error(25)'.$L['upload_failed']); if(!move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)) return $this->_('Error(26)'.$L['upload_failed']); $this->image = $this->is_image(); if(DT_CHMOD) @chmod(DT_ROOT.'/'.$this->saveto, DT_CHMOD); return true; } }
先經過幾個基本引數的檢查,然後呼叫 $this->is_allow()
來進行安全檢查 include/upload.class.php:72:
<?php function is_allow() { if(!$this->fileformat) return false; if(!preg_match("/^(".$this->fileformat.")$/i", $this->ext)) return false; if(preg_match("/^(php|phtml|php3|php4|jsp|exe|dll|cer|shtml|shtm|asp|asa|aspx|asax|ashx|cgi|fcgi|pl)$/i", $this->ext)) return false; return true; }
可以看到這裡僅僅對 $this->ext
進行了檢查,如前此時 $this->ext
為 jpg
,檢查通過。
接著會進行真正的儲存。通過 $this->set_savepath($this->savepath); $this->set_savename($this->savename);
設定了 $this->saveto
,然後通過 move_uploaded_file($this->file, DT_ROOT.'/'.$this->saveto)
將 file
儲存到 $this->saveto
,注意此時的 savepath
、 savename
、 saveto
均以php為字尾,而 $this->file
實際指的是第二個jpg檔案。
漏洞利用
綜上,上傳兩個檔案,其中第一個檔案以php為結尾如 1.php
,用於設定字尾名為 php
;第二個檔案為 1.jpg
,jpg用於繞過檢測,其內容為php一句話木馬(圖片馬)。
然後訪問 http://127.0.0.1/file/temp/avatar1.php 即可。其中 1
是自己的 _userid
不過實際利用上會有一定的限制。
第一點是destoon使用了偽靜態規則,限制了file目錄下php檔案的執行。
第二點是avatar.inc.php中在 $upload->save()
後,會再次對檔案進行檢查,然後重新命名為 xx.jpg
:
省略... $img = array(); $img[1] = $dir.'.jpg'; $img[2] = $dir.'x48.jpg'; $img[3] = $dir.'x20.jpg'; $md5 = md5($_username); $dir = DT_ROOT.'/file/avatar/'.substr($md5, 0, 2).'/'.substr($md5, 2, 2).'/_'.$_username; $img[4] = $dir.'.jpg'; $img[5] = $dir.'x48.jpg'; $img[6] = $dir.'x20.jpg'; file_copy($file, $img[1]); file_copy($file, $img[4]); 省略...
因此要利用成功就需要條件競爭了。
補丁分析
在upload的一開始,就進行一次後綴名的檢查。其中is_image如下:
function is_image($file) { return preg_match("/^(jpg|jpeg|gif|png|bmp)$/i", file_ext($file)); }
在 __construct()
的foreach中使用了break,獲取了第一個檔案後就跳出迴圈。
在 is_allow()
中增加對 $this->savename
的二次檢查。
最後
嘛,祝各位大師傅中秋快樂!