作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/kwp5uxom7Amrj6S_-g8r4Q

前言

在前幾個月,Thinkphp連續爆發了多個嚴重漏洞。由于框架應用的廣泛性,漏洞影響非常大。為了之后更好地防御和應對此框架漏洞,阿爾法實驗室對Thinkphp框架進行了詳細地分析,并在此分享給大家共同學習。

本篇文章將從框架的流程講起,讓大家對Thinkphp有個大概的認識,接著講述一些關于漏洞的相關知識,幫助大家在分析漏洞時能更好地理解漏洞原理,最后結合一個比較好的RCE漏洞(超鏈接)用一種反推的方式去進行分析,讓大家將漏洞和框架知識相融合。體現一個從學習框架到熟悉漏洞原理的過程。

一、框架介紹

ThinkPHP是一個免費開源的,快速、簡單的面向對象的輕量級PHP開發框架,是為了敏捷WEB應用開發和簡化企業應用開發而誕生的。ThinkPHP從誕生以來一直秉承簡潔實用的設計原則,在保持出色的性能和至簡的代碼的同時,也注重易用性。

二、環境搭建

2.1 Thinkphp環境搭建

安裝環境:Mac Os MAMP集成軟件

PHP版本:5.6.10

Thinkphp版本:5.1.20

thinkphp安裝包獲取(Composer方式):

首先需要安裝composer。

curl -sS https://getcomposer.org/installer | php

下載后,檢查Composer是否能正常工作,只需要通過 php 來執行 PHAR:

img

若返回信息如上圖,則證明成功。

然后將composer.phar 移動到bin目錄下并改名為composer

mv composer.phar /usr/local/bin/composer

Composer安裝好之后,打開命令行,切換到你的web根目錄下面并執行下面的命令:

composer create-project topthink/think=5.1.20 tp5.1.20  –prefer-dist

若需要其他版本,可通過修改版本號下載。

驗證是否可以正常運行,在瀏覽器中輸入地址:

http://localhost/tp5.1.20/public/

img

如果出現上圖所示,那么恭喜你安裝成功。

2.2 IDE環境搭建及xdebug配置

PHP IDE工具有很多,我推薦PhpStorm,因為它支持所有PHP語言功能, 提供最優秀的代碼補全、重構、實時錯誤預防、快速導航功能。

PhpStorm下載地址:https://www.jetbrains.com/phpstorm/

Xdebug

Xdebug是一個開放源代碼的PHP程序調試器,可以用來跟蹤,調試和分析PHP程序的運行狀況。在調試分析代碼時,xdebug十分好用。

下面我們說一下xdebug怎么配置(MAMP+PHPstrom)

1.下載安裝xdebug擴展(MAMP自帶 )。

2.打開php.ini文件,添加xdebug相關配置

[xdebug]

xdebug.remote_enable = 1

xdebug.remote_handler = dbgp

xdebug.remote_host = 127.0.0.1

xdebug.remote_port = 9000 #端口號可以修改,避免沖突

xdebug.idekey = PHPSTROM

然后重啟服務器。

3.客戶端phpstorm配置

3.1點擊左上角phpstorm,選擇preferences

img

3.2 Languages & Frameworks -> PHP,選擇PHP版本號,選擇PHP執行文件。

img

img

在選擇PHP執行文件的時候,會顯示 “Debugger:Xdebug”,如果沒有的話,點擊open打開配置文件。

img

將注釋去掉即可。

3.3配置php下的Debug

img

Port和配置文件中的xdebug.remote_port要一致。

3.4配置Debug下的DBGp proxy

img

填寫的內容和上面php.ini內的相對應。

3.5配置servers

img

點擊+號添加

3.6配置debug模式

img

img

在Server下拉框中,選擇我們在第4步設置的Server服務名稱,Browser選擇你要使用的瀏覽器。所有配置到此結束。

4.xdebug使用

img

開啟xdeubg監聽

img

下一個斷點,然后訪問URL,成功在斷點處停下。

img

三、框架流程淺析

img

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

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

