Thinkphp 5.0.9 SQL注入

前言

thinkphp5.0.9的一个比较鸡肋的SQL注入。主要还是借以了解tp中报错注入的原理。
创建:

composer create-project topthink/think=5.0.9 thinkphp5.0.9
    "require": {
    
    
        "php": ">=5.4.0",
        "topthink/framework": "5.0.9"
    },
composer update

index控制器这样写:

<?php
namespace app\index\controller;

class Index
{
    
    
    public function index()
    {
    
    
        $id = input("id/a");
        $data = db("users")->where("id","in",$id)->select();
        dump($data);
    }
}

还是用的sqli-labs的数据库:
在这里插入图片描述

分析

payload:

?id[0,updatexml(0,concat(0x7e,user(),0x7e),0)]=1

不过还是先正常的走一遍流程,?id=1

input方法没啥好说的,直接进入where方法,
在这里插入图片描述
这里就不具体分析了,where之前分析过很多次了,这里基本上就是处理$this->options['where']
在这里插入图片描述
再进入select方法,还是老样子,看一下SQL语句的生成:
在这里插入图片描述

    public function select($options = [])
    {
    
    
        $sql = str_replace(
            ['%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'],
            [
                $this->parseTable($options['table'], $options),
                $this->parseDistinct($options['distinct']),
                $this->parseField($options['field'], $options),
                $this->parseJoin($options['join'], $options),
                $this->parseWhere($options['where'], $options),
                $this->parseGroup($options['group']),
                $this->parseHaving($options['having']),
                $this->parseOrder($options['order'], $options),
                $this->parseLimit($options['limit']),
                $this->parseUnion($options['union']),
                $this->parseLock($options['lock']),
                $this->parseComment($options['comment']),
                $this->parseForce($options['force']),
            ], $this->selectSql);
        return $sql;
    }

还是熟悉的替换,进入parseWhere()方法:

    /**
     * where分析
     * @access protected
     * @param mixed $where   查询条件
     * @param array $options 查询参数
     * @return string
     */
    protected function parseWhere($where, $options)
    {
    
    
        $whereStr = $this->buildWhere($where, $options);
        if (!empty($options['soft_delete'])) {
    
    
            // 附加软删除条件
            list($field, $condition) = $options['soft_delete'];

            $binds    = $this->query->getFieldsBind($options);
            $whereStr = $whereStr ? '( ' . $whereStr . ' ) AND ' : '';
            $whereStr = $whereStr . $this->parseWhereItem($field, $condition, '', $options, $binds);
        }
        return empty($whereStr) ? '' : ' WHERE ' . $whereStr;
    }

where语句在buildWhere中生成,然后在前面拼接上"WHERE"这个字符串。跟进buildWhere()方法:

/**
 * 生成查询条件SQL
 * @access public
 * @param mixed     $where
 * @param array     $options
 * @return string
 */
public function buildWhere($where, $options)
{
    
    
    if (empty($where)) {
    
    
        $where = [];
    }

    if ($where instanceof Query) {
    
    
        return $this->buildWhere($where->getOptions('where'), $options);
    }

    $whereStr = '';
    $binds    = $this->query->getFieldsBind($options);
    foreach ($where as $key => $val) {
    
    
        $str = [];
        foreach ($val as $field => $value) {
    
    
            if ($value instanceof \Closure) {
    
    
                // 使用闭包查询
                $query = new Query($this->connection);
                call_user_func_array($value, [ & $query]);
                $whereClause = $this->buildWhere($query->getOptions('where'), $options);
                if (!empty($whereClause)) {
    
    
                    $str[] = ' ' . $key . ' ( ' . $whereClause . ' )';
                }
            } elseif (strpos($field, '|')) {
    
    
                // 不同字段使用相同查询条件(OR)
                $array = explode('|', $field);
                $item  = [];
                foreach ($array as $k) {
    
    
                    $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
                }
                $str[] = ' ' . $key . ' ( ' . implode(' OR ', $item) . ' )';
            } elseif (strpos($field, '&')) {
    
    
                // 不同字段使用相同查询条件(AND)
                $array = explode('&', $field);
                $item  = [];
                foreach ($array as $k) {
    
    
                    $item[] = $this->parseWhereItem($k, $value, '', $options, $binds);
                }
                $str[] = ' ' . $key . ' ( ' . implode(' AND ', $item) . ' )';
            } else {
    
    
                // 对字段使用表达式查询
                $field = is_string($field) ? $field : '';
                $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);
            }
        }

        $whereStr .= empty($whereStr) ? substr(implode(' ', $str), strlen($key) + 1) : implode(' ', $str);
    }

    return $whereStr;
}

前面的if都进不去,最后是执行这里:

$field = is_string($field) ? $field : '';
$str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);

进行了字符串的拼接,$key是"AND",但是注意到了最后的substr还会把这个前面的"AND"给去掉。跟进一下parseWhereItem()方法,一路上一些琐碎的操作就不说了,主要是这里:

} 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) . ')';
    }
}

