ThinkPHP最新版本SQL注入漏洞


如下controller即可觸發SQL注入:

code 區域

public function test()

{

    $uname = I('get.uname');

    $u = M('user')->where(array(

        'uname' => $uname

    ))->find();

    dump($u);

}



為什么?

我們看看代碼。我從github下載的最新源碼:https://github.com/liu21st/thinkphp

/ThinkPHP/Library/Think/Db/Driver.class.php 531
行:

code 區域

// where子單元分析

protected function parseWhereItem($key,$val) {

$whereStr = '';

if(is_array($val)) {

if(is_string($val[0])) {

if(preg_match('/^(EQ|NEQ|GT|EGT|LT|ELT)$/i',$val[0])) { // 比較運算

$whereStr .= $key.' '.$this->comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);

}elseif(preg_match('/^(NOTLIKE|LIKE)$/i',$val[0])){// 模糊查找

if(is_array($val[1])) {

$likeLogic = isset($val[2])?strtoupper($val[2]):'OR';

if(in_array($likeLogic,array('AND','OR','XOR'))){

$likeStr = $this->comparison[strtolower($val[0])];

$like = array();

foreach ($val[1] as $item){

$like[] = $key.' '.$likeStr.' '.$this->parseValue($item);

}

$whereStr .= '('.implode(' '.$likeLogic.' ',$like).')';

}

}else{

$whereStr .= $key.' '.$this->comparison[strtolower($val[0])].' '.$this->parseValue($val[1]);

}

}elseif('bind'==strtolower($val[0])){ // 使用表達式

$whereStr .= $key.' = :'.$val[1];

}elseif('exp'==strtolower($val[0])){ // 使用表達式

$whereStr .= $key.' '.$val[1];

}elseif(preg_match('/IN/i',$val[0])){ // IN 運算

if(isset($val[2]) && 'exp'==$val[2]) {

$whereStr .= $key.' '.strtoupper($val[0]).' '.$val[1];

}else{

if(is_string($val[1])) {

$val[1] = explode(',',$val[1]);

}

$zone = implode(',',$this->parseValue($val[1]));

$whereStr .= $key.' '.strtoupper($val[0]).' ('.$zone.')';

}

}elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN運算

$data = is_string($val[1])? explode(',',$val[1]):$val[1];

$whereStr .= $key.' '.strtoupper($val[0]).' '.$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;

}



這就是處理where條件的函數,我們看到如下片段:

code 區域

}elseif(preg_match('/BETWEEN/i',$val[0])){ // BETWEEN運算

$data = is_string($val[1])? explode(',',$val[1]):$val[1];

$whereStr .= $key.' '.strtoupper($val[0]).' '.$this->parseValue($data[0]).' AND '.$this->parseValue($data[1]);

}



當匹配/BETWEEN/i$val[0]時,則將strtoupper($val[0])直接插入了SQL語句。

這個匹配:preg_match('/BETWEEN/i',$val[0]),明顯是有問題的。因為這個匹配沒加^$也就是首尾限定,所以只要我們的$val[0]中含有between時,這個匹配就可以成立,就產生了一個SQL注入。

為了防止I函數對我們輸入的過濾影響,我們看看I函數:

code 區域