第二行引入base.php基礎文件,加載了Loader類,然后注冊了一些機制–如自動加載功能、錯誤異常的機制、日志接口、注冊類庫別名。

img

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

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

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

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

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

  4. 如果存在Composer安裝,則注冊Composer自動加載

  5. 注冊extend擴展目錄

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

然后再說下自動加載流程,看看程序是如何進行自動加載的?

img

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

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

img

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

img

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

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

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

img

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

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

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

  3. 模塊\入口綁定

    img

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

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

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

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

  7. 記錄路由和請求信息

    img

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

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

  9. 根據獲取的調度信息執行路由調度

img

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

img

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

img

img

在應用的數據響應輸出之后,系統會進行日志保存寫入操作,并最終結束程序運行。

img

四、漏洞預備知識

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

4.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下呢?這就要涉及要另一個概念—根命名空間。

4.1.1 根命名空間

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

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

img

4.2 URL訪問

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

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

如果不支持PATHINFO的服務器可以使用兼容模式訪問如下

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

什么是pathinfo模式?

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

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

而pathinfo模式是這樣的

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

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

什么是$_SERVER['PATH_INFO']?

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

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

img

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

img

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

1.入口文件

文件地址:public\index.php

作用:負責處理請求

2.模塊(以前臺為例)

模塊地址:application\index

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

3.控制器

控制器目錄:application\index\controller

作用:書寫業務邏輯

  1. 操作(方法)

在控制器中定義的方法

  1. 參數名/參數值

方法中的參數及參數值

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

img

那么可以輸入http://serverName/index.php/index(模塊)/Test(控制器)/hello(方法)/name(參數名)/world(參數值)

img

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

另外再講一下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

img

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

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

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

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

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

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

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

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

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

4.3.1 路由定義

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

4.3.2 路由檢測

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

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

img

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

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

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

img

F7跟進routeCheck()方法

img

route_check_cache路由緩存默認是不開啟的。

img

然后我們進入path()方法

img

繼續跟進pathinfo()方法

img

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

img

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

img

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

4.3.3 路由解析

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

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

img

img

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

img

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

img

跟進parseUrl()方法

img

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

img

img

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

img

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

img

img

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

img

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

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

img

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

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

img

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

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

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

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

img

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

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

img

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

img

img

img

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變量中。

img

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

img

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

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

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

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

4.3.4 路由調度

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

img

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

img

將閉包函數注冊為中間件,然后存入了$this->queue[‘route’]數組中。

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

img

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

img

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

img

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

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

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

img

跟進exec()方法

img

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

img

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

img

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

img

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

img

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

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

img

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

img

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

img

img

img

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

img

img

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

繼續往下看

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

img

在這個閉包函數內,主要做了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傳入。

img

img

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

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

img

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

4.3.5 響應輸出及應用結束

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

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

img

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

img

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

img

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

img

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

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

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

img

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

img

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

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

img

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

img

輸出完畢后,跳到了appShutdown()方法,保存日志并結束了整個程序運行。

4.4 流程總結

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

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

五、漏洞分析及POC構建

相信大家在看了上述內容后,對Thinkphp這個框架應該有所了解了。接下來,我們結合最近一個思路比較好的RCE漏洞再來看下。為了更好地理解漏洞,我通過以POC構造為導引的方式對漏洞進行了分析,同時以下內容也體現了我在分析漏洞時的想法及思路。

在/thinkphp/library/think/Container.php 中340行:

img

在Container類中有個call_user_func_array()回調函數,經常做代碼審計的小伙伴都知道,這個函數非常危險,只要能控制$function$args,就能造成代碼執行漏洞。

如何利用此函數?

通過上面的URL路由分析,我們知道Thinkphp可由外界直接控制模塊名、類名和其中的方法名以及參數/參數值,那么我們是不是可以將程序運行的方向引導至這里來。

如何引導呢?

要調用類肯定需要先將類實例化,類的實例化首先需要獲取到模塊、類名,然后解析模塊和類名去組成命名空間,再根據命名空間的特性去自動加載類,然后才會實例化類和調用類中的方法。

我們先對比之前正常的URL試著構建下POC。

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

http://127.0.0.1/tp5.1.20/public/index.php/模塊?/Container/invokefunction

構建過程中,會發現幾個問題。

  1. 模塊應該指定什么,因為Container類并不在模塊內。

  2. 模塊和類沒有聯系,那么組建的命名空間,程序如何才能加載到類。

先別著急,我們先從最開始的相關值獲取來看看(獲取到模塊、類名),此過程對應上面第四大節中的4.3.3路由解析中。

img

img

app_multi_module為true,所以肯定進入if流程,獲取了$module$bind$available的值。在紅色框處如果不為true,則會直接報錯結束運行,所以此處需要$module$available都為True。而$available的值一開始就被定義為False,只有在后續的3個if條件中才會變為true。

來看下這3個if條件,在默認配置下,由于沒有路由綁定,所以$bind為null。而empty_module默認模塊也沒有定義。所以第三個也不滿足,那么只能寄托于第二個了。

img

在第二個中,1是判斷$module是否在禁止訪問模塊的列表中,2是判斷是否存在這個模塊。

img

img

所以,這就要求我們在構造POC時,需要保證模塊名必須真實存在并且不能在禁用列表中。在默認配置中,我們可以指定index默認模塊,但是在實際過程中,index模塊并不一定存在,所以就需要大家去猜測或暴力破解了,不過一般模塊名一般都很容易猜解。

獲取到模塊、類名后,就是對其進行解析組成命名空間了。此過程對應上面第四大節中的4.3.4路由調度中。

img

這里首先對$name(類名)進行判斷,當$name以反斜線\開始時會直接將其作為類的命名空間路徑。看到這里然后回想一下之前的分析,我們會發現這種命名空間路徑獲取的方式和之前獲取的方式不一樣(之前是進入了parseClass方法對模塊、類名等進行拼接),而且這種獲取是不需要和模塊有聯系的,所以我們想是不是可以直接將類名以命名空間的形式傳入,然后再以命名空間的特性去自動加載類?同時這樣也脫離了模塊這個條件的束縛。

那我們現在再試著構造下POC:

img

http://127.0.0.1/tp5.1.20/public/index.php/index/think\Container/invokefunction

剩下就是指定$function參數和$var參數值了,根據傳參特點,我們來構造下。

http://127.0.0.1/tp5.1.20/public/index.php/index/think\Container/invokefunction/function/call_user_func_array/vars[0]/phpinfo/vars[1][]/1

構造出來應該是這樣的,但是由于在pathinfo模式下,$_SERVER['PATH_INFO']會自動將URL中的“\”替換為“/”,導致破壞掉命名空間格式,所以我們采用兼容模式。

默認配置中,var_pathinfo默認為s,所以我們可以用$_GET[‘s’]來傳遞路由信息。

http://127.0.0.1/tp5.1.20/public/index.php?s=index/think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

另外由于App類繼承于Container類,所以POC也可以寫成:

http://127.0.0.1/tp5.1.20/public/index.php?s=index/think\App/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

漏洞利用擴大化

  1. 以反斜線\開始時直接將其作為類的命名空間路徑。

  2. thinkphp命名空間自動加載類的特性。

由于這兩點,就會造成我們可以調用thinkphp框架中的任意類。所以在框架中,如果其他類方法中也有類似于invokefunction()方法中這樣的危險函數,我們就可以隨意利用。

例如:Request類中的input方法中就有一樣的危險函數。

img

跟入filterValue()方法

img

POC:

img

http://127.0.0.1/tp5.1.20/public/index.php?s=index/\think\Request/input&filter=phpinfo&data=1

六、結語

寫這篇文章的其中一個目的是想讓大家知道,通過框架分析,我們不僅可以在分析漏洞時變得更加容易,同時也可以對漏洞原理有一個更深的理解。所以,當我們在分析一個漏洞時,如果很吃力或者總有點小地方想不通的時候,不如從它的框架著手,一步一步來,或許在你學習完后就會豁然開朗,亦或者在過程中你就會明白為什么。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/888/