20.Ecshop 2.x/3.x SQL註入/任意代碼執行漏洞(附實戰exp)
Ecshop 2.x/3.x SQL註入/任意代碼執行漏洞
影響版本:
Ecshop 2.x
Ecshop 3.x-3.6.0
漏洞分析:
該漏洞影響ECShop 2.x和3.x版本,是一個典型的“二次漏洞”,通過user.php文件中display()函數的模板變量可控,從而造成SQL註入漏洞,而後又通過SQL註入漏洞將惡意代碼註入到危險函數eval中,從而實現了任意代碼執行。
值得一提的是攻擊者利用的payload只適用於ECShop 2.x版本導致有部分安全分析者認為該漏洞不影響ECShop 3.x,這個是因為在3.x的版本裏有引入防註入攻擊的安全代碼,通過我們分析發現該防禦代碼完全可以繞過實現對ECShop 3.x的攻擊(詳見下文分析)。
註:以下代碼分析基於ECShop 2.7.3
SQL註入漏洞分析:
首先我們看一下漏洞的起源點 user.php ,在用戶login這裏有一段代碼:
/* 用戶登錄界面 */
elseif ($action == ‘login‘)
{
if (empty($back_act))
{
if (empty($back_act) && isset($GLOBALS[‘_SERVER‘][‘HTTP_REFERER‘]))
{
$back_act = strpos($GLOBALS[‘_SERVER‘][‘HTTP_REFERER‘], ‘user.php‘) ? ‘./index.php‘ : $GLOBALS[‘_SERVER‘][‘HTTP_REFERER‘ ];
}
else
{
$back_act = ‘user.php‘;
}
}
$captcha = intval($_CFG[‘captcha‘]);
if (($captcha & CAPTCHA_LOGIN) && (!($captcha & CAPTCHA_LOGIN_FAIL) || (($captcha & CAPTCHA_LOGIN_FAIL) && $_SESSION[‘login_fail‘] > 2 )) && gd_version() > 0)
{
$GLOBALS[‘smarty‘]->assign(‘enabled_captcha‘, 1);
$GLOBALS[‘smarty‘]->assign(‘rand‘, mt_rand());
}
$smarty->assign(‘back_act‘, $back_act);
$smarty->display(‘user_passport.dwt‘);
}
Ecshop使用了php模版引擎smarty,該引擎有兩個基本的函數assign()、display()。assign()函數用於在模版執行時為模版變量賦值,display()函數用於顯示模版。
smarty運行時,會讀取模版文件,將模版文件中的占位符替換成assign()函數傳遞過來的參數值,並輸出一個編譯處理後的php文件,交由服務器運行。
可以看到 $back_act 是從 $GLOBALS[‘_SERVER‘][‘HTTP_REFERER‘] 獲取到的,HTTP_REFERER 是外部可控的參數,這也是我們註入payload的源頭。
在模版執行時,assign()把 $back_act 賦值給模版變量 back_act ,下面我們跟進這個模版變量到 /includes/cls_template.php :
/**
* 註冊變量
*
* @access public
* @param mix $tpl_var
* @param mix $value
*
* @return void
*/
function assign($tpl_var, $value = ‘‘)
{
if (is_array($tpl_var))
{
foreach ($tpl_var AS $key => $val)
{
if ($key != ‘‘)
{
$this->_var[$key] = $val;
}
}
}
else
{
if ($tpl_var != ‘‘)
{
$this->_var[$tpl_var] = $value;
}
}
}
從上面的assign()函數,我們看出 $tpl_var 接受我們傳過來的參數 $back_act ,但不是個數組,所以 $this ->_var[$back_act] = $back_act ,而後調用display()函數來顯示模板,在includes/init.php文件中創建了Smarty對象cls_template來處理模版文件,對應的文件是 includes/cla_template.php:
function display($filename, $cache_id = ‘‘)
{
$this->_seterror++;
error_reporting(E_ALL ^ E_NOTICE);
$this->_checkfile = false;
$out = $this->fetch($filename, $cache_id);
if (strpos($out, $this->_echash) !== false)
{
$k = explode($this->_echash, $out);
foreach ($k AS $key => $val)
{
if (($key % 2) == 1)
{
$k[$key] = $this->insert_mod($val);
}
}
$out = implode(‘‘, $k);
}
error_reporting($this->_errorlevel);
$this->_seterror--;
echo $out;
}
從user.php調用display()函數,傳遞進來的$filename是user_passport.dwt,從函數來看,首先會調用 $this->fetch 來處理 user_passport.dwt 模板文件,fetch函數中會用 $this->make_compiled 來編譯模板。user_passport.dwt其中一段如下:
<td align="left">
<input type="hidden" name="act" value="act_login" />
<input type="hidden" name="back_act" value="{$back_act}" />
<input type="submit" name="submit" value="" class="us_Submit" />
make_compiled 會將模板中的變量解析,也就是在這個時候將上面assign中註冊到的變量 $back_act 傳遞進去了,解析完變量之後返回到display()函數中。此時$out是解析變量後的html內容,後面的代碼是判斷 $this->_echash 是否在 $out 中,如果存在的話,使用 $this->_echash 來分割內容,得到$k然後交給insert_mod處理,我們來查找 _echash 的值:
發現 _echash 是個默認的值,不是隨機生成的,所以 $val 內容可隨意控制。跟進$this->insert_mod:
function insert_mod($name) // 處理動態內容
{
list($fun, $para) = explode(‘|‘, $name);
$para = unserialize($para);
$fun = ‘insert_‘ . $fun;
return $fun($para);
}
$val傳遞進來,先用 | 分割,得到 $fun 和 $para,$para進行反序列操作,$fun 和 insert_ 拼接,最後動態調用 $fun($para),函數名部分可控,參數完全可控。接下來就是尋找以 insert_ 開頭的可利用的函數了,在 /includes/lib_insert.php 有一個 insert_ads() 函數,正好滿足要求。看下 insert_ads() 函數:
function insert_ads($arr)
{
static $static_res = NULL;
$time = gmtime();
if (!empty($arr[‘num‘]) && $arr[‘num‘] != 1)
{
$sql = ‘SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ‘ .
‘p.ad_height, p.position_style, RAND() AS rnd ‘ .
‘FROM ‘ . $GLOBALS[‘ecs‘]->table(‘ad‘) . ‘ AS a ‘.
‘LEFT JOIN ‘ . $GLOBALS[‘ecs‘]->table(‘ad_position‘) . ‘ AS p ON a.position_id = p.position_id ‘ .
"WHERE enabled = 1 AND start_time <= ‘" . $time . "‘ AND end_time >= ‘" . $time . "‘ ".
"AND a.position_id = ‘" . $arr[‘id‘] . "‘ " .
‘ORDER BY rnd LIMIT ‘ . $arr[‘num‘];
$res = $GLOBALS[‘db‘]->GetAll($sql);
}
else
{
if ($static_res[$arr[‘id‘]] === NULL)
{
$sql = ‘SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ‘.
‘p.ad_height, p.position_style, RAND() AS rnd ‘ .
‘FROM ‘ . $GLOBALS[‘ecs‘]->table(‘ad‘) . ‘ AS a ‘.
‘LEFT JOIN ‘ . $GLOBALS[‘ecs‘]->table(‘ad_position‘) . ‘ AS p ON a.position_id = p.position_id ‘ .
"WHERE enabled = 1 AND a.position_id = ‘" . $arr[‘id‘] .
"‘ AND start_time <= ‘" . $time . "‘ AND end_time >= ‘" . $time . "‘ " .
‘ORDER BY rnd LIMIT 1‘;
$static_res[$arr[‘id‘]] = $GLOBALS[‘db‘]->GetAll($sql);
}
$res = $static_res[$arr[‘id‘]];
}
$ads = array();
$position_style = ‘‘;
20.Ecshop 2.x/3.x SQL註入/任意代碼執行漏洞(附實戰exp)