function I($name,$default='',$filter=null,$datas=null) {

    if(strpos($name,'/')){ // 指定修飾符

        list($name,$type)     =    explode('/',$name,2);

    }

if(strpos($name,'.')) { // 指定參數來源

list($method,$name) = explode('.',$name,2);

}else{ // 默認為自動判斷

$method = 'param';

}

switch(strtolower($method)) {

case 'get' : $input =& $_GET;break;

case 'post' : $input =& $_POST;break;

case 'put' : parse_str(file_get_contents('php://input'), $input);break;

case 'param' :

switch($_SERVER['REQUEST_METHOD']) {

case 'POST':

$input = $_POST;

break;

case 'PUT':

parse_str(file_get_contents('php://input'), $input);

break;

default:

$input = $_GET;

}

break;

case 'path' :

$input = array();

if(!empty($_SERVER['PATH_INFO'])){

$depr = C('URL_PATHINFO_DEPR');

$input = explode($depr,trim($_SERVER['PATH_INFO'],$depr));

}

break;

case 'request' : $input =& $_REQUEST; break;

case 'session' : $input =& $_SESSION; break;

case 'cookie' : $input =& $_COOKIE; break;

case 'server' : $input =& $_SERVER; break;

case 'globals' : $input =& $GLOBALS; break;

case 'data' : $input =& $datas; break;

default:

return NULL;

}

if(''==$name) { // 獲取全部變量

$data = $input;

$filters = isset($filter)?$filter:C('DEFAULT_FILTER');

if($filters) {

if(is_string($filters)){

$filters = explode(',',$filters);

}

foreach($filters as $filter){

$data = array_map_recursive($filter,$data); // 參數過濾

}

}

}elseif(isset($input[$name])) { // 取值操作

$data = $input[$name];

$filters = isset($filter)?$filter:C('DEFAULT_FILTER');

if($filters) {

if(is_string($filters)){

$filters = explode(',',$filters);

}elseif(is_int($filters)){

$filters = array($filters);

}

 

foreach($filters as $filter){

if(function_exists($filter)) {

$data = is_array($data) ? array_map_recursive($filter,$data) : $filter($data); // 參數過濾

}elseif(0===strpos($filter,'/')){

    // 支持正則驗證

    if(1 !== preg_match($filter,(string)$data)){

        return isset($default) ? $default : NULL;

    }

}else{

$data = filter_var($data,is_int($filter) ? $filter : filter_id($filter));

if(false === $data) {

return isset($default) ? $default : NULL;

}

}

}

}

if(!empty($type)){

    switch(strtolower($type)){

        case 's': // 字符串

            $data     =    (string)$data;

            break;

        case 'a':    // 數組

            $data     =    (array)$data;

            break;

        case 'd':    // 數字

            $data     =    (int)$data;

            break;

        case 'f':    // 浮點

            $data     =    (float)$data;

            break;

        case 'b':    // 布爾

            $data     =    (boolean)$data;

            break;

    }

}

}else{ // 變量默認值

$data = isset($default)?$default:NULL;

}

is_array($data) && array_walk_recursive($data,'think_filter');

return $data;

}



較前些版本有些改進:

1.
加了類型強制轉換$type,但在默認情況下$type是空的,強制類型轉換是不存在的。

2.
is_array($data) && array_walk_recursive($data,'think_filter');放在最后一行。我們看看think_filter這個過濾函數:

code 區域

function think_filter(&$value){

    // TODO 其他安全過濾

 

    // 過濾查詢特殊字符

if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|LIKE|NOTLIKE|BETWEEN|IN)$/i',$value)){

$value .= ' ';

}

}



這個實際上就是對我之前那個漏洞的一個解決方案,將一些關鍵詞后面加空格。但我們看到,這個正則是存在"^$"首尾限定符的。所以只有傳入參數完全"等於"BETWEEN的時候才會被加上空格,而且這里加上空格也不會影響漏洞的產生,因為漏洞位置的正則沒有加^$首尾限定符。



還有一個說明:之前thinkphp出了個"錯誤"的補丁,這個補丁已經被官方去掉了,所以不用考慮那個補丁造成的一些干擾。

漏洞證明:

那我們回到最初那段代碼:

code 區域

public function test()

{

    $uname = I('get.uname');

    $u = M('user')->where(array(

        'uname' => $uname

    ))->find();

    dump($u);

}



這個代碼,我們來測試一下:



果然是有漏洞的,我們看看具體執行的SQL語句:



就在BETWEEN處。除了BETWEEN外還有IN,我就一塊說明了。

Onethink
演示:



細我就不說了,和之前一樣。

修復方案:

正則一定要寫明確:

/^BETWEEN$/i

版權聲明:轉載請注明來源 phith0n@烏雲


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM