本文來自i春秋作者:索馬里的海賊

前言

看了版主 jing0102 的{代碼審計思路 (通讀+審計) Mlecms(中危漏洞/不簡單), 感覺挺有意思 于是也回去下了一套代碼看看 不得不說小眾CMS的開發能力、安全意識跟大廠商還是有不少差距的 限于篇幅 不是關鍵部分就不貼代碼了

一、發現隱患

拿到一套源碼 首先得找到下手的地方,不管是不是新手 我都建議從index.php開始。 index.php做為固定主頁面,里面肯定包含了整套系統的配置讀取,初始化等內容,跟隨這些包含的內容或者文件 就能大致了解整套系統的處理框架 流程等信息 這些信息在審計中都是非常重要的。經常有剛入門的審計同學不知道怎么才能加載某個文件,或者明明發現某個地方存在問題,卻不知道如何訪問去觸發,這都是對流程不熟悉的結果。

感覺說的廢話都能出一本書了,接下來直接來看看今天的主角mlecms index.php 中包含了一個 inc/include/ 目錄下的header.php 而 header.php 又包含了 common.inc.php common.inc.php 又包含了 globals.php .

這3個文件都是用來初始化站點的數據,在看到globals.php的時候 發現有這么一段

foreach(array('_GET','_POST','_COOKIE') as $_request){
????????foreach($$_request as $i => &$n){
????????????????${$i} = daddslashes($n);
????????}
}

接觸過代碼審計的人應該很熟悉,這是一段偽全局的代碼。很多流行cms都會用,也出過不少問題,dz dedecms都在偽全局上吃過苦頭。

這里并沒有對變量名進行判斷就直接用雙GBLOBAS超全局變量導致的getshell么

好在這套系統并沒有用$GBLOABS來做什么文章,而且變量的值都經過了daddslashes做了轉義。那如果是$_FILES呢~

二、從隱患開始

正常的流程中$_FILES變量是當產生用戶上傳動作時一個系統初始化的數組

$_FILES['userfile']['name'] //客戶端機器文件的原名稱。
$_FILES['userfile']['type'] //文件的 MIME 類型,如果瀏覽器提供此信息的話。一個例子是“image/gif”。不過此 MIME 類型在 PHP 端并不檢查,因此不要想當然認為有這個值。
$_FILES['userfile']['size'] //已上傳文件的大小,單位為字節。
$_FILES['userfile']['tmp_name'] //文件被上傳后在服務端儲存的臨時文件名。
$_FILES['userfile']['error'] //和該文件上傳相關的錯誤代碼。此項目是在 PHP 4.2.0 版本中增加的。

當正常上傳的時候 數組中的tmp_name值是不可控的,但因為上面的提到隱患,$_FILES變量可以通過GPC提交來覆蓋了。 當然$_FILES可控在嚴謹的上傳流程里也不一定能造成很大的危害 我們就來看看這套系統的上傳流程 搜索$_FILES 有4個文件使用了這個變量 去掉后臺功能和ckeditor 找到了 inc/class/avatar.class.php 看名字應該是跟頭像上傳有關,來看看具體內容 inc/class/avatar.class.php 行34

//這里的注釋等下回來看
public function onuploadavatar() {
????????????????@header("Expires: 0");
????????????????@header("Cache-Control: private, post-check=0, pre-check=0, max-age=0", FALSE);
????????????????@header("Pragma: no-cache");
????????????????$this->init_input($_GET['agent']);
????????????????$uid = $this->input['uid'];? //uid來自input數組 input數組來自init_input()函數
????????????????if(empty($uid)) {
????????????????????????return -1;
????????????????}
????????????????if(empty($_FILES['Filedata'])) {
????????????????????????return -3;
????????????????}
????????????????list($width, $height, $type, $attr) = getimagesize($_FILES['Filedata']['tmp_name']); //這里調用getimagesize函數來檢查文件內容
????????????????$imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
????????????????$filetype = $imgtype[$type]; //限定了文件后綴來自$imgtype數組
????????????????$tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //臨時保存文件名 [固定]+uid+文件后綴 
????????????????file_exists($tmpavatar) && @unlink($tmpavatar); // 如果已經存在 先刪除
????????????????if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) { 
????????????????????????@unlink($_FILES['Filedata']['tmp_name']); //如果移動成功 就刪了原文件
????????????????????????list($width, $height, $type, $attr) = getimagesize($tmpavatar); //再次調用getimagesize函數檢查移動后的文件
????????????????????????if($width < 10 || $height < 10 || $type == 4) { //如果長度寬度不符合要求 或者$type=4(我記得是swf文件好像)? 就刪掉目標文件
????????????????????????????????@unlink($tmpavatar);
????????????????????????????????return -2;
????????????????????????}
????????????????} else {
????????????????????????@unlink($_FILES['Filedata']['tmp_name']); //移動失敗? 也刪掉原文件
????????????????????????return -4;
????????????????}
????????????????global $config;
????????????????$avatarurl = $config['url'].'inc/tmp/other/member_'.$uid.$filetype; 
????????????????return $avatarurl;
????????}

看到了這句

