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