前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >听说thinkphp又出事了?

听说thinkphp又出事了?

作者头像
安恒网络空间安全讲武堂
发布2018-04-18 16:53:10
8360
发布2018-04-18 16:53:10
举报
文章被收录于专栏:安恒网络空间安全讲武堂
0x01 前言

听说thinkphp又出事了,之前看过一次tp5的源码,不过只看了查询(select)的过程,这次问题出在update和insert中,但是归根结底还是fileExp()这个函数出了问题,分析过程有部分重复,建议配合前文食用:)

0x02 准备工作

根据payload

name[0]=dec&name[1]=1 and (extractvalue(1,concat(1,(user()))))#&name[2]=1

dec是thinkphp实现的一个sql的表达式,和之前的not like性质类似。

dec这个表达式是在5.0.13新实现的,该漏洞在5.0.16被修复,所以影响范围也在这之间。

测试代码:

application\index\controller\Index.php

<?php

namespace app\index\controller;

class Index

{

public function index()

{

$username = input('post.name/a');

db('user')->where(['id'=> 1])->update(['username'=>$username]);

}

}

0x03 分析

3.1 input()

这个函数可以看之前的文章Thinkphp5实现安全数据库操作以及部分运行流程分析的3.1部分,流程是相同的。

3.2 update()

直接定位到这个漏洞关键点,thinkphp/library/think/db/Builder.php的update函数。

具体定位过程可以看之前文章的3.2部分。

/**

* 生成update SQL

* @access public

* @param array $data 数据

* @param array $options 表达式

* @return string

*/

public function update($data, $options)

{

$table = $this->parseTable($options['table'], $options);

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

}

问题出在更新数据处,跟进parseData函数。

3.2.1 parseData()

/**

* 数据分析

* @access protected

* @param array $data 数据

* @param array $options 查询参数

* @return array

* @throws Exception

*/

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)) {

if ($options['strict']) {

throw new Exception('fields not exists:[' . $key . ']');

}

} elseif (is_null($val)) {

$result[$item] = 'NULL';

/*******这里是重点************/

} elseif (is_array($val)) {

switch ($val[0]) {

case 'exp':

$result[$item] = $val[1];

break;

case 'inc':

$result[$item] = $this->parseKey($val[1]) . '+' . $val[2];

break;

case 'dec':

$result[$item] = $this->parseKey($val[1]) . '-' . $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;

}

可以看到当$val是数组时,会将其第一个元素取出来作为表达式。这里有exp、des、inc三个表达式,但是exp已经被过滤了(filterExp里的第一个。。。)。第二个元素进入了parseKey函数,第三个元素没有任何操作直接拼接。跟进parseKey()看一下:

3.2.2 parseKey()

/**

* 字段名分析

* @access protected

* @param string $key

* @param array $options

* @return string

*/

protected function parseKey($key, $options = [])

{

return $key;

}

emmmm,不要慌。thinkphp/library/think/db/Builder.php只是一个抽象类,具体实现在这里

thinkphp/library/think/db/builder/Mysql.php

/**

* 字段和表名处理

* @access protected

* @param string $key

* @param array $options

* @return string

*/

protected function parseKey($key, $options = [])

{

$key = trim($key);

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;

}

并没有什么过滤,不过在某些情况下会用反引号把字段的值包起来,可能会影响payload的构造。

经过这么一系列操作构造出来的sql语句像下面这样:

update `user` set username=1 #name[1] 可控

- #name[0] dec => -

1 #name[2] 可控

有两处可控,约等于为所欲为。

3.3 insert()

thinkphp/library/think/db/Builder.php构造insert语句的时候同样使用了parseData()函数,也存在问题。

/**

* 生成insert SQL

* @access public

* @param array $data 数据

* @param array $options 表达式

* @param bool $replace 是否replace

* @return string

*/

public function insert(array $data, $options = [], $replace = false)

{

// 分析并处理数据

$data = $this->parseData($data, $options);

}

0x04 防御

thinkphp在5.0.16版本修复了这个漏洞,并没有在filterExp函数中增加过滤,补丁如下:

elseif (is_array($val) && !empty($val)) {

switch ($val[0]) {

case 'exp':

$result[$item] = $val[1];

break;

case 'inc':

if ($key == $val[1]) {

$result[$item] = $this->parseKey($val[1]) . '+' . floatval($val[2]);

}

break;

case 'dec':

if ($key == $val[1]) {

$result[$item] = $this->parseKey($val[1]) . '-' . floatval($val[2]);

}

break;

}

}

加了一个判断,保证$val[1]为对应的字段名,并且把$val[2]进行了强制类型转换。

最后说一句,漏洞的利用是建立在thinkphp**接受数组形式的参数**的基础上,虽然这种写法在查询中非常少见,但是在更新、插入的时候还是有相应的需求的。总而言之,一切用户输入都是有害的,还是那句话,框架只是简化开发的一种工具,并不能把应用安全全部交给框架来处理。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-04-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 恒星EDU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档