作者:Feynman@深信服千里目安全實驗室
原文鏈接:https://mp.weixin.qq.com/s/OWi3G4ETrV-yBsnWgdU_Ew

一、組件介紹

1.1 基本信息

ThinkPHP是一個快速、兼容而且簡單的輕量級國產PHP開發框架,誕生于2006年初,原名FCS,2007年元旦正式更名為ThinkPHP,遵循Apache 2開源協議發布,從Struts結構移植過來并做了改進和完善,同時也借鑒了國外很多優秀的框架和模式,使用面向對象的開發結構和MVC模式,融合了Struts的思想和TagLib(標簽庫)、RoR的ORM映射和ActiveRecord模式。

ThinkPHP可以支持windows/Unix/Linux等服務器環境,正式版需要PHP 5.0以上版本支持,支持MySql、PgSQL、Sqlite多種數據庫以及PDO擴展,ThinkPHP框架本身沒有什么特別模塊要求,具體的應用系統運行環境要求視開發所涉及的模塊決定。

1.2 版本介紹

ThinkPHP發展至今已有14年歷史,其核心開發版本已有數十個之多。2009年10月,ThinkPHP 2.0版本完成了新的重構和飛躍,成就了這一劃時代的版本,從此,ThinkPHP就基于此開始了長達十多年的演化與發展。ThinkPHP發展至今,核心版本主要有以下幾個系列,即ThinkPHP 2系列、ThinkPHP 3系列、ThinkPHP 5系列、ThinkPHP 6系列,各個系列之間在代碼實現及功能方面,有較大區別。其中ThinkPHP 2以及thinkPHP 3系列已經停止維護,ThinkPHP 5系列現使用最多,而ThinkPHP 3系列也積累了較多多的歷史用戶。版本細分如下圖所示:

1.3 使用量及使用分布

根據全網數據統計,使用ThinkPHP的網站多達15萬余個,其中大部分集中在國內,約占使用量的75%以上。其中,浙江、北京、山東、廣東四省市使用量最高,由此可見,ThinkPHP在國內被廣泛應用。通過網絡空間搜索引擎的數據統計和柱狀圖表,如下圖所示。

(數據來源:Zoomeye)

二、高危漏洞介紹

通過對ThinkPHP漏洞的收集和整理,過濾出其中的高危漏洞,可以得出如下列表。

漏洞名稱 漏洞ID 影響版本 漏洞披露日期
ThinkPHP 2.x/3.0 遠程代碼執行漏洞 ThinkPHP 2.x,3.0 2012
ThinkPHP 3.2.4 SQL注入漏洞 CVE-2018-18546 ThinkPHP <= 3.2.4 2018
ThinkPHP 3.2.4 SQL注入漏洞 CVE-2018-18529 ThinkPHP <= 3.2.4 2018
thinkphp 3.1.3 s parameter注入漏洞 CVE-2018-10225 ThinkPHP <= 3.1.3 2018
ThinkPHP 3.x update方法 SQL注入漏洞 ThinkPHP <3.2.4 2017
ThinkPHP 3.x orderby方法 SQL注入漏洞 ThinkPHP <3.2.4 2017
ThinkPHP 3.x where SQL注入漏洞 ThinkPHP <3.2.4 2018
ThinkPHP 3.x exp SQL注入漏洞 ThinkPHP <3.2.4 2018
ThinkPHP 3.x bind SQL注入漏洞 ThinkPHP <3.2.4 2018
ThinkPHP SQL注入漏洞--paraData方法 ThinkPHP 5.0.13-5.0.15 2018
ThinkPHP SQL注入漏洞--paraArraryData方法 ThinkPHP 5.1.6-5.1.7 2018
ThinkPHP SQL注入漏洞--parseWhereItem方法 ThinkPHP 5 2018
ThinkPHP SQL注入漏洞--parseOrder方法 ThinkPHP 5.1.16-5.1.22 2018
ThinkPHP SQL注入漏洞--orderby方法 ThinkPHP 5.0.0-5.0.21
Thinkphp 5.1.3-5.1.25
2018
ThinkPHP cacheFile變量文件包含漏洞 ThinkPHP 5.0.0-5.0.18 2018
ThinkPHP cache緩存函數遠程代碼執行漏洞 ThinkPHP 5.0.0-5.0.10 2017
ThinkPHP 5遠程代碼執行漏洞 ThinkPHP 5.0.7-5.0.22
ThinkPHP 5.1.0-5.1.30
2018
ThinkPHP 5遠程代碼執行漏洞 ThinkPHP 5.0.0-5.0.23
ThinkPHP 5.1.0-5.1.30
2019
ThinkPHP 6 任意文件操作漏洞 ThinkPHP 6.0.0-6.0.1 2020
ThinkPHP 6 反序列化漏洞 ThinkPHP 6.0.0-6.0.1 2020

