一文速通thinkphp3.2.3代碼審計(jì)
ThinkPHP是一個快速、簡單的基于MVC和面向?qū)ο蟮妮p量級PHP開發(fā)框架,遵循Apache2開源協(xié)議發(fā)布,從誕生以來一直秉承簡潔實(shí)用的設(shè)計(jì)原則,在保持出色的性能和至簡的代碼的同時,尤其注重開發(fā)體驗(yàn)和易用性,并且擁有眾多的原創(chuàng)功能和特性,為WEB應(yīng)用開發(fā)提供了強(qiáng)有力的支持。
3.2版本則在原來的基礎(chǔ)上進(jìn)行一些架構(gòu)的調(diào)整,引入了命名空間支持和模塊化的完善,為大型應(yīng)用和模塊化開發(fā)提供了更多的便利。
詳細(xì)見:https://www.kancloud.cn/manual/thinkphp/1679
目錄結(jié)構(gòu)
其中框架目錄Thinkphp結(jié)構(gòu)如下:
開始使用
整個網(wǎng)站的入口文件就是www/index.php,index.php包含了框架的入口文件,所以訪問后可以直接加載thinkphp框架

訪問成功運(yùn)行,框架會自動在www/Application/Home/Controller/目錄下生成IndexController.class.php,這里修改內(nèi)容如下:

再次訪問

配置文件
thinkphp的配置文件在www/ThinkPHP/Conf/convention.php

URL模式
入口文件是應(yīng)用的單一入口,對應(yīng)用的所有請求都定向到應(yīng)用入口文件,系統(tǒng)會從URL參數(shù)中解析當(dāng)前請求的模塊、控制器和操作:
這是3.2版本的標(biāo)準(zhǔn)URL格式。
url默認(rèn)是大小寫敏感的,也可以通過修改convertion.php,達(dá)到url不區(qū)分大小寫的目的
在訪問入口文件時,如果沒有指定模塊、控制器、方法,默認(rèn)會訪問HOME模塊下面的Index控制器的index方法,所以下面訪問是等效的,這也是為什么修改了index方法后可以直接體現(xiàn)出來
這種URL模式就是系統(tǒng)默認(rèn)的PATHINFO模式,不同的URL模式獲取模塊和操作的方法不同,ThinkPHP支持的URL模式有四種:普通模式、PATHINFO、REWRITE和兼容模式,可以在convertion.php設(shè)置URL_MODEL參數(shù)改變URL模式。

實(shí)戰(zhàn)練習(xí),ctfshow569
提示flag在Admin模塊,Login控制器,ctfshowLogin方法。前面看的配置文件可知默認(rèn)url模式為pathinfo,所以構(gòu)造url:
路由
要使用路由功能,前提是你的URL支持PATH_INFO(或者兼容URL模式也可以,采用普通URL模式的情況下不支持路由功能),并且在應(yīng)用(或者模塊)配置文件中開啟路由:

然后就是配置路由規(guī)則了,在模塊的配置文件中使用URL_ROUTE_RULES參數(shù)進(jìn)行配置,配置格式是一個數(shù)組,每個元素都代表一個路由規(guī)則,例如:
規(guī)則路由
規(guī)則路由是一種比較容易理解的路由定義方式,采用ThinkPHP設(shè)計(jì)的規(guī)則表達(dá)式來定義。
實(shí)戰(zhàn)ctfshow570
下載好源碼,看到common內(nèi)的全局路由配置如下:
因?yàn)閡rl里不方便傳斜杠,使用post傳命令,訪問url:
Thinkphp的渲染機(jī)制
Thinkphp的控制器中有show函數(shù),可以將文本渲染成html的網(wǎng)頁

如果渲染時傳入的變量未經(jīng)過濾,則會導(dǎo)致代碼執(zhí)行,跟進(jìn)show函數(shù),調(diào)用了display函數(shù)

繼續(xù)跟進(jìn),代碼在fetch函數(shù)中,完成了解析渲染

fetch函數(shù)中重點(diǎn)關(guān)注輸出緩沖區(qū)中的代碼

如果引擎類型是php,則使用eval渲染,會直接執(zhí)行代碼。但是thinkphp的默認(rèn)渲染引擎是Think,所以重點(diǎn)關(guān)注listen函數(shù),listen函數(shù)中,exec存在輸出的可能居高