if(@copy($_FILES['Filedata']['tmp_name'], $tmpavatar) || @move_uploaded_file($_FILES['Filedata']['tmp_name'], $tmpavatar)) {

move_upload_file函數會在移動文件之前檢查文件是否為合法的上傳臨時文件,如果想搞事,偽造的tmp_name是不會通過函數檢查的

但copy就不一樣了 不管你來源 不管你目的 直接給你懟過去。 再看看上面這句 兩個函數都嘗試了 所以如果我們偽造$_FILES['Filedata']['tmp_name']=/etc/passwd就是一個妥妥的任意文件讀取了 更進一步 如果偽造$_FILES['Filedata']['tmp_name']=http://xxx.com/shell.txt 是不是就能getshell了呢

來看看程序的流程(減少篇幅 回去看上面代碼段的注釋) 可以看到 不管哪個流程 最終都會刪掉原文件(也就是我們偽造的$_FILES['Filedata']['tmp_name'])你應該不想讀了個數據庫配置信息導致整個網站癱瘓吧。而且這里保存的文件后綴來自數組 并不能隨意偽造來getshell。

到這里真的沒辦法了嗎?

三、真的沒辦法了嗎?

真的沒辦法了嗎?當然不,代碼審計就是要有死磕的精神,你攔我繞,你堵我懟。來看看怎么懟。

先總結一下目前的狀況 由于偽全局未過濾,導致$_FILES變量覆蓋 又由于使用了copy函數來進行文件移動 不會檢查文件是否合法 可能導致任意文件讀取和getshell的問題 問題是 不管走哪個流程 都會unlink原文件 讀取關鍵配置會導致關鍵配置文件被刪 站點直接癱瘓

一個一個來解決

關于文件讀取 有沒有辦法 能讓copy(src,dst)成功 而unlink(src)失敗呢

答案是有的 就是神奇的php://filter 這里限于篇幅 不再細說這個schema 百度一下有幾位前輩早已寫過有關的文章

利用php://filter/resource=路徑/文件名 就可以達到我們想要的效果 copy成功 unlink失敗,雖然copy成功之后 第二個getimagesize檢查后面的unlink沒辦法bypass,不過已經生成了,那我讀不讀就由不得你了。時間競爭大家應該不陌生, 我趕在你生成和刪除中間的一瞬間讀到不就行了,時間競爭的關鍵一點就是,目標要明確,如果我不知道你文件名 胡亂去猜的話 這個時間間隔肯定是不夠的,但是文件名我們是已知的( [固定]+uid+文件后綴),所以多試幾次 肯定能成功。

好了 現在任意文件讀取這個漏洞已經拿下了,那貪心一點 能getshell么?

答案當然也是能。

來看看保存的文件名格式。

 //這里調用getimagesize函數來檢查文件內容
????????????????$imgtype = array(1 => '.gif', 2 => '.jpg', 3 => '.png');
????????????????$filetype = $imgtype[$type]; //限定了文件后綴來自$imgtype數組
????????????????$tmpavatar = MLEINC.'/tmp/other/member_'.$uid.$filetype; //臨時保存文件名 [固定]+uid+文件后綴[/size][/size][/color][/size][/font][/size][/font][/color]

為了getshell 這里需要讓getimagesize()失敗 這樣$type就不會被初始化

$filetype=$imgtype[$type];

文件后綴就變成null了 接下來如果能控制uid來自

$this->init_input($_GET['agent']);
$uid = $this->input['uid'];? //uid來自input數組 input數組來自init_input()函數

看看init_input函數

public function init_input($getagent = '') {
????????????????$input = $_GET['input'];
????????????????if($input) {
????????????????????????$input = encryption($input,'DECODE',WEBKEY); 
????????????????????????parse_str($input,$this->input); 
????????????????????????$agent = $getagent ? $getagent : $this->input['agent'];
????????????????????????if(($getagent && $getagent != $this->input['agent']) || (!$getagent && md5($_SERVER['HTTP_USER_AGENT']) != $agent)) {
????????????????????????????????exit('Access denied for agent changed');
????????????????????????} elseif($this->time - $this->input['time'] > 3600) {
????????????????????????????????exit('Authorization has expired');
????????????????????????}
????????????????}
????????????????if(empty($this->input)) {
????????????????????????exit('Invalid input');
????????????????}
????????}

$_GET['input'];中解密并用parse_str賦值給了$this->input數組 這里用的加解密函數encryption()其實就是dz的 authcode 函數,還是比較安全的。密鑰WEBKEY來自inc/config/version.config.php 如果我們能知道密鑰WEBKEY 就能偽造uid=.php的input值來getshell

怎么得到這個WEBKEY值呢,別忘了上面的任意文件讀取哦~

四、利用

畢竟還是個0day 危害也比較大 這里就不公開具體的getshell代碼了 主要是分享一個從拿到cms開始發現安全隱患 到如何利用安全隱患 再遇到困難 解決困難最終成功利用的過程。

總結

寫文章比看代碼累多了。。。 代碼審計 靠的其實就是對編程語言的理解。怎么去快速發現問題,怎么去繞坑,都需要不斷的積累。 最后祝大家0day多多

原文地址:http://bbs.ichunqiu.com/thread-13703-1-1.html?from=seebug


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/79/