中可以看出,ThinkPHP近年出現的高風險漏洞主要存在于框架中的函數,這些漏洞均需要在二次開發的過程中使用了這些風險函數方可利用,所以這些漏洞更應該被稱為框架中的風險函數,且這些風險點大部分可導致SQL注入漏洞,所以,開發者在利用ThinkPHP進行Web開發的過程中,一定需要關注這些框架的歷史風險點,盡量規避這些函數或者版本,則可保證web應用的安全性。

從上表數據來看,ThinkPHP 3系列版本的漏洞多是是2016/2017年被爆出,而ThinkPHP 5系列版本的漏洞基本為2017/2018年被爆出,從2020年開始,ThinkPHP 6系列的漏洞也開始被挖掘。

其中,2018年與2019年交替之際,ThinkPHP爆出了兩枚重量級的遠程代碼執行漏洞,這兩枚漏洞均不需要進行二次開發即可利用,攻擊者可通過框架中已有邏輯,直接構造惡意流量,在服務器中執行任意代碼,獲取服務器的最高權限。時至今日,這兩枚漏洞的利用在全網中仍非常活躍,且被譽為ThinkPHP框架中的“沙皇炸彈”。

三、漏洞利用鏈

3.1、暴露面梳理

根據ThinkPHP的歷史高位漏洞,梳理出分版本的攻擊風險點,開發人員可根據以下圖標,來規避ThinkPHP的風險版本,如下ThinkPHP暴露面腦圖。

3.2、利用鏈總結

基于暴露面腦圖,我們可以得出幾種可以直接利用的ThinkPHP框架漏洞利用鏈,不需要進行二次開發。

3.2.1、ThinkPHP 2.x/3.0 GetShell

ThinkPHP低于3.0 - GetShell

  • ThinkPHP 低版本可以使用以上漏洞執行任意系統命令,獲取服務器權限。

3.2.2、ThinkPHP 5.0 GetShell

ThinkPHP 5.0.x - GetShell

  • 首先明確ThinkPHP框架系列版本。
  • 根據ThinkPHP版本,如是5.0.x版本,即可使用ThinkPHP 5.x遠程代碼執行漏洞,無需登錄,即可執行任意命令,獲取服務器最高權限。

3.2.3、ThinkPHP 5.1 GetShell

  • ThinkPHP 5.1.x - GetShell
  • 首先明確ThinkPHP框架系列版本。
  • 根據ThinkPHP版本,如是5.1.x版本,即可使用ThinkPHP 5.x遠程代碼執行漏洞1,無需登錄,即可執行任意命令,獲取服務器最高權限。
  • 如需使用ThinkPHP 5.x遠程代碼執行漏洞2,則需要index.php文件中跳過報錯提示,即 文件中有語句:“error_reporting(0);”,故該漏洞在5.1.x系列版本利用需要滿足以上前提,利用較難。

四、高可利用漏洞分析

從高危漏洞列表中,針對ThinkPHP不需二次開發即可利用的高危漏洞進行深入分析。

4.1、ThinkPHP 2.x/3.0遠程代碼執行漏洞

4.1.1、漏洞概要

  • 漏洞名稱:ThinkPHP 2.x/3.0遠程代碼執行
  • 參考編號:無
  • 威脅等級:高危
  • 影響范圍:ThinkPHP 2.x/3.0
  • 漏洞類型:遠程代碼執行
  • 利用難度:簡單

4.1.2、漏洞描述

ThinkPHP是為了簡化企業級應用開發和敏捷WEB應用開發而誕生的開源MVC框架。Dispatcher.class.php中res參數中使用了preg_replace的/e危險參數,使得preg_replace第二個參數就會被當做php代碼執行,導致存在一個代碼執行漏洞,攻擊者可以利用構造的惡意URL執行任意PHP代碼。

4.1.3、漏洞分析