跟進(jìn)exec發(fā)現(xiàn)是一個類的動態(tài)創(chuàng)建

模板的代碼傳遞給了這個類,跟進(jìn)這個類

run內(nèi)沒有其他輸出的地方,而且模板內(nèi)容和模板的緩存位置被傳遞進(jìn)load,所以跟進(jìn)load函數(shù),有call_user_func_array,可以動態(tài)的調(diào)用系統(tǒng)函數(shù),這里調(diào)用的就是load函數(shù)

load里有include包含了緩存代碼,之前傳入的php就是在這里執(zhí)行,然后包含結(jié)果存入輸出緩沖區(qū)中

后面被讀取到后,返回給了瀏覽器。
實(shí)戰(zhàn)ctfshow571
題目拿到文件目錄,發(fā)現(xiàn)Index控制器存在無過濾參數(shù):


讀取flag

信息泄露
1、ThinkPHP在開啟DEBUG的情況下會在Runtime目錄下生成日志,而且debug很多網(wǎng)站都沒有關(guān)
2、ThinkPHP默認(rèn)安裝后,也會在Runtime目錄下生成日志
THINKPHP3.2 結(jié)構(gòu):Application\Runtime\Logs\Home\16_09_09.log
THINKPHP3.1結(jié)構(gòu):Runtime\Logs\Home\16_09_09.log
日志存儲結(jié)構(gòu)是 :項(xiàng)目名\Runtime\Logs\Home\年份_月份_日期.log
參考鏈接:https://juejin.cn/post/7028872177424269343
實(shí)戰(zhàn)ctfshow?web572

burp抓包爆破日志文件
發(fā)現(xiàn)后門

SQL注入
1.使用find查詢
在執(zhí)行用戶傳入的參數(shù)前,會先執(zhí)行
獲取列名和類型,然后再生成用戶的sql語句。實(shí)例化M的時候,查詢列的語句不可控,這里重點(diǎn)看生成用戶sql語句的具體流程:

獲取了GET提交的id參數(shù),然后傳進(jìn)find函數(shù)

這里查詢的列是id,實(shí)例化M實(shí)際上是創(chuàng)建一個Model類,pk的值在Model類里已經(jīng)預(yù)定義為id,定義位置在/tp3.2.3/ThinkPHP/Library/Think/Model.class.php

thinkphp把碎片化的參數(shù)組合成一個sql語句大體原理,其存在語句模板,執(zhí)行查詢操作就使用查詢模板。獲取全部參數(shù)后,再替換模板原本的字段。
獲取參數(shù)的最后一步是解析參數(shù),即分析用戶提交的參數(shù)。具體的做法是把用戶提交的參數(shù)傳入_parseOptions函數(shù):

在_parseOptions函數(shù)里會獲取用戶查詢的目標(biāo)表、列的信息,后面就會看到為什么獲取這些信息了:

在這里進(jìn)入了_parseType函數(shù),解析用戶提交參數(shù)的類型

這里依據(jù)之前獲得的表、列信息,對參數(shù)類型進(jìn)行轉(zhuǎn)換,如果數(shù)據(jù)庫中的id列是int類型,則進(jìn)行intval轉(zhuǎn)換,float則floatval

所以如果數(shù)據(jù)庫中的id是int類型(mysql的id列默認(rèn)是int(3)),在這里payload會被轉(zhuǎn)換成純數(shù)字,這里我把數(shù)據(jù)庫id類型改成了varchar(255),后面沒有什么操作,返回了參數(shù)

再傳進(jìn)select函數(shù),生成sql語句:

buildSelectSql函數(shù)生成了sql語句,query函數(shù)執(zhí)行了語句,讀取結(jié)果。先進(jìn)buildSelectSql函數(shù)

沒有操作,進(jìn)parseSql函數(shù)看看:

這里就是之前提到的模板替換,如果不是要查詢的參數(shù),就替換成空,這里id是在where后面,所以進(jìn)入parseWhere函數(shù)

在判斷完參數(shù)名后,進(jìn)入parseWhereItem函數(shù)檢查參數(shù)值,然后進(jìn)入了parseValue函數(shù)

