看我是如何利用升級系統一鍵GetShell
i春秋作家:小豬
漏洞名稱:看我是如何利用升級系統一鍵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 |
);
$xml .= "<content>" ;
|
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
函式代表空格轉換成%20
,trim
函式代表移除字串兩側的空白字元。
第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春秋論壇閱讀喲~