ThinkPHP3.2.3 SQL注入漏洞分析

  • A+

where注入

在控制器写如下demo开始测试

php
public function ghtwf01(){
$data = M('users')->where('id='.I('id'))->find();
dump($data);
}

$data处下断点跟踪调试,F7进入

I方法中判断传参方式

php
switch(strtolower($method)) {
case 'get' :
$input =& $_GET;
break;
case 'post' :
$input =& $_POST;
break;
case 'put' :
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
case 'param' :
switch($_SERVER['REQUEST_METHOD']) {
case 'POST':
$input = $_POST;
break;
case 'PUT':
if(is_null($_PUT)){
parse_str(file_get_contents('php://input'), $_PUT);
}
$input = $_PUT;
break;
default:
$input = $_GET;
}

判断出是GET传参,然后选择一个过滤方式

1
跟进,返回过滤函数htmlspecialchars,然后使用该函数过滤传入的参数

2
这里因为$data不是数组所以没有使用think_filter函数过滤,我们也来看一下think_filter函数的内容

``` php
function think_filter(&$value){
// TODO 其他安全过滤

// 过滤查询特殊字符
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
    $value .= ' ';
}

}
```

并没有过滤updatexml报错注入函数,就算使用了think_filter过滤了,也没办法防御
接下来进入where函数,作用是将$where的值放入options['where']

php
if(isset($this->options['where'])){
$this->options['where'] = array_merge($this->options['where'],$where);
}else{
$this->options['where'] = $where;
}

3
接下来进入find函数

php
$options = $this->_parseOptions($options);

_parseOptions函数的作用是获取到表名和别名

4

继续进入select函数

php
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函数

php
public function buildSelectSql($options=array()) {
if(isset($options['page'])) {
// 根据页数计算limit
list($page,$listRows) = $options['page'];
$page = $page>0 ? $page : 1;
$listRows= $listRows>0 ? $listRows : (is_numeric($options['limit'])?$options['limit']:20);
$offset = $listRows*($page-1);
$options['limit'] = $offset.','.$listRows;
}
$sql = $this->parseSql($this->selectSql,$options);
return $sql;
}

再跟进parseSql方法

php
public function parseSql($sql,$options=array()){
$sql = str_replace(
array('%TABLE%','%DISTINCT%','%FIELD%','%JOIN%','%WHERE%','%GROUP%','%HAVING%','%ORDER%','%LIMIT%','%UNION%','%LOCK%','%COMMENT%','%FORCE%'),
array(
$this->parseTable($options['table']),
$this->parseDistinct(isset($options['distinct'])?$options['distinct']:false),
$this->parseField(!empty($options['field'])?$options['field']:'*'),
$this->parseJoin(!empty($options['join'])?$options['join']:''),
$this->parseWhere(!empty($options['where'])?$options['where']:''),
$this->parseGroup(!empty($options['group'])?$options['group']:''),
$this->parseHaving(!empty($options['having'])?$options['having']:''),
$this->parseOrder(!empty($options['order'])?$options['order']:''),
$this->parseLimit(!empty($options['limit'])?$options['limit']:''),
$this->parseUnion(!empty($options['union'])?$options['union']:''),
$this->parseLock(isset($options['lock'])?$options['lock']:false),
$this->parseComment(!empty($options['comment'])?$options['comment']:''),
$this->parseForce(!empty($options['force'])?$options['force']:'')
),$sql);
return $sql;
}

这里主要看parseWhere方法

php
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;
}

跟进parseThinkWhere方法

php
protected function parseThinkWhere($key,$val) {
$whereStr = '';
switch($key) {
case '_string':
// 字符串模式查询条件
$whereStr = $val;
break;
case '_complex':
// 复合查询条件
$whereStr = substr($this->parseWhere($val),6);
break;
case '_query':
// 字符串模式查询条件
parse_str($val,$where);
if(isset($where['_logic'])) {
$op = ' '.strtoupper($where['_logic']).' ';
unset($where['_logic']);
}else{
$op = ' AND ';
}
$array = array();
foreach ($where as $field=>$data)
$array[] = $this->parseKey($field).' = '.$this->parseValue($data);
$whereStr = implode($op,$array);
break;
}
return '( '.$whereStr.' )';
}

因为$key=__string,所以直接$whereStr = $val,然后break
最后返回完整的SQL语句

5
然后执行

6
成功注入

find/select/delete注入

三种注入方法都一样,这里以find注入为例
在控制器写如下demo开始测试

php
public function ghtwf01(){
$data = M('users')->find(I('id'));
dump($data);
}

$data处下断点跟踪调试,F7进入
同样的I方法判断传参方式,因为传入的是数组,所以这次会使用think_filter函数过滤,但是没有过滤报错函数,所以导致过滤没用
然后同样的在find方法里面跟进_parseOptions方法,因为$options['where']String,所以不会执行if里面的_parseType方法

php
if(isset($options['where']) && is_array($options['where']) && !empty($fields) && !isset($options['join'])) {
// 对数组查询条件进行字段类型检查
foreach ($options['where'] as $key=>$val){
$key = trim($key);
if(in_array($key,$fields,true)){
if(is_scalar($val)) {
$this->_parseType($options['where'],$key);
}
......

继续看一看_parseType方法有什么过滤呢

php
protected function _parseType(&$data,$key) {
if(!isset($this->options['bind'][':'.$key]) && isset($this->fields['_type'][$key])){
$fieldType = strtolower($this->fields['_type'][$key]);
if(false !== strpos($fieldType,'enum')){
// 支持ENUM类型优先检测
}elseif(false === strpos($fieldType,'bigint') && false !== strpos($fieldType,'int')) {
$data[$key] = intval($data[$key]);
}elseif(false !== strpos($fieldType,'float') || false !== strpos($fieldType,'double')){
$data[$key] = floatval($data[$key]);
}elseif(false !== strpos($fieldType,'bool')){
$data[$key] = (bool)$data[$key];
}
}
}

使用$data[$key] = intval($data[$key]);进行了强制int,导致无法注入

回到刚才的分析中,进入select->buildSelectSql->parseSql->parseWhere

7

但是如果直接传入

?id=1 and 1=updatexml(1,concat(0x7e,(database()),0x7e),1)

那么就会触发_parseOptions里面的_parseType方法,导致注入失败
因为如果这样,这儿的$options['where']是一个数组,满足执行_parseType的条件,但是不满足in_array($key,$fields,true),还是进不去_parseType函数,但是就能逃过吗,后面执行了unset函数,删除了$options['where']['id'],注入语句都没有,也就没法注入了

梳理一下find注入,关键代码位于find方法

php
public function find($options=array()) {
if(is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;
$options = array();
$options['where'] = $where;
}
......

如果传入的是id['where'],那么$options['where']就是一个字符串

9
不会进行后续处理

如果传入的是id,那么$options就会变成一个二维数组,$options['where'][id]才是字符串,$options['where']是一个数组

php
public function find($options=array()) {//$options = String
if(is_numeric($options) || is_string($options)) {
$where[$this->getPk()] = $options;//$where['id'] = String
$options = array();
$options['where'] = $where;//$options['where'] = {"id":String}
}

相关推荐: att&ck之CMSTP

前言 att&ck是一个知识大矩阵,本文是抠出矩阵中一个小知识点展开讲解。 CMSTP Microsoft连接管理器配置文件安装程序(CMSTP.exe)是用于安装连接管理器服务配置文件的命令行程序。CMSTP.exe接受安装信息文件(INF)作为参数…