前言:
上次做了成信大的安詢杯第二屆CTF比賽,遇到一個tp6的題,給了源碼,目的是讓通過pop鏈審計出反序列化漏洞。
這里總結一下tp6的反序列化漏洞的利用。
0x01環境搭建
現在tp新版本的官網不開源了,但是可以用composer構建環境,系統需先安裝composer。然后執行命令:
composer create-project topthink/think=6.0.x-dev v6.0
cd v6.0
php think run
或者可以去github上下載,但是需要改動很多,也可以去csdn上下載人家配好了的源碼。
tp6需要php7.1及以上的環境才能搭建成功。
搭建完成后訪問ip加/public即可
0x02利用條件
需要在根目錄下的 /app/controller的index.php里面存在unserialize()函數且為可控點,例如存在
unserialize($_GET['payload'])
0x03POP鏈分析
總的目的是跟蹤尋找可以觸發__toString()
魔術方法的點
先從起點__destruct()或__wakeup方法
開始,因為它們就是unserialize的觸發點。
剛裝了系統,還沒下載phpstorm,先利用seay審計,然后全局搜索__destruct()方法,這里用了/vendor/topthink/think-orm/src下的Model.php,因為它里面含有save()方法可以被觸發。
跟進去,當$this->lazySave == true
時,$this->save()
方法將被觸發
$this->lazySave == true
時,會觸發$this->save()
方法
跟進save()方法
發現對$this->exists屬性進行判斷,如果為true則調用updateData()
方法,false則調用insertData()
方法。
試着跟進一下updateData()方法,發現updateData
方法可以繼續利用
那么我們首先要構造參數使得updateData()被觸發,需要將下面這個if過掉
跟進isEmpty()方法看一下,
保證isEmpty返回false,以及$this->trigger(‘BeforeWrite’)返回true即可繞過判斷然后觸發我們的updateData()方法。
構造$this->data為非空數組 構造$this->withEvent為false 構造$this->exists為true
繼續跟進updateData()方法
需要繞過含有return的判斷,第一個判斷前面的save方法符合條件,第二個if需要$data非空
$data的屬性值由方法getChangedData()控制,跟進它
如圖兩個變量初始值為[] 符合下面if的判斷,結果返回1,即默認返回$data=1,確保這一步繞過之后我們就到了checkAllowFields()方法了
跟進checkAllowFields方法
發現了個字符串拼接
由於$this->field==null,$this->schema==null,因此默認滿足
那么我們看上面的db()方法
跟進db()方法
發現滿足判斷條件的話,就存在$this->table . $this->suffix
參數的拼接,這不就是那兩個熟悉的拼接嗎,可以觸發__toString
滿足connect函數的調用即可。
后面就是延續tp5反序列化的觸發toString
魔術方法了,就是原來vendor/topthink/think-orm/src/model/concern/Conversion.php的__toString開始的利用鏈
目前的邏輯鏈圖:
也就是:
__destruct()——>save()——>updateData()——>checkAllowFields()——>db()——>$this->table . $this->suffix(字符串拼接)——>toString()
這一部分構造的參數為:
$this->exists = true; $this->$lazySave = true; $this->$withEvent = false;
尋找__toString觸發點
全局搜索__toString(),找到vendor/topthink/think-orm/src/model/concern/Conversion.php
跟進toJson()方法
跟進toArray()方法
第三個foreach里面存在getAttr
方法,他是個關鍵方法,我們需要觸發他
觸發條件:
$this->visible[$key]存在,即$this->visible存在鍵名為$key的鍵,而$key則來源於$data的鍵名,$data則來源於$this->data,也就是說$this->data和$this->visible要有相同的鍵名$key
構造參數,把$key
做為參數傳入getAttr
方法
跟進getAttrr()方法
然后$key值就傳入到了getData()方法,跟進getData方法
第一個if判斷傳入的值,$key值不為空,因此繞過,然后$key值傳入到了getRealFieldName()方法,跟進getRealFieldName
方法
當$this->strict
為true
時直接返回$name
,即$key
回到getData
方法,此時$fieldName = $key
,進入判斷語句:
返回$this->data[$fielName]也就是$this->data[$key]
,記為$value
,再回到getAttr
也就是返回 $this->getValue($key, $value, null);
跟進getValue()方法
只要滿足$this->withAttr[$key]存在且$this->withAttr[$key]不為數組就可以觸發命令執行。
最終會把$this->withAttr[$key]作為函數名動態執行函數,而$value作為參數,就可以利用執行系統函數達到命令執行。
到這里呈現了一條完整的POP鏈。
后半部分__toString的邏輯鏈圖
這一部分參數賦值:
$this->table = new think\model\Pivot(); $this->data = ["key"=>$command]; $this->visible = ["key"=>1]; $this->withAttr = ["key"=>$function]; $this->$strict = true;
因為這里的Model
類為抽象abstract
類,所以我們需要找一個繼承於Model
的類,比如Pivot
類
0x04 利用exp
tp默認主頁目錄為/public/,這下面的index.php會定位到/app/controller/index.php文件,因此url中/public/將調用app/controller/index.php,那么我們要知道里面的GET變量,用來傳參數。
那么我們定位到app/controller/index.php,找到里面的unserialize()函數里面的GET變量。我這里的環境里面沒有unserialize()函數,我們手動加上並加上一個GET變量
網上我看到其他tp6框架的這個文件里面用的是$u這個變量來裝unserialize()的值,並且用的GET變量是c,而且還對GET變量進行了base64_decode()編碼,我這里為了對應安詢杯比賽環境,沒有加上base64編碼。
首先利用phpggc工具生成exp,phpggc是一個反序列化payload生成工具,大佬們已經將tp6的exp上傳到了phpggc,需要安裝在linux上,然后執行以下命令生成exp的payload
./phpggc -u ThinkPHP/RCE2 'phpinfo();'
將生成的序列化字符串的payload傳參給GET變量c,發送請求,然后將執行phpinfo()。
如果是真實的tp6框架,試試將payload進行base64編碼后再發送請求。