Thinkphp5代碼審計(jì)(二)未開啟路由rce
thinkphp5.0.18 rce
配置文件默認(rèn)是沒有開啟強(qiáng)制路由的

瀏覽器訪問網(wǎng)頁后,進(jìn)入框架入口

在入口處,只有兩個(gè)步驟,第一步引入了convertion的配置

跟進(jìn)第二步,看看框架怎么執(zhí)行的,run方法里面initCommon會(huì)生成配置信息

跟進(jìn)

判斷了配置文件的類型,如果是php文件,就執(zhí)行一遍set方法

這樣就加載了配置信息,加載完配置信息后,程序返回run方法繼續(xù)執(zhí)行,在116行進(jìn)入了routeCheck方法

routerCheck方法,608行path方法內(nèi)的pathinfo判斷了url訪問模式,并返回了處理后的路徑信息,隨后加載了路由配置

如果是強(qiáng)制路由模式,就會(huì)拋出錯(cuò)誤

這里我用完整的url,/public/index.php/index/Index/index

在parseUrl方法內(nèi),把url處理后返回了路由信息的數(shù)組

最后這些信息傳進(jìn)了exec
在thinkphp3里exec方法會(huì)使用eval或者include渲染網(wǎng)頁,我們看看thinkphp5是怎樣的

渲染在module里完成的

跟進(jìn)module,在module方法內(nèi)的第567行,把url傳入的信息轉(zhuǎn)換成了控制器的路徑

跟進(jìn)controller,這里有一個(gè)過去模塊和控制器信息的方法getModuleAndClass

這里第一個(gè)分支比較重要,是區(qū)分pathinfo模式和兼容模式的

如果這里沒有反斜杠\,就會(huì)進(jìn)入parseClass方法,強(qiáng)制在模塊后面加上controller

這時(shí)候執(zhí)行的類,就是命名空間內(nèi)app\index\controller\Index類


如果要但是這個(gè)命名空間是開發(fā)者自己寫的,可以直接利用的可能性不大,所以回到getModuleAndClass方法,想辦法進(jìn)入think庫,這里構(gòu)造一個(gè)url去訪問/thinkphp_5.0.15/thinkphp/library/think/App.php內(nèi)的invokeFunction,至于為什么訪問這個(gè)方法,因?yàn)閡p看到payload上是這個(gè)方法,說明里面可以執(zhí)行命令。
構(gòu)造思路如下:
1.模塊名似乎沒有影響,還是用index
2.app.php的命名空間是think,所以控制器名是think\app

3.方法名invokeFunction
4.pathinfo模式不能使用反斜杠,會(huì)被替換成斜杠,所以u(píng)rl要使用兼容模式格式
瀏覽器訪問下試試:

這樣我們就能控制訪問的類了,在把控制器名稱返回后,又在581行使用判斷了is_callable函檢查invokefunction方法是否存在于控制器

再跟進(jìn)595行,看看invokeMethod內(nèi)做了什么

先是通過ReflectionMethod類獲取了源代碼中app類的信息,隨后在339行,bindParams方法獲取url中的參數(shù)存儲(chǔ)進(jìn)vars變量,然后遍歷方法的參數(shù),創(chuàng)建一個(gè)數(shù)目對(duì)應(yīng)的數(shù)組

如果url中有invokeFunction需要的參數(shù)名,就傳遞進(jìn)去,否則就拋出缺少參數(shù)的異常

構(gòu)造參數(shù)的方法需要分析invokeFunction的源碼,從源碼中看到,invokeFunction需要兩個(gè)參數(shù),第一個(gè)是function,第二個(gè)是vars

所以u(píng)rl里需要有function和vars參數(shù),參數(shù)值我們先任意填寫,這時(shí)候再次訪問,程序會(huì)執(zhí)行到343行的invokeArgs

ReflectionMethod里指定了反射的方法是think\app類中的invokeFunction
invokeArgs是ReflectionMethod類中的一個(gè)內(nèi)置方法,接受兩個(gè)參數(shù),第一個(gè)是think\app類對(duì)象,第二個(gè)是傳遞給反射方法的參數(shù)(數(shù)組形式),返回方法執(zhí)行的結(jié)果

所以invokeArgs是把vars反射到invokeFunction了,跟進(jìn)invokeFunction

這里會(huì)使用ReflectionFunction反射一個(gè)函數(shù),?ReflectionFunction用法和ReflectionMethod差不多,這里選擇call_user_func_array,為什么system函數(shù)不行,繼續(xù)跟進(jìn)下去就明白了,跟進(jìn)bindParams,里面有一個(gè)reset函數(shù)

system接受的字符串類型的參數(shù),但是reset方法是處理數(shù)組或者對(duì)象的,傳入字符串會(huì)報(bào)錯(cuò),所以不能反射system參數(shù),需要通過反射call_user_func_array函數(shù),調(diào)用system。
這里我們傳遞進(jìn)一個(gè)vars數(shù)組,構(gòu)造方法參考手冊

vars數(shù)組第一個(gè)元素是字符串類型,第二個(gè)元素是數(shù)組類型,在url里就如下構(gòu)造
這時(shí)候在瀏覽器使用我們完整的payload:
這是反射進(jìn)call_user_func_array的參數(shù)

ReflectionFunction::invokeArgs()用法如下:

反射完畢后,把命令執(zhí)行的結(jié)果返回:


篇幅過長,本來想多寫幾個(gè)洞的,沒想到這次審計(jì)這么難,個(gè)別難點(diǎn)不看別人payload還沒想不出來。