如何进行ThinkPHP框架SQL注入

如何进行ThinkPHP框架SQL注入,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

简要描述

ThinkPHP是一个免费开源的,快速、简单的面向对象的轻量级PHP开发框架,是为了敏捷WEB应用开发和简化企业应用开发而诞生的。ThinkPHP从诞生的12年间一直秉承简洁实用的设计原则,在保持出色的性能和至简的代码的同时,也注重易用性。目前ThinkPHP框架是国内使用量最大的框架之一,国内用户量众多。

近日,360企业安全集团代码卫士团队安全研究人员发现该框架V5.1.7-V5.1.8 版本在底层数据处理驱动解析数据的时候存在缺陷,一定场景下,攻击者可以通过构造恶意数据包利用SQL注入的方式获取用户数据库内容。360企业安全集团代码卫士团队已第一时间和ThinkPHP团队进行沟通修复,建议相关用户及时更新官方发布的新版本。

漏洞分析

注:该漏洞ThinkPHP官方团队在报送当天(2018-04-06)紧急进行了修复处理,详细请参考:https://github.com/top-think/framework/commit/39bb0fe6d50ee77e0779f646b10bce08c442a5e3

以下漏洞分析基于ThinkPHP V5.1.8(2018-04-05未更新版)

这里我们主要跟进分析执行update操作的过程。为了方便理解,先直接放出函数的调用栈。

Mysql.php :200, think/db/builder/Mysql-> parseArrayData()

Builder.php :147, think/db/Builder-> parseData()

Builder.php :1139, think/db/Builder-> update()

Connection.php :1149, think/db/Connection-> update()

Query.php :2571, think/db/Query-> update()

Index.php :18, app/index/controller/Index-> testsql()

Container.php :285, ReflectionMethod ->invokeArgs()

Container.php :285, think/Container-> invokeReflectMethod()

Module.php :139, think/route/dispatch/Module-> run()

Url.php :31, think/route/dispatch/Url-> run()

App.php :378, think/App-> think/{closure}()

Middleware.php :119, call_user_func_array:{ C:/wamp64/www/think518/thinkphp/library/think/Middleware. php:119}()

Middleware.php :119, think/Middleware-> think/{closure}()

Middleware.php :74, call_user_func:{ C:/wamp64/www/think518/thinkphp/library/think/Middleware. php:74}()

Middleware.php :74, think/Middleware-> dispatch()

App.php :399, think/App-> run()

index.php :21, { main}()

缺陷关键点为thinkphp解析用户传递过来的Data可控,且可以绕过安全检查。

根据文件 Connection.php:1149,think/db/Connection->update()第1102行update函数分析,这个函数的主要功能是用于执行update SQL语句。

//Connection.php:1149, think/db/Connection->update()
public function update(Query $query)
   {
       $options = $query->getOptions();
       if (isset($options ['cache']) && is_string($options['cache' ]['key'])) {
           $key = $options['cache']['key' ];                    
       }
       $pk   = $query->getPk($options );                    
       $data = $options['data'];
       if (empty($options ['where'])) {
           // 如果存在主键数据 则自动作为更新条件
           if (is_string($pk ) && isset( $data[$pk])) {                            
               $where[ $pk] = [$pk, '=' , $data[$pk]];
               if (!isset($key )) {
                   $key = $this->getCacheKey($query , $data[$pk]);
               }
               unset( $data[$pk]);
           } elseif (is_array($pk )) {
               // 增加复合主键支持
               foreach ($pk as $field ) {
                   if (isset($data [$field])) {
                       $where [$field] = [$field, '=', $data[$field ]];                            
                   } else {
                       // 如果缺少复合主键数据则不执行
                       throw new Exception( 'miss complex primary data');
                   }
                   unset( $data[$field]);
               }
           }
           if (!isset($where )) {
               // 如果没有任何更新条件则不执行
               throw new Exception( 'miss update condition');
           } else {
               $options[ 'where']['AND'] = $where;
               $query-> setOption('where', ['AND' => $where ]);                        
           }
       } elseif (!isset($key ) && is_string( $pk) && isset ($options['where'][ 'AND'][$pk])) {                                    
           $key = $this->getCacheKey($query , $options['where'][ 'AND'][$pk]);
       }
       // 更新数据
       $query-> setOption('data', $data );                    
       // 生成UPDATE SQL语句
       $sql  = $this->builder->update ($query);
       $bind = $query->getBind();
       if (!empty($options ['fetch_sql'])) {
           // 获取实际执行的SQL语句
           return $this->getRealSql($sql , $bind);
       }
       // 检测缓存
       $cache = Container::get( 'cache');
       if (isset($key ) && $cache-> get($key)) {                            
           // 删除缓存
           $cache-> rm($key);
       } elseif (!empty($options ['cache']['tag'])) {
           $cache-> clear($options['cache' ]['tag']);
       }
       // 执行操作
       $result = '' == $sql ? 0 : $this->execute($sql , $bind);
       if ($result) {                    
           if (is_string($pk ) && isset( $where[$pk])) {                            
               $data[ $pk] = $where [$pk];
           } elseif (is_string($pk ) && isset( $key) && strpos ($key, '|' )) {
               list( $a, $val) = explode('|', $key);
               $data[ $pk]     = $val ;                    
           }
           $query-> setOption('data', $data );                    
           $query-> trigger('after_update');
       }
       return $result;
   }

