php excel中解析显示html代码_骑士cms从任意文件包含到远程代码执行漏洞分析
前言
前些日子,騎士cms 官方公布了一個系統緊急風險漏洞升級通知:騎士cms 6.0.48存在一處任意文件包含漏洞,利用該漏洞對payload文件進行包含,即可造成遠程代碼執行漏洞。這篇文章將從漏洞公告分析開始,敘述一下筆者分析漏洞與構造payload時遇到的有趣的事情。
漏洞情報
官方發布的系統緊急風險漏洞升級通知如下:
http://www.74cms.com/news/show-2497.html
從官方公布的信息來看,官方修復了兩個地方:
1、/Application/Common/Controller/BaseController.class.php
2、/ThinkPHP/Library/Think/View.class.php
從BaseController.class.php這處補丁來看:
筆者猜測漏洞多半出在了渲染簡歷模板的assign_resume_tpl方法中。從補丁修復上來看,增添了如下代碼
$tpl_file = $view->parseTemplate($tpl);if(!is_file($tpl_file)){
return false;
}
可以發現程序通過$view->parseTemplate對$tpl參數進行處理,并對處理結果$tpl_file進行is_file判斷
我們先跟入$view->parseTemplate看看
從上圖143行的結果來看,parseTemplate中也是先通過is_file判斷,然后將符合的結果返回。
如果此處傳入的$tpl變量是文件,那么這個文件可以順利的通過parseTemplate與assign_resume_tpl方法中的is_file判斷。回想一下,這是一個文件包含漏洞,成功利用的先前條件是惡意的文件得存在,然后被包含。這個漏洞多半是通過assign_resume_tpl方法的$tpl參數傳入一個真實存在的待包含的惡意文件,而補丁先通過parseTemplate方法內的is_file判斷了一次這個惡意文件是否存在,接著又在assign_resume_tpl方法通過is_file方法判斷一次,成功的利用一定會使is_file為true。那assign_resume_tpl方法中增加的代碼是否有作用?又有著什么作用?
這個問題筆者將在文章最后介紹。
接下來從第二處View.class.php這處補丁來看:
補丁將fetch 方法中
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);代碼注釋替換為
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_'));在thinkphp中,E()函數是用來拋出異常處理的。可見這處的修改應該是不想讓$templateFile變量值寫到日志log文件中。
單從這點來看,命令執行所需的payload百分百是可以通過$templateFile變量寫到log文件里的,然后配合任意文件包含漏洞將這個log文件包含并執行。
漏洞分析
通過對漏洞情報的分析,我們差不多知道了這個漏洞的來龍去脈:
通過控制fetch 方法中$templateFile變量,將payload寫入log文件
通過assign_resume_tpl方法包含這個存在payload的log文件
首先我們拋開怎么把payload寫入log文件,先來看看文件包含漏洞怎么回事。
經過上文的猜測,我們可以通過assign_resume_tpl方法包含任意文件。首先我們要看看怎么通過請求調用assign_resume_tpl方法
如何訪問assign_resume_tpl方法
assign_resume_tpl方法位于common模塊base控制器下。通過對Thinkphp路由的了解,assign_resume_tpl方法多半是用如下url進行調用
http://127.0.0.1//74cms/index.php?m=common&c=base&a=assign_resume_tpl
但是實際上,程序拋出了個錯誤
這是為什么呢?經過動態調試發現一個有意思的事情:common模塊是并不能被直接調用的。原因如下:
\ThinkPHP\Library\Think\Dispatcher.class.php中存在如下代碼
從上圖代碼可見,因為我們common模塊位于MODULE_DENY_LIST中,因此不能直接通過m=common來調用common模塊。
既然不能直接調用,看看有沒有其他的辦法調用common模塊base控制器下的assign_resume_tpl方法
經過研究發現,幾乎所有其他的控制器,最終都繼承自common模塊的BaseController控制器
我們拿Home模塊的AbcController控制器舉例,見下圖:
AbcController 繼承FrontendController
而FrontendController由繼承了BaseController
因此可以通過get請求
http://127.0.0.1/74cms/index.php?m=home&c=abc&a=assign_resume_tpl&variable=1&tpl=2
來調用BaseController下的assign_resume_tpl,并將$variable=1、$tpl=2參數傳遞進去
同理,Home模塊下的IndexController控制器也是可以的,見下圖
IndexController繼承FrontendController,從上文可知,FrontendController繼承BaseController。因此也可以通過get請求
http://127.0.0.1/74cms/index.php?m=home&c=index&a=assign_resume_tpl&variable=1&tpl=2
來訪問BaseController下的assign_resume_tpl并向該方法傳參
我們后續分析就用
http://127.0.0.1/74cms/index.php?m=home&c=index&a=assign_resume_tpl&variable=xxx&tpl=xxx
這樣的形式調用assign_resume_tpl方法
既然我們可以通過請求向存在漏洞的assign_resume_tpl方法傳參了,距離漏洞利用成功已經不遠了
用測試文件觸發文件包含
我們接下來”假裝”在后臺上傳一個payload,用assign_resume_tpl這個接口包含下試試
筆者手動在如下目錄里放了個test.html
為什么這么放呢?因為筆者在源代碼里看到如下代碼
這里是74cms使用assign_resume_tpl調用word_resume.html的形式。因此筆者在測試時也在word_resume.html通目錄下放置了一個test.html,其內容如下:
構造如下請求
http://127.0.0.1/74cms/index.php?m=home&c=index&a=assign_resume_tpl&variable=1&tpl=Emailtpl/test
請求將調用assign_resume_tpl方法。動態調試過程如下:
可見此時$tpl為Emailtpl/test,get請求中參數成功傳入了。
我們來看一下fetch里怎么實現的
程序會執行到fetch方法中的Hook::listen('view_parse',$params);代碼處
此處代碼很關鍵,需要詳細說明下。Hook::listen('view_parse',$params);這處代碼的作用大體上有兩個:
Compiler:將模板文件經過一定解析與編譯,生成緩存文件xxx.php
Load:通過include方法加載上一步生成的xxx.php緩存文件
簡而言之,Hook::listen('view_parse',$params);先通過Compiler將攻擊者傳入的模板文件編譯為一個緩存文件,隨后調用Load加載這個編譯好的緩存文件。
首先我們來看下生產緩存文件過程
Compiler
從Hook::listen('view_parse',$params);到compiler方法的調用鏈如下:
該方法會將thinkphp的html模板中定義的標簽,解析成php代碼。例如模板中的”qscms:company_show/”
就會被解析成
除此之外,compiler方法還會將生成的xxx.php文件頭部加上一個如下代碼以防止該文件被直接執行
<?php if (!defined('THINK_PATH')) exit();說完compiler方法的功能后,我們來看下compiler方法是如何處理我們的test.html。
test.html中的代碼為<?php phpinfo(); ?>,經過解析之后,返回值見下圖
上圖compiler方法最終返回的是strip_whitespace($tmplContent);
但strip_whitespace方法的作用是去除代碼中的空白和注釋,對我們的payload沒什么實際意義。
最終compiler方法返回值為
<?php if (!defined('THINK_PATH')) exit(); phpinfo();?>這個值被寫入一個緩存文件,見下圖
緩存文件位于data/Runtime/Cache/Home/8a848d32ad6f6040d5461bb8b5f65eb0.php
到此為止,compiler流程已經結束,我們接下來看看加載過程
Load
Load代碼如下圖所示
從Hook::listen('view_parse',$params);到load方法的調用鏈如下:
從第一張圖可見,load代碼最終會include 我們compiler流程中生產的那個data/Runtime/Cache/Home/8a848d32ad6f6040d5461bb8b5f65eb0.php緩存文件
當8a848d32ad6f6040d5461bb8b5f65eb0.php被include之后,其中的惡意代碼執行,見下圖
執行成功后,瀏覽器如下
等等,為什么沒有phpinfo的回顯呢?是不是我們phpinfo執行失敗了?我們換一個payload試試,見下圖
這次我們執行一個生產目錄的命令
可見命令執行成功了。但是為什么phpinfo沒有回顯呢?
phpinfo回顯哪去了
從上文看,我們使用測試文件進行包含利用成功了,但是phpinfo的回顯卻不見了。進過研究發現,原因還是在fetch方法里。在fetch中,注意看下圖紅框處代碼:
Fetch中的load流程,即加載payload執行phpinfo的過程在上圖126行處Hook::listen('view_parse',$params);代碼中完成的。
而在此之前,程序通過ob_start打開緩沖區,因此phpinfo輸出的信息被存儲于緩沖區內,而在Hook::listen代碼執行之后,又通過ob_get_clean將緩沖區里的內容取出賦值給$content并刪除當前輸出緩沖區。因此phpinfo雖然執行成功,但回顯并不會顯示在瀏覽器頁面上。
如果想要獲取回顯,我們該怎么辦呢?這其實很簡單,見下圖
此時生成的緩存文件如下:
雖然在include這個緩存文件之前,程序通過ob_start打開緩沖區將phpinfo的輸出存到緩沖區里,但我們可以通過執行ob_flush沖刷出(送出)輸出緩沖區中的內容,打印到瀏覽器頁面上
怎么將payload寫入文件
上文我們一直在用一個手動上傳的test.html,很顯然這在實際漏洞利用過程中是不行的。我們需要想辦法在目標服務器里寫入一個payload。
在這里筆者繞了很多彎路,嘗試著在圖片上傳處做文章,但最后失敗了。后來筆者突然想起來官方的補丁,還記得上文我們從官方補丁中得到的漏洞情報?
補丁將fetch 方法中
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_').':'.$templateFile);代碼注釋替換為
if(!is_file($templateFile)) E(L('_TEMPLATE_NOT_EXIST_'));修改之處的E()函數是用來拋出異常處理的,而補丁將$templateFile刪除,正是不想讓$templateFile變量值寫到日志log文件中。看來payload是可以寫到日志文件里的。
我們回過頭來,看看fetch 方法中$templateFile變量怎么控制
還記得上文的分析嗎?$templateFile變量其實就是請求中傳入的tpl變量可以被攻擊者控制。從上圖來看,只要請求中傳入的tpl變量不是文件,就可以將tpl變量值寫入log文件。
那么我們就讓請求中傳入的tpl變量為payload字符串,滿足不是文件判斷,讓這個payload寫到日志中
實際發送如下請求控制$templateFile變量寫入日志文件
動態調試如下:
日志被寫到data/Runtime/Logs/Home/20_12_02.log,見下圖
但有個問題:我們為什么不像上文一貫作風,使用get請求傳遞tpl變量值呢?因為從get請求中url會在日志文件中被url編碼,而post請求則不然。因此只能發送post請求。
到此,完整的利用鏈構造出來了,發送如下請求即可包含日志文件并執行payload
寫在最后
總得來說這個漏洞并不復雜,但是卻很巧妙。在此過程中遇到很多有趣是問題。
構造圖片payload問題
在從官方補丁中發現利用log文件寫入payload思路之前,筆者花費大量時間嘗試利用圖片上傳寫入payload。因為74cms中利用了ThinkImage(也就是php-GD)對圖片的渲染和處理導致webshell代碼錯位失效,筆者嘗試了這篇文章里的思路
https://paper.seebug.org/387/#2-bypass-php-gdwebshell
這下倒是成功了一半:ThinkImage出現異常拋出錯誤了,并沒有對筆者webshell圖片進行渲染和處理,這看起來太棒了。但壞消息是,因為ThinkImage拋出異常,程序并沒有把筆者上傳成功后存儲于服務器上的圖片名稱拋出來,而圖片名稱是通過uniqid()函數生成的隨機數。uniqid() 函數基于以微秒計的當前時間,生成一個唯一的ID。筆者也沒有辦法猜測出上傳后的圖片名是什么,因此作罷。
這個問題與接下來的問題相關,也就是官方的補丁到底有沒有效
官方第一處補丁到底有沒有用
還記得上文漏洞情報分析那里,關于第一處補丁筆者的分析嗎?
補丁在assign_resume_tpl方法中增添了如下代碼
$tpl_file = $view->parseTemplate($tpl);if(!is_file($tpl_file)){
return false;
}
筆者在分析漏洞之前的想法是:因為這是一個文件包含漏洞,而assign_resume_tpl方法正是這個漏洞的入口,因此如果我們傳入的$tpl必定是一個文件,這樣可以輕松的繞過$view->parseTemplate($tpl);(parseTemplate中進行判斷,如果傳入的tpl是文件則直接return)與if(!is_file($tpl_file))判斷。
但經過深入的漏洞分析發現,assign_resume_tpl方法不僅是文件包含漏洞的入口,也是后續將payload寫入log文件的接口,通過控制assign_resume_tpl方法的tpl參數為字符串形式的payload,則這個payload將會在fetch中被寫入日志文件。
但在assign_resume_tpl方法中增加了判斷
$tpl_file會是payload字符串拼接.html這樣的形式,接下來的if(!is_file($tpl_file))會return false,而保護程序不進入fetch。
但這樣真有必要嗎?因為fetch中也打了補丁,經過上文對補丁的分析,就算是assign_resume_tpl方法中沒有修改使得payload進入了fetch,由于補丁的原因fetch中也不會把payload寫入日志了,因此這里的補丁顯的沒有太大必要。
官方補丁可以繞過嗎
經過從上面兩個問題的思考,可以發現一個新的問題,那就是官方補丁是否可以繞過。通過對漏洞的了解,官方補丁實際起作用的是不讓payload寫入日志文件。如果真的有人有辦法在圖片中寫入payload并上傳成功,在assign_resume_tpl方法中直接包含這個文件即可利用成功。assign_resume_tpl方法中的補丁并沒有限制tpl參數為文件。
也就是說:要么官方補丁是可以輕松繞過的、要么通過構造圖片webshell這條路走不通。具體哪個是對的,就要看看官方后續是否又出補丁繞過公告與一個新的補丁了。
轉載于https://xz.aliyun.com/t/8596
更多技術文章請關注公眾號:豬豬談安全
師傅們點贊、轉發、在看就是最大的支持
總結
以上是生活随笔為你收集整理的php excel中解析显示html代码_骑士cms从任意文件包含到远程代码执行漏洞分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: geth访问节点_以太坊客户端Geth控
- 下一篇: centos7 网卡配置vlan_Cen