听说thinkphp又出事了?

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

原文发布于微信公众号 - 安恒网络空间安全讲武堂(gh_fa1e45032807)

原文发表时间:2018-04-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏耕耘实录

PHP学习笔记(2)--PHP数据库操作基本知识

11650
来自专栏JAVA后端开发

通用数据级别权限的框架设计与实现(3)-数据列表的权限过滤

查看上篇文章通用数据级别权限的框架设计与实现(2)-数据权限的准备工作,我们开始数据列表的权限过滤. 原理:我们在做过滤列表时,根据用户权限自动注入到相关SQ...

1.5K50
来自专栏有趣的django

SQLAlchemy SQLAlchemy

51900
来自专栏

C++实现线程安全的单例模式

在某些应用环境下面,一个类只允许有一个实例,这就是著名的单例模式。单例模式分为懒汉模式,跟饿汉模式两种。 首先给出饿汉模式的实现 template <class...

23070
来自专栏JavaEdge

单例模式(Singleton Pattern)百媚生1 动机2 定义结构分析优点缺点适用场景应用总结实现方式1、懒汉式(非线程安全)2、懒汉式(线程安全)4、双重检验锁模式(double checke

397100
来自专栏乐沙弥的世界

mongoDB 文档查询

1、文档查询db.users.find()等价于db.users.find( {} ) 2、基于and运算符的多个组合条件可以省略and运算符的多个组合条件...

21510
来自专栏JavaEE

mybatis-plus的使用 ------ 入门

mybatis在持久层框架中还是比较火的,一般项目都是基于ssm。虽然mybatis可以直接在xml中通过SQL语句操作数据库,很是灵活。但正其操作都要通过SQ...

4.3K40
来自专栏跟着阿笨一起玩NET

C#设计模式学习笔记-单例模式

  最近在学设计模式,学到创建型模式的时候,碰到单例模式(或叫单件模式),现在整理一下笔记。

9820
来自专栏happyJared

设计模式入门:单例模式

  单例模式属于创建型模式,是一种较为简单的设计模式,但也是最容易让人犯错的。在不同的单例模式实现中,首先要确保构造函数是私有的,然后提供一个静态入口(方法)用...

16420
来自专栏静默虚空的博客

[设计模式]单例模式

简介 单例模式(Singleton Pattern)保证一个类只有一个实例,并提供一个访问它的全局访问点。 单例模式是一种对象创建型模式 (可参考 设计模式 ...

21790

扫码关注云+社区

领取腾讯云代金券