第1146行, $query->setOption('data',$data);这里将用户传递的 $dataset到 $query变量中,为下一步的生成 UPDATE SQL语句做准备,执行 $sql=$this->builder->update($query);语句,重点马上要来了,跟进 Builder.php:1139,think/db/Builder->update()函数

//Builder.php:1139, think/db/Builder->update()
public function update(Query $query)
   {
       $options = $query->getOptions();
       $table = $this->parseTable($query , $options['table']);
       $data  = $this->parseData($query , $options['data']);
       if (empty($data )) {
           return '';
       }
       foreach ($data as $key => $val) {
           $set[] = $key . ' = ' . $val;
       }
       return str_replace(
           [ '%TABLE%', '%SET%', '%JOIN%', '%WHERE%' , '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],
           [
               $this-> parseTable($query, $options ['table']),
               implode( ' , ', $set),
               $this-> parseJoin($query, $options ['join']),
               $this-> parseWhere($query, $options ['where']),
               $this-> parseOrder($query, $options ['order']),
               $this-> parseLimit($query, $options ['limit']),
               $this-> parseLock($query, $options ['lock']),
               $this-> parseComment($query, $options ['comment']),
           ],
           $this-> updateSql);
   }

刚刚我们将用户可控的 $dataset到 $query['options']中,这里我们先获取 $query['options']内容到  $options中,然后对Data进行解析 $data=$this->parseData($query,$options['data']);

//Builder.php:147, think/db/Builder->parseData()
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;
   }

在第115行,通过 foreach($dataas$key=>$val)处理 $data,然后解析 $key保存到  $item变量中去,之后执行下面的判断逻辑,想要合理地进入各个判断分支,就要巧妙的构造 $key和 $value也就是 $data的值。紧接着我们进入漏洞触发点  $value=$this->parseArrayData($query,$val);,跟进函数 $value=$this->parseArrayData($query,$val);

//Mysql.php:200, think/db/builder/Mysql->parseArrayData()
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 . ')/')';//需要简单的构造一下sql语句
               break;
           default:
               $result = false;
       }
       return $result;
   }

这里 $type、 $value和 $data均为可控值,那么函数返回的 $result也就是可控的。回到上一个  Builder.php文件中,将返回的结果赋值到 $result[$item]=$value;中,之后的生成SQL语句和常见的流程没有任何差别不再展开具体分析。

验证截图

如何进行ThinkPHP框架SQL注入    

修复建议

更新受影响ThinkPHP版本到最新版本

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。

原创文章,作者:745907710,如若转载,请注明出处:https://blog.ytso.com/228615.html

(0)
上一篇 2022年1月11日
下一篇 2022年1月11日

相关推荐

发表回复

登录后才能评论