進(jìn)入parseValue后,進(jìn)入了escapeString函數(shù),在這里執(zhí)行了addslashes函數(shù),所以直接單引號閉合是不行的。
網(wǎng)上addslashes繞過方法沒成功,所以想辦法讓程序跳過過濾。在最開始看到如果傳入的參數(shù)是數(shù)組,就會直接跳過獲取列名,列值這一步:

這樣就可以操控parseSql內(nèi)替換的值

原本where的是一個鍵值對的數(shù)組,我們令他是一個字符串,這樣進(jìn)入parseWhere函數(shù)后會跳過過濾

這樣就直接在where后面拼接了值,所以構(gòu)造payload思路就來了:

實(shí)戰(zhàn)ctfshow 573
題目源碼:
https://blog.csdn.net/miuzzx/article/details/119424071
payload:


2.使用where查詢(傳入字符串)
生成的sql語句是:
直接閉合即可造成sql注入
實(shí)戰(zhàn)ctfshow web574

3.反序列化注入
thinkphp3.2.3反序列化用戶參數(shù)的時候,會導(dǎo)致sql注入。
參考:https://www.freebuf.com/articles/web/329045.html
尋找可以直接觸發(fā)的魔術(shù)方法,首選__destruct,因?yàn)橛|發(fā)門檻最低,看網(wǎng)上大佬做法是全局搜索所有析構(gòu)函數(shù),再逐個去看,這里搜索到了12個:

但是大部分參數(shù)直接傳入了ftp_close,fclose這類函數(shù),或者調(diào)用了一個不可控參數(shù)的方法,所以都不去看,只剩下了一個/tp3.2.3/ThinkPHP/Library/Think/Image/Driver/Imagick.class.php,里面調(diào)用了destory

但是找不到這個destory的來源,只能全局搜索

調(diào)用destroy方法的時候沒有傳參,所以這個delete語句注入不了,查看其他方法
在/tp3.2.3/ThinkPHP/Library/Think/Session/Driver/Memcache.class.php調(diào)用了delete方法

同樣是找不到來源,繼續(xù)全局搜索delete,存在8個結(jié)果

這里排除掉2個抽象函數(shù),剩下的6個類文件,其中4個是繼承了Driver類和Model類,Driver類是一個抽象類,所以也跳過,先看Model類

1.變量pk是預(yù)定義的列名,這里可以操控。
2.由于調(diào)用destory方法的時候沒有參數(shù),變量options是字符串類型。
3.這里假設(shè)payload里沒有逗號,這樣就直接創(chuàng)建了where的sql條件數(shù)組(列名=>列值),列名和列值都可控。
4._parseOptions上面sql注入提到過,會對比列的類型,由于列名和列值可控,先不考慮。
5.最后調(diào)用了delete方法,這個delete方法執(zhí)行了sql語句,最后返回了結(jié)果,delete后面調(diào)試定位。
動態(tài)調(diào)試一下,poc:

控制器:


跟進(jìn)去看運(yùn)行時的變量值,在每個類調(diào)用處打上斷點(diǎn):


前面推測的沒有問題,重點(diǎn)看下面delete的方法做了什么

拋出異常了,因?yàn)閐b未定義,想辦法構(gòu)造出一個db變量,db變量修改的位置

繼續(xù)找_db變量修改的位置

這里實(shí)例化了數(shù)據(jù)庫連接

config變量如下:

配置傳入了一個動態(tài)類

所以db變量實(shí)際上就是創(chuàng)建的Mysql對象,Mysql類繼承了Driver類,所以delete方法是來自Driver類,把這個Mysql類加入poc。
成功執(zhí)行


現(xiàn)在想辦法sql注入,剛才的流程里產(chǎn)生的參數(shù)會經(jīng)過addslashes轉(zhuǎn)換,所以不方便注入?;氐組odel的delete方法,看到開頭實(shí)際上有一個分支可以令options是一個數(shù)組

我們令pk變量的值和data數(shù)組的鍵名相同,就可以通過data傳入payload,根據(jù)上面find函數(shù)的sql注入我們知道,只要傳入的數(shù)組滿足以下關(guān)系,就可以繞過大量檢查
所以修改一下我們的poc,第一次傳入delete方法的options直接為空,同時Model類里的options也為空,直接進(jìn)入條件分支
重新運(yùn)行一下