漏洞存在在文件 /ThinkPHP/Lib/Think/Util/Dispatcher.class.php 中,ThinkPHP 2.x版本中使用preg_replace的/e模式匹配路由,我們都知道,preg_replace的/e模式,和php雙引號都能導致代碼執行的,即漏洞觸發點在102行的解析url路徑的preg_replace函數中。代碼如下:

該代碼塊首先檢測路由規則,如果沒有制定規則則按照默認規則進行URL調度,在preg_replace()函數中,正則表達式中使用了/e模式,將“替換字符串”作為PHP代碼求值,并用其結果來替換所搜索的字符串。

正則表達式可以簡化為“\w+/([\^\/])”,即搜索獲取“/”前后的兩個參數,{}里面可以執行函數,然后我們在thinkphp的url中的偶數位置使用${}格式的php代碼,即可最終執行thinkphp任意代碼執行漏洞,如下所示:

index.php?s=a/b/c/${code}

index.php?s=a/b/c/${code}/d/e/f

index.php?s=a/b/c/d/e/${code}

由于ThinkPHP存在兩種路由規則,如下所示:

http://serverName/index.php/模塊/控制器/操作/[參數名/參數值...]
如果不支持PATHINFO的服務器可以使用兼容模式訪問如下:
http://serverName/index.php?s=/模塊/控制器/操作/[參數名/參數值...]

也可采用 index.php/a/b/c/${code}一下形式。

4.2、ThinkPHP 5.x 遠程代碼執行漏洞1

4.2.1、漏洞概要

  • 漏洞名稱:ThinkPHP 5.0.x-5.1.x 遠程代碼執行漏洞
  • 參考編號:無
  • 威脅等級:嚴重
  • 影響范圍:ThinkPHP v5.0.x < 5.0.23,ThinkPHP v5.1.x < 5.0.31
  • 漏洞類型:遠程代碼執行
  • 利用難度:容易

4.2.2、漏洞描述

2018年12月10日,ThinkPHPv5系列發布安全更新,修復了一處可導致遠程代碼執行的嚴重漏洞。此次漏洞由ThinkPHP v5框架代碼問題引起,其覆蓋面廣,且可直接遠程執行任何代碼和命令。電子商務行業、金融服務行業、互聯網游戲行業等網站使用該ThinkPHP框架比較多,需要格外關注。由于ThinkPHP v5框架對控制器名沒有進行足夠的安全檢測,導致在沒有開啟強制路由的情況下,黑客構造特定的請求,可直接進行遠程的代碼執行,進而獲得服務器權限。

4.2.3、漏洞分析

本次ThinkPHP 5.0的安全更新主要是在library/think/APP.php文件中增加了對控制器名的限制,而ThinkPHP 5.1的安全更新主要是在library/think/route/dispatch/Module.php文件中增加了對控制器名的限制。

從以上補丁更新可知,該漏洞的根源在于框架對控制器名沒有進行足夠的檢測,從而會在未開啟強制路由的情況下被引入惡意外部參數,造成遠程代碼執行漏洞。

由ThinkPHP的架構可知,控制器(controller)是通過url中的路由進行外部傳入的,即/index.php?s=/模塊/控制器/操作/[參數名/參數值...],控制器作為可控參數,經過library/think/APP.php文件進行處理,我們跟蹤路由處理的邏輯,來完整看一下該漏洞的整體調用鏈:

首先在run()主函數中,url傳入后需要經過路由檢查,如下代碼所示:

跟進 self::routeCheck 函數

在 620行中調用 $request->path() 函數,該函數位于thinkphp/library/think/Request.php文件中,在該函數中跟進到本文件的$this->pathinfo()函數,在該函數中,就進行url解析,獲取路由中的各個部分內容。

其中var_pathinfo參數即為系統默認參數,默認值為s,通過GET方法將獲取到的var_pathinfo的值,即s=/模塊/控制器/操作/[參數名/參數值...]的內容送到routeCheck()函數中$path參數進行路由檢查處理。

繼續回到routeCheck()函數:

在初始化路由檢查配置之后,就進行Route::check,由以上代碼看出,若路由尋不到對應操作,即返回$result=false,且開啟了強制路由$must的情況下,就會拋出異常,并最終進入Route::parseUrl函數,進行$path解析,以上就進入了我們的漏洞觸發點