首先$exp是in,满足条件进入if。然后foreach遍历$value,然后拼接出一个$bindKey$bindNamewhere_id$bindKeywhere_id_in_0
这部分的操作基本就是这些:

$bindKey = $bindName . '_in_' . $k;

$array[]        = ':' . $bindKey;

$zone = implode(',', $array);

$whereStr .= $key . ' ' . $exp . ' (' . (empty($zone) ? "''" : $zone) . ')';

这里$key是0,因为传入的id被处理成数组了,0是键。然后就相当于直接拼接到了预编译参数的后面,产生这样的$whereStr

`id` IN (:where_id_in_0)

SQL语句如下:

SELECT * FROM `users` WHERE  `id` IN (:where_id_in_0) 

因此问题就很明显了,就是:where_id_in_的后面,直接拼接我们传入的id的键,并没有进行过滤,因此构造:

?id[0,updatexml(1,concat(0x7e,database(),0x7e),1)]=1

在这里插入图片描述
但是复现这个漏洞的关键不在这里。产生SQL语句之后继续跟进,在这里进行SQL语句的执行:
在这里插入图片描述
再不断跟进,进入到最终的query方法:

public function query($sql, $bind = [], $master = false, $pdo = false)
    {
    
    
        $this->initConnect($master);
        if (!$this->linkID) {
    
    
            return false;
        }

        // 记录SQL语句
        $this->queryStr = $sql;
        if ($bind) {
    
    
            $this->bind = $bind;
        }

        // 释放前次的查询结果
        if (!empty($this->PDOStatement)) {
    
    
            $this->free();
        }

        Db::$queryTimes++;
        try {
    
    
            // 调试开始
            $this->debug(true);
            // 预处理
            if (empty($this->PDOStatement)) {
    
    
                $this->PDOStatement = $this->linkID->prepare($sql);
            }
            // 是否为存储过程调用
            $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);
            // 参数绑定
            if ($procedure) {
    
    
                $this->bindParam($bind);
            } else {
    
    
                $this->bindValue($bind);
            }
            // 执行查询
            $this->PDOStatement->execute();
            // 调试结束
            $this->debug(false);
            // 返回结果集
            return $this->getResult($pdo, $procedure);
        } catch (\PDOException $e) {
    
    
            if ($this->isBreak($e)) {
    
    
                return $this->close()->query($sql, $bind, $master, $pdo);
            }
            throw new PDOException($e, $this->config, $this->getLastsql());
        } catch (\ErrorException $e) {
    
    
            if ($this->isBreak($e)) {
    
    
                return $this->close()->query($sql, $bind, $master, $pdo);
            }
            throw $e;
        }
    }

比较熟悉的,之前都分析过很多次了,关键在这里:
在这里插入图片描述
执行到这里的时候,就会抛出异常了,只是停留在了prepare这里。
预编译有三步:

  1. prepare($SQL) 编译SQL语句
  2. bindValue($param, $value) 将value绑定到param的位置上
  3. execute() 执行

我本来理解的是SQL语句是执行应该是在第三步,在执行的时候报错,但实际上只执行到prepare的时候就抛出了异常,这样SQL语句应该是没有执行的,怎么会出现报错注入呢?

下面参考了p神的文章,放一下p神的例子:

<?php
$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

$db = new PDO('mysql:dbname=security;host=127.0.0.1;', 'root', 'root', $params);

try {
    
    
    $link = $db->prepare('SELECT * FROM users WHERE id in (:where_id, updatexml(0,concat(0x7e,user(),0x7e),0))');
} catch (\PDOException $e) {
    
    
    var_dump($e);
}

虽然抛出了异常,但是仍然成功执行了SQL语句,var_dump出了结果。
在这里插入图片描述
原因就是这个:

$params = [
    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES  => false,
];

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的默认设置确实是false:
在这里插入图片描述
因此在第一步prepare的时候就会与mysql进行交互,当时报错注入。

但是这样的SQL注入鸡肋就鸡肋在只能报错出user(),database()这样的,子查询都不行:
在这里插入图片描述

但是,如果你将user()改成一个子查询语句,那么结果又会爆出Invalid parameter number: parameter was not defined的错误。因为没有过多研究,说一下我猜测:预编译的确是mysql服务端进行的,但是预编译的过程是不接触数据的 ,也就是说不会从表中将真实数据取出来,所以使用子查询的情况下不会触发报错;虽然预编译的过程不接触数据,但类似user()这样的数据库函数的值还是将会编译进SQL语句,所以这里执行并爆了出来。

所以这种SQL注入略显鸡肋,不过还是学习了一下思路,以及PDO的这个ATTR_EMULATE_PREPARES的问题。

修复

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
取消了对键的处理,改成对值得处理,而且拼接用得时$i这个计数,最后拼接得预处理也只能是:where_id_in_1,:where_id_in_1这样的了。

猜你喜欢

转载自blog.csdn.net/rfrder/article/details/114629971