在drupal框架中,比較經典又離我們最近的莫過於18年的CVE-2018-7600這個漏洞了。但透過本人閱讀和學習此漏洞分析文章的過程中,發現都是針對於此漏洞點的詳細分析。相對於此框架運行流程不是很熟悉的人可能在閱讀完後很難理解。
下面主要分為兩大部分:
第一部分是drupal框架流程的簡介(這裡主要針對8.x系列),讓我們知道在symfony開源框架基礎上的drupal框架是如何利用監聽者模式來支撐起整個繁雜的處理流程,並讓我們對框架如何處理一個請求有基本的了解。
第二部分,結合框架對漏洞CVE-2018-7600的運行流程進行詳細解讀,在漏洞觸發的起始點首先透過動態調試正常資料包來了解drupal框架對其的處理流程,借此利用正常包中的可控變數來建構POC包。讓我們不僅能對開頭和結果得以了解,更能讓中間的流程透明化。得以觸類旁通。
Drupal是使用PHP語言編寫的開源內容管理框架(CMF),它由內容管理系統(CMS)和PHP開發框架(Framework)共同構成。連續多年榮獲全球最佳CMS大獎,是基於PHP語言最著名的WEB應用程式。
Drupal架構由三大部分組成:核心、模組、主題。三者透過Hook機制緊密的連結起來。其中,核心部分由世界上多位著名的WEB開發專家組成的團隊負責開發與維護。
Drupal整合了強大且可自由設定的功能,可支援從個人部落格(PersonalWeblog)到大型社群驅動(Community-Driven)的網站等各種不同應用程式的網站專案。 Drupal最初是由DriesBuytaert所開發的一套社群討論軟體。之後,由於它的靈活的架構,方便的擴展等特性,使得世界上成千上萬個程式設計師加入了Drupal的開發與應用中。今天,它已經發展成為一套強大的系統,許多大型機構都採用基於Drupal的框架建站,包括The Onion,Ain't ItCool News,SpreadFirefox,Ourmedia,KernelTrap,NewsBusters等等。它特別常見於社區主導的網站。
首先可直接透過官網下載頁面https://www.drupal.org/download 直接下載最新版本或透過https ://www.drupal.org/project/drupal/releases/xxx xxx代表你想下載的版本號,來下載對應版本的原始碼檔案。 你也可以用PHP套件管理工具composer進行下載。
2.2 drupal安裝
安裝環境:WIN7 32位元
整合環境:PHPSTUDY
# 可能出現的問題與解決方法: 1. php版本問題:最好為PHP7.0以上 2. datetime問題#解決方法:
php.ini 中設定
#3.安裝警告
這兩個問題(warning)可以不解決。
針對問題1解決方法:升級php版本為7.1以上。
針對問題2解決方法:
在php.ini中,找到[opcache],在這個地下加入如下。
zend_extension="C:\xxx\xxx\php\php-7.0.12-nts\ext\php_opcache.dll"opcache.memory_consumption =128opcache.interned_strings_buffer=8opcache.max_accelerated_files=4000
##opcache.revalidate_freq=60opcache。 ##opcache.enable_cli=1 4. 因為drupal處理有些請求過慢,有可能會導致逾時出現異常,在Php.ini中max_execution_time選項設定大點即可。 三、框架淺析######3.1目錄結構######下面是drupal 8.5.7 原始碼解壓縮後的目錄:############ ###/core drupal的核心資料夾,詳見後文說明######/modules 裡存放自訂或下載的模組######/profiles 裡存放下載和安裝的自訂設定檔###/sites 資料夾,在drupal 7 或更早的版本中,主要存放網站使用的主題和模組活其他網站檔案。
/themses 裡存放自訂或下載的主題
/vendor 裡存放程式碼的依賴函式庫
接下來我們來看核心資料夾core下的目錄結構
/core/assets - drupal 所使用的各種擴充庫,如jquery,ckeditor,backbone,normalizeCSS等
/core/config - drupal 中的核心設定檔
/core/includes – 模組化的底層功能函數,如模組化系統本身
/core/lib – drupal提供的原始核心類別
# /core/misc – 核心所需的前端雜項文件,如JS,CSS,圖片等。
/core/modules – 核心模組,大約80項左右
/core/profiles – 內建安裝設定檔
/core/scripts – 開發人員使用的各種命裡腳本
/tests – 測試相關用的檔案
/core/themes – 核心主題
Drupal是建立在symfony開源框架之上的,在symfony的官網上可知sysmfony就是一個可復用的php組件集,可以將任何一個組件獨立的運用到自己的應用程序中來,在symfony官網裡每一個組件都有獨立的文檔,這些元件有些被drupal直接使用,有些根據drupal自己的特性進行了修改。
我們先來看看symfony的執行流程
Drupal 與symfony 在設計上也使用了相同的理念,它們都認為任何一個網站系統其實就是一個把請求轉換成回應的系統。
在drupal的路由系統中,我們可以看到各個元件之間的關係:
在此基礎上,drupal對symfony的處理流程進行了細化,構成了現在這個龐大的drupal處理回應流程。
圖片連結網址為https://www.drupal.org/docs/8/api/render-api/the-drupal-8-render-pipeline 如需可自行下載高畫質版。
# 入口檔案非常簡潔,只有6行程式碼量,卻貫穿了整個drupal,由於drupal的核心系統過於龐大,分析不可能面面俱到,我們將從入口檔案一行行來看,分析下它的運作流程。
首先是$autoloader =require_once 'autoload.php'; 表面上看單單的是包含了一個autoload.php的文件,實際上drupal會利用PHP自動加載機制創建一個自動加載器,並獲取了一個自動載入的物件。
下面從程式碼方面簡略看下其流程:其根本是呼叫vendor/autoload.php 中的getLoader函數。
接著我們進入函數看看它做了什麼:
ClassLoader物件就是利用裡面定義的基本對應關係去找函數和類別定義檔。
函數最後回傳實例化載入器,至此第一步完成,drupal以後就不需要手動的 include一大堆檔案了,省去了大量工作。
接著是 $kernel =new DrupalKernel('prod', $autoloader); drupal創建了一個新的drupal內核對象,為處理即將到來的請求對像做準備。
緊接著是入口檔案中的$request= Request::createFromGlobals()這一行程式碼。對於一個物件導向的系統來說,我們不應該直接使用$_POST,$_GET,$_COOKIE等這些全域變數。 Drupal把它們全部封裝進了$request物件。這樣不僅簡單方便,而且使用請求的物件可直接加入一些額外的功能和自訂的屬性。
最終,會把對應的全域變數加到request物件中,並回傳封裝好的request物件。
如果說上面的操作只是預備階段,那麼接下來$response = $kernel->handle($request);這行程式碼將開始步入正題,由drupal核心物件kernel來處理request請求。
Drupal的處理核心是利用了設計模式裡面的監聽者模式。其中包括一個事件來源,裡麵包含了不同的事件以及事件等級。另一部分就是需要執行事件的程式或函數,我們叫它監聽者。在請求處理的這個流程中,每到一個節點,會派發出對應的事件,監聽者會根據所取得的事件物件和等級來進行對應的操作。
其中系統核心事件或繼續沿用symfony框架中的事件,位於kernelevents.php中,其中包含八大核心:
Const REQUEST = 'kernel.request' 執行框架程式碼中的任何代碼之前,請求分派的開始觸發的。
Const EXCEPTION = ‘kernel.exception’ 出現未捕獲的例外時觸發的事件。
Const VIEW = ‘kernel.view’ 當控制器回傳值不是response 實例時觸發。此時控制器回傳的是渲染數組,來進一步進行渲染工作。
Const CONTOLLER = ‘kernel.controller’ 解析request請求找到相對應的控制器時觸發,並且可以對此控制器進行修改。
Const CONTROLLER_ARGUMENTS =‘kernel.controller_arguments’ 解析控制器的參數時觸發,並可對參數進行變更。
Const RESPONSE = ‘kernel.response’ 建立回應回覆要求時觸發,並可修改或取代要回覆的對應。
Const TERMINATE = ‘kernel.terminate’ 一旦發送回應,就會觸發。這個事件會允許處理繁重的post-response任務。
Const FINISH_REQUEST = ‘kernel.finish_request’完成Request請求時觸發,可在請求期間變更應用程式時重設應用程式的全域和環境狀態。
除了這些核心的事件,drupal中的每位監聽者也會派遣它們自己的事件。這些檔案的位置位於\core\lib\Drupal\Core\目錄下相對應資料夾中。它們都是以events.php結尾,文件中定義了對應的靜態事件變數。
我們接下來看下drupal 核心的請求流程:
開始請求request---》解析請求得到控制器並修正------》解析控制器參數-- --》根據控制器呼叫其中的方法-----》觀察控制器的回傳情況:回傳回應物件reponse或繼續進行渲染------》傳送回應。如果整個流程中途產生異常,會直接觸發異常事件進行異常的分發。請求對像在整個流程中除了會對核心請求事件的回應,還會根據實際情況進入回應其他普通模組事件的分支,但是不管中途的過程如何崎嶇坎坷,最終都會重新回歸主流程回傳回應物件response。
接下來從原始碼中觀察下上述具體行為:
從index.php中繼續跟進便進入了drupalkernel.php文件,我們來看看做了那些操作。
接下來就是一系列的處理函數函數呼叫鏈,我們一直跟進handle函數即可,這樣我們直接可跟進核心函數handleraw
這裡我們繼續跟進即將回傳的filterResponse函數。
這裡的回應物件將一層一層的回傳(需要注意的是不是所有的回應結果都會走這個流程),但最終都會封裝成respone回應對象,回到index.php檔案中的$response變數中。然後呼叫$response->send()發送封裝好的回應物件。
有時我們發送的請求操作的內容會過於繁瑣,所以當上面的呼叫結束後,我們的drupal核心在關閉前會做最後的處理。流程進入Index.php檔案的最後一行,呼叫$kernel->terminate($request,$response),我們根據呼叫鏈跟進stackedhttpkernel.php檔案
#至此,整個週期已經結束。
我們發現上面整個過程中出現最多的就是派發事件這個操作了,其實所有派發進行的流程是相同的,派發的具體過程在ContainerAwareEventDispatcher.php檔案中,我們拿kernel.request事件來進行舉例說明。
系統中監聽者總數有19個之多,每個監聽者其中又會有與之相關的服務名,我們會根據傳入的事件名稱匹配對應的監聽者,接著遍歷挨個呼叫其中的服務名所對應的功能函數。我們這裡是kernel.request事件,呼叫方式為回呼呼叫。
透過第三部分單純的框架分析可能只對流程有一個模糊的概念,接下來我們結合漏洞實例,針對比較經典的drupal框架漏洞cve-2018-7600來仔細觀察下此漏洞在框架中的詳細運作流程。我們這裡利用漏洞觸發環境的版本為8.5.0,此版本漏洞觸發更為直觀,所以我們後面分析所用程式碼版本如不做說明皆為此版本。
4.1 修補程式比較
因為此漏洞在8.5.1版本中修復,5.0 和5.1又只相差一個子版本,我們可以更清楚的在原始碼中對比出其中的差異。看看官方是如何修復這個漏洞的:在8.5.1版本的源碼中,新增了一個RequestSanitizer.php文件,裡面是對request請求部分進行過濾,在stripDangerousValues方法中過濾了以#開頭且不再白名單裡的所有鍵名的值。
在prehandle方法中呼叫了上述檔案新增的方法進行過濾,下圖右邊紅色部分為8.5.1新增的過濾程式碼。
此處過濾程式碼的呼叫位置是在drupal核心處理請求之前。這樣可以一勞永逸,徹底修復了這個漏洞。
接著我們進入drupal官網查看官方文件發現了drupal render api對#開頭有特殊處理,關鍵文件連結在下方
https ://www.drupal.org/docs/8/api/render-api/render-arrays並根據checkpoint安全團隊發布了一份關於此漏洞相關技術細節報告。連結如下:https://research.checkpoint.com/uncovering-drupalgeddon-2/。發現漏洞觸發的來源是8.5.0版本中註冊用戶功能中的頭像上傳功能。
4.2資料包在框架中的運作流程
我們既然知道了漏洞的觸發源頭,那麼先隨便上傳一張圖片,抓一個正常的初始包看看情況。
接著在入口檔案index.php中,經過createfromglobals函數的包裝,drupal把我們傳入的參數全部封裝進了request物件中。
4.2.1KernelEvents::REQUEST發放事件
由於上文中對框架流程做了介紹,以下是drupal核心處理我們的request請求階段了,我們這裡直接把斷點下在handleRaw上,並進入第一個KernelEvents::REQUEST派發事件,看看監聽者們都對此次請求做了什麼。
首先drupal嘗試處理Option請求,可惜我們這裡是POST請求,所以不處理,直接放行。
接著會去處理URL路徑上的斜線問題,會把多個斜線開頭的路徑轉換成單一斜線
然後會根據請求驗證身份,我們這裡沒有做登陸,是遊客身份,所以這裡也不做特殊處理。
接下來會清理含有$ _GET['destination']和$ _REQUEST ['destination']目標參數,防止重定向攻擊。
緊接著會依據POST請求中的_drupal_ajax參數來判斷此請求是否為AJAX請求,並設定相關屬性。
接下來是根據請求中的URL部分來匹配對應的路由,這裡drupal會先在路由快取中查找對應的匹配項,如果沒有則再進行全部的路由查表操作。 (由於程式碼比較多,這裡不做全部截取,只截取部分程式碼),處理函數在onKernelRequest 中,同時,我們也可以在user.routing.yml檔案中找到相關資訊。
路由找到了,接下來就是去檢查此路由是否可用
#緊接著就是檢查網站是否處於維護模式,如果是維護模式則退出帳戶,檢查站點是否脫機,檢查動態頁面緩存,預先處理非路由設置,根據參數看是否禁用副本伺服器。這些操作的相關函數,均截圖在下方。
至此,KernelEvents::REQUEST 的所有監聽者的行為分析完畢,我們可以看到上面這些操作主要做的是一些額外的措施,我們可以忽略不看,但是從中我們也提煉出了一些有價值的信息,通過請求對象匹配到了相關的路由信息。
4.2.2KernelEvents::CONTROLLER與KernelEvents::CONTROLLER_ARGUMENTS事件
接下來在handleraw函數中,drupal透過剛剛匹配到的路由資訊來找到真正的請求控制器和對應的參數。
我們先來看看KernelEvents::CONTROLLER的監聽者們會做那些操作。
首先,為了以後不做衝突,在對應的管理器上設定了關鍵的KEY
緊接著為了確保後面處理資料時的完整性,這裡利用閉包把回呼處理控制器的函數存進$event對象
因為KernelEvents::CONTROLLER_ARGUMENTS並沒有屬於它自己的監聽者,所以這裡派發直接放行。
4.2.3 呼叫控制器
在handleRaw中處理完了請求相關的事件派發,並從request中找到了對應的控制器後,就該根據控制器找到對應的處理函數了。下方call_user_function中的控制器已經被替換為上圖中閉包回呼函數了,這裡的呼叫控制器相當於直接進入上圖中的閉包函數中。
在drupal中,控制器都會加入渲染上下文,以確保每個控制器處理過程中如果有需要渲染的地方直接進行渲染操作。
根據控制器進入到了真正的呼叫方法,也就是getContenResult中,表單的建構正式開始。
4.2.4 表單建置
進入buildForm 函數後,我們先得到POST的資訊並存入form_state。
在buildForm函數的retrieveForm函數中,form表單開始初步組裝,如果其中有元素需要渲染,drupal大部分會直接利用\Drupal::service('renderer ')->renderPlain();這個渲染服務對元素進行渲染操作,最終渲染函數的主要操作在doRender函數中。
根據rquest組裝的form表單在組裝完成之後,馬上就要處理表單請求了,這裡processForm這個函數進行了這個操作,在這個函數中,運用遞歸的操作來處理行為,我們是一個圖片上傳操作,在這其中也會對此行為進行處理,處理完畢後會進行圖片的移動。接著對每個元素和token進行檢查校驗,最後根據結果rebuild整個Form表單。
若您想在processForm中追蹤圖片的處理流程,可直接對下方函數進行斷點設置,並根據堆疊回溯來找出您關心的操作。
在運行完processForm函數後這裡給出rebuild後部分FORM表單截圖
到這裡整個表單的處理操作已經完成了。
4.2.5 異常派發。
上一步完成表單作業後,在不知不覺中已將request請求物件轉換成了回應response物件。眼看就要逐層返回並進行send操作了,但是在接下來的流程中drupal發現這是一個ajax請求,這裡主動把操作攔截了下來,並拋出AJAX異常來對此次請求做額外的處理。
擷取到例外狀況之後再處理例外狀況中進行對例外狀況的發放作業。
在這裡的發放其實是遍歷並且符合異常的過程,發生異常有許多情況,並符合到正確的例外然後進行特定的處理。如果沒有符合到,放行即可。我們這裡配對到了AJAX的異常,如果還比較關心其他異常的處理流程,在kernel.exception陣列中尋找即可。
我們進一步跟進發現onException的buildResponse函數中,有對AJAX的特定處理方法。
在uploadAjaxCallback函數中,我們從封包的URL中取得element_parents參數的值,並以此為key從我們最終處理完成的FORM表單中取得出結果,接著對此結果進行渲染並呈現在HTML頁面上。
根據我們POST套件中URL得參數,我們這裡取出了FORM表單中user_picture下widget數組中的第一項。
最後在doRender中要被渲染的物件就是剛取出的元素。
渲染後整個處理過程已即將步入尾聲,開始建立response並逐層回傳。
4.2.6 kernel.response事件
既然到了response階段,那麼肯定就要開始觸發response對應了,接下來我們來看看response 有哪些監聽者
在response的發放函數中,其根本是對response物件的添磚加瓦以及做一些對應的擴充操作。如判斷動態頁面是否需要緩存,是否需要新增快取上下文,處理佔位符,在成功回應時設定額外標頭等。以上所有的操作都會在listeners下的kernel.response數組中,這裡不做詳細展開介紹。
4.2.7 kernel.finish_request
當request 和response 的操作都做完了之後,接下來會告訴drupal 核心所有已經完畢,會傳送finish_request事件,這個事件的監聽者只有一個:為了讓URL產生器在正確的上下文中運行,我們需要把目前請求設定為父請求。
4.2.8 kernel.terminate事件
完成上述操作後,request請求從請求堆疊中彈出,並逐層返回Index.php入口主頁面進行reponse的發送。最後進行掃尾工作,觸發kernel.terminate事件,判斷相關換成是否需要寫入檔案。最終drupal內核關閉。整個流程結束。
4.3 整個流程總結
透過上一個小節分解式的解析了整個流程,我們下面來簡單概括下:
傳送封包-->根據URL匹配相關路由-->根據路由找到對應的控制器-->根據控制器得到處理方法(我們這裡是表單相關操作)-->進行表單的建構與渲染-->處理表單請求- ->處理完表單後判斷是否為AJAX操作-->主動拋出異常利用AJAX回調來重新渲染URL中標註的FORM表單key-->完成相應構建響應對象-->發送對應-- >掃尾結束。
結合上面架構的分析與理解開始建構POC。在checkpoint安全團隊發布了一份關於此漏洞相關技術細節報告(上文有連結)中可知,漏洞觸發點是在表單建置好之後,觸發AJAX異常,從FROM表單提取出要渲染的對象,進行渲染時觸發,也就是在最終的doRender函數中。我們在doRender中發現如下可利用點:
根據第四部分我們對一個正常上傳包在框架中運行的流程的分析,我們可知想讓我們自己建構的內容在doRender中成功觸發漏洞,首先需要控制流程,讓其進入AJAX回呼部分。在下方這個if判斷中,我們可知需要同時滿足三個條件,$ajax_form_request ,$form_state->isProcessingInput() 和$request->request->get('form_id')== $form_id。 $ajax_form_request的值從下圖可知是由ajax_form這個變數控制,form_id 是表單的id。
接下來,並利用url中的element_parents參數值來取得表單陣列中的數值。在第四部分4.2.5小節有所講述,此處不再重複說明。 最後建構對應的變數利用doRender函數中的call_user_func_array來觸發漏洞。
根據上述描述,我們利用mail參數建構如下POC套件
除了上述郵件參數可控外,在分析過程中同時發現form_build_id參數也可控,另一種POC如下。
以上是如何深入分析drupal8框架和漏洞動態調試的詳細內容。更多資訊請關注PHP中文網其他相關文章!