首先,在該函數中進行url解析,然后,進入到parseUrlPath函數,根據/進行路由地址切割,通過數組返回:

最終在parseUrl函數中,將返回的route后返回:

回到thinkphp/library/think/App.php文件的run()函數:

在完成RouteCheck后,進入到exec()函數中去:

在該函數中,首先路由信息首先進入module()函數進行檢驗,該函數首先查看該路由中的模塊信息是否存在且是否存在于禁止的模塊類表中:

模塊存在的話,繼續往下跟蹤,分別將模塊中的controller、actionName經過處理后賦值到action,最終action被賦值給了$call參數。

最終$call參數進入了self::invokeMethod()進行處理:

在函數中,通過反射ReflectionMethod獲取controller(method[0])和action(method[1])對象下的方法,然后通過$args = self::bindParams($reflect, $vars);獲取到傳入參數。以上即為漏洞調用鏈。

我們根據Payload來進行最終攻擊鏈的總結:

siteserver/public/index.php?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami

根據上面的分析,我們將路由解析為:

module:index

controller:think\app

action:invokefunction

通過上述的利用鏈,最終通過反射ReflectionMethod進入到Think/app文件中的invokefunction方法中:

通過構造參數,最終即可執行任意代碼。

4.3、ThinkPHP 5.x 遠程代碼執行漏洞2

4.3.1、漏洞概要

  • 漏洞名稱:ThinkPHP 5.0.x-5.1.x遠程代碼執行漏洞
  • 參考編號:無
  • 威脅等級:嚴重
  • 影響范圍:ThinkPHP v5.0.x < 5.0.23,ThinkPHP v5.1.x < 5.0.31
  • 漏洞類型:遠程代碼執行漏洞
  • 利用難度:容易

4.3.2、漏洞描述

2019年1月11日,某安全團隊公布了一篇ThinkPHP 5.0.遠程代碼執行漏洞文檔,公布了一個ThinkPHP 5.0.遠程代碼執行漏洞。文章中的該漏洞與2018年12月的ThinkPHP 5.0.*遠程代碼執行漏洞原理相似,攻擊者可利用該漏洞在一定條件下獲取目標服務器的最高權限。后經研究,在一定條件下,ThinkPHP 5.1.x版本也存在該漏洞,在滿足條件的情況下,攻擊者可利用該漏洞執行任意代碼。

4.3.3、漏洞分析

該漏洞的漏洞關鍵點存在于thinkphp/library/think/Request.php文件中:

從代碼中可知:

method()函數主要用于請求方法的判斷,var_method沒有通過,為可控參數,通過外部傳入,thinkphp支持配置“表單偽裝變量”,var_method在在外部的可控參數表現為_method:

由于var_method沒有做任何過濾,我們可以通過控制_method參數的值來動態調用Request類中的任意方法,通過控制$_POST的值來向調用的方法傳遞參數。由上可知,漏洞存在于method()函數中,我們就需要尋找該函數的調用鏈,來構造POC。

第一個構造鏈在__construct()構造方法中,該方法如下:

函數中對option的鍵名為該類屬性時,則將該類同名的屬性賦值為$options中該鍵的對應值。因此可以構造請求如下,來實現對Request類屬性值的覆蓋,例如覆蓋filter屬性。filter屬性保存了用于全局過濾的函數。

再上一個漏洞分析過程中,我們跟蹤到了路由檢查self::routeCheck 函數,在過程中,會進入到thinkphp/library/think/Route.php文件中的check()函數,函數中調用了method()方法,并將函數執行結果轉換為小寫后保存在method = strtolower(method最終的值就可以被控制了。

在該函數中,調用了method()函數,在該函數中,就將進行變量覆蓋:

通過調用構造函數__construct(),最終將請求參數保存到input參數。

在進行routecheck后,已完成了第一部分調用鏈,實現了變量覆蓋,接下來就是要實現變量覆蓋后的代碼執行,具體調用鏈如下:

返回到App.php文件中的run()函數,接著進入到exec()函數中,然后進入到module()函數中,最終進入到了invokeMethod()函數,

從invokeMethod()函數中進入到bindParams()函數,然后進入到param()函數:

然后最終調用到input()函數:

最終我們根據array_walk_recursive()函數,進入到了filterValue()函數:

最終,通過回調函數call_user_func執行了代碼,整個調用鏈如上所示。


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