再次運(yùn)行到這里執(zhí)行刪除操作的時候,我們已經(jīng)可以控制表名和條件

ctfshow 575題目試一下
題目部分源碼
這里可以用show方法的代碼執(zhí)行打,但是這里直接用反序列化打,poc如下,在當(dāng)前目錄寫入chanra.txt
把payload復(fù)制到cookie,運(yùn)行:
寫入成功

嘗試寫shell

訪問成功

4.comment注入
控制器代碼如下:

這個方法沒有過濾,可以直接執(zhí)行payload。簡單追蹤下

在comment打上斷點(diǎn),跟進(jìn)去

直接賦值給了options,返回this,之后繼續(xù)執(zhí)行find方法,find方法之前看到過,后面會構(gòu)造一個sql語句,跟進(jìn)去看看:

用options中的comment值替換原本的%comment%字段,跟進(jìn)去看看有沒有過濾

沒有過濾,直接拼接了
實(shí)戰(zhàn)ctfshow web576
題目源碼
發(fā)送payload

訪問webshell

5.使用where查詢(傳入數(shù)組)
之前傳入where字符串時,沒有經(jīng)過過濾就被數(shù)據(jù)庫執(zhí)行了,這次看下傳入數(shù)組會怎樣,先看看where方法做了什么
源碼:
打上斷點(diǎn)運(yùn)行

直接在options的where鍵創(chuàng)建了一個數(shù)組,在find方法里我們講到過,如果where的值是數(shù)組,那么會有addslashes轉(zhuǎn)義的,進(jìn)行跟進(jìn)看看怎么繞過。中間過程和find方法重復(fù),就略過了,直接到構(gòu)造sql語句的方法來看

跟進(jìn)

這里面的條件分支我們都不可控,只能再去其他函數(shù)看

parseKey是用反引號包裹列名的,沒想出怎么利用,略過

正常情況下參數(shù)在這里是字符串,直接addslashes轉(zhuǎn)義后就返回了,但是這里條件分支可以控制,控制了exp變量,可以做的事情就多了

這里令id的值是一個數(shù)組

成功進(jìn)入這個分支

又有一個分支


最后還是逃不出addslashes的命運(yùn),當(dāng)時我想著這不是沒漏洞了,后來看了別人文章才反應(yīng)過來,沒有引號的paylaod不就可以執(zhí)行了。。。。

明顯這個分支更容易滿足,構(gòu)造一個二維數(shù)組傳進(jìn)去

傳進(jìn)去之后就繞過了單引號包裹,且payload里沒有引號

所以解析后沒有引號包裹

成功爆出數(shù)據(jù)

上題目 ctfshow web577
當(dāng)成數(shù)字型注入

其他漏洞
6.php原生引擎下,assign方法變量覆蓋導(dǎo)致的RCE
默認(rèn)情況下tp框架使用的是THINK引擎渲染網(wǎng)頁,但是開發(fā)者可以手動設(shè)置/tp3.2.3/ThinkPHP/Conf/convention.php的TMPL_ENGINE_TYPE值為PHP,來使用原生引擎。

漏洞代碼:

傳入assign的如果是數(shù)組,則會創(chuàng)建一個數(shù)組array($name[key]=>$name[value])。
傳入assign的如果是字符串,則會創(chuàng)建一個數(shù)組array($name=>$from)。

漏洞代碼

display方法是渲染用的,沒有參數(shù)就渲染自身名稱的html文件模板,我這里已經(jīng)創(chuàng)建了/tp3.2.3/Application/Admin/View/Login/index.html文件,內(nèi)容是123。

我們跟進(jìn)幾個方法后就會看到渲染位置


關(guān)于extract函數(shù)的說明


把數(shù)組里的鍵值對提取成變量,flags為EXTR_OVERWRITE時,會覆蓋已有變量。
修改url

再跟蹤到這里,看到已經(jīng)覆蓋了變量


poc有兩種寫法
上題目 ctfshow 578
篇幅過長,超出上限,發(fā)不出圖片了,大家自己試下。。。。