<bdo id="2b3yk"><code id="2b3yk"></code></bdo>

  • <output id="2b3yk"><sup id="2b3yk"></sup></output>
    <output id="2b3yk"><ruby id="2b3yk"></ruby></output>
  • <code id="2b3yk"><delect id="2b3yk"></delect></code>

  • 從工具的代碼角度學安全:Sublist3r子域名枚舉神器源碼閱讀

    2018-09-23 95278人圍觀 ,發現 2 個不明物體 工具

    *本文作者:mscb,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。

    頁首配圖

    前言

    Sublist3r是一款簡單易用的子域名枚舉利器。但是今天我不打算講解它的使用教程,而是深入一層,從它的代碼中窺探原理。也許有人可能很不理解,為什么不好好的使用工具而偏偏去深究它的代碼實現細節。

    從我個人角度來說,我覺得:

    第一個層面,一位優秀的安全從業者,至少在讀/寫代碼層面不能成為短柄。如果一味的只使用工具,不了解原理,那很容易成為大家口中的“腳本小子(script kiddie)”。

    第二個層面,這是我個人推崇的的學習方法,從自己的學習道路中,我喜歡遵循3個W的學習原則,即(What->Why->What):

    第一個What表示在初學階段,需要學會哪些東西干些什么,比如知道了Sublist3r工具可以用來枚舉子域名。

    第二個Why則表示在中級階段,去了解這個東西的實現原理,以及為什么要這么實現,比如通過工具的代碼學會它的底層細節,與此同時還能豐富知識體系。

    第三個What表示在高級階段,知道要干什么事情需要什么工具,不在細究技術細節,而是通過前期的學習,形成自己的知識網絡,能精準的根據當前情況定位到具體的工具。

    Sublist3r工具介紹

    從Sublist3r的GitHub主頁(https://github.com/aboul3la/Sublist3r)上,我們可以了解到這是一款基于python開發使用OSINT技術的子域名枚舉工具。幫助滲透測試者和bug捕獲者收集目標域名的子域名。

    什么是OSINT

    OSINT是英文名“Open-source intelligence”的縮寫,中文名稱叫“公開來源情報”。從公眾可見的信息中,查找所需目標的信息。說的通俗易懂一點,就是我想知道某某東西的詳細信息,我在公開的信息里查找、檢索,找到有關這個東西的任何內容都提取出來,最后將這些內容匯總,得到較為詳細的信息。再說的通俗易懂一點就是:查資料。再再通俗一點就是:找百度。最終目標信息的準確度、詳細程度均依賴于查找源。所以Sublist3r的原理就是這樣,但是Sublist3r的查找源很多,不僅僅是在個別搜索引擎上查找。搜索源包括有百度、Yahoo、Google、Bing、Ask、Netcraft等等除此之外使用通過查找SSL證書、DNS、暴力枚舉等這些手段去查找子域名。

    簡單使用

    可以通過git clone https://github.com/aboul3la/Sublist3r.git安裝Sublist3r。然后使用sudo pip install -r requirements.txt安裝依賴庫。最后通過一個簡單的命令查找子域名,這里以freebuf為例子。python3 sublist3r.py -d freebuf.com網站內容越多查詢時間越慢,可以加一個-v來實時顯示找到的子域名。

    代碼細節

    代碼目錄

    在Linux終端下,使用tree能打印當前目錄等結構樹:

    .
    ├── LICENSE
    ├── README.md
    ├── requirements.txt
    ├── subbrute
    │   ├── init.py
    │   ├── names.txt
    │   ├── resolvers.txt
    │   ├── subbrute.py
    └── sublist3r.py
    

    入口函數

    sublist3r的的主要代碼實現在sublist3r.py文件中。可以使用你喜歡的代碼編輯器,打開這個文件。為了便于閱讀,我們把所有的代碼塊(def、class)合上。你大概會看到如下的場景:

    import re
    import sys
    #......省略若干引用
    def banner():
    def paeser_error(errmsg):
    #......省略若干函數
    class enumratorBase(object):
    class enumratorBaseThreaded(multiprocessing.Process, enumratorBase):
    class GoogleEnum(enumratorBaseThreaded):
    class YahooEnum(enumratorBaseThreaded):
    class AskEnum(enumratorBaseThreaded):
    class BingEnum(enumratorBaseThreaded):
    class BaiduEnum(enumratorBaseThreaded):
    #......省略若干 XXEnum 的類
    def main(domain, threads, savefile, ports, silent, verbose, enable_bruteforce, engines):
        #...省略main內容
        
    if name == "main":
        args = parse_args()
        domain = args.domain
        threads = args.threads
        savefile = args.output
        ports = args.ports
        enable_bruteforce = args.bruteforce
        verbose = args.verbose
        engines = args.engines
        if verbose or verbose is None:
            verbose = True
        banner()
        res = main(domain, threads, savefile, ports, silent=False, verbose=verbose, enable_bruteforce=enable_bruteforce, engines=engines)
    

    如果不是使用包引用,而是直接在控制臺打開,會執行if name == "main":下的內容。這里我們可以大概的了解到這里的功能,主要是用于處理用戶在控制臺輸入的參數,并將其作為參數去調用main函數。我們可以簡單的了解一下這些參數,domain是用戶輸入的主域名,比如freebuf.com;savefile是用于指定查找結束后子域名數據的保存目;verbose就是我們前面說的-v參數,通過這個參數來控制是否實時輸出信息;engines是查找引擎(搜索源),如果為空則表示查找全部的引擎,如果用戶指定了某個引擎(比如baidu),則只使用指定的引擎(比如baidu)進行查找。

    main函數

    一下是main這個函數的代碼,我省略了很多的細節和錯誤處理部分的代碼。

    def main(domain, threads, savefile, ports, silent, verbose, enable_bruteforce, engines):
        search_list = set()
        if is_windows:
            subdomains_queue = list()
        else:
            subdomains_queue = multiprocessing.Manager().list()
        # Validate domain
        domain_check = re.compile("^(http|https)?[a-zA-Z0-9]+([-.]{1}[a-zA-Z0-9]+).[a-zA-Z]{2,}$")
        #...省略域名驗證代碼
        
        parsed_domain = urlparse.urlparse(domain)
        supported_engines = {'baidu': BaiduEnum,
                             'yahoo': YahooEnum,
                             'google': GoogleEnum,
                             'bing': BingEnum,
                             'ask': AskEnum,
                             'netcraft': NetcraftEnum,
                             'dnsdumpster': DNSdumpster,
                             'virustotal': Virustotal,
                             'threatcrowd': ThreatCrowd,
                             'ssl': CrtSearch,
                             'passivedns': PassiveDNS
                             }
        chosenEnums = []
        if engines is None:
            chosenEnums = [
                BaiduEnum, YahooEnum, GoogleEnum, BingEnum, AskEnum,
                NetcraftEnum, DNSdumpster, Virustotal, ThreatCrowd,
                CrtSearch, PassiveDNS
            ]
        else:
            engines = engines.split(',')
            #...省略自定義引擎處理
        # Start the engines enumeration
        enums = [enum(domain, [], q=subdomains_queue, silent=silent, verbose=verbose) for enum in chosenEnums]
        for enum in enums:
            enum.start()
        for enum in enums:
            enum.join()
        subdomains = set(subdomains_queue)
        for subdomain in subdomains:
            search_list.add(subdomain)
        subdomains = search_list
        if subdomains:
            subdomains = sorted(subdomains, key=subdomain_sorting_key)
            if savefile:
                write_file(savefile, subdomains)
               #.....省略
        return subdomains
    

    首先,代碼開頭根據本地的操作系統類型去聲明變量subdomains_queue,這個用于保存找到的子域名。接著你可以看到domain_check,這個用于驗證域名是否合法。字典變量supported_engines內即為今天的重頭戲——查找引擎。冒號前面的字符串比如‘baidu’表示引擎的名字,冒號后面的BaiduEnum表示對應的類class BaiduEnum(enumratorBaseThreaded)

    {
    'baidu': BaiduEnum,
    'yahoo': YahooEnum,
    'google': GoogleEnum,
    'bing': BingEnum,
    'ask': AskEnum,
    'netcraft': NetcraftEnum,
    'dnsdumpster': DNSdumpster,
    'virustotal': Virustotal,
    'threatcrowd': ThreatCrowd,
    'ssl': CrtSearch,
    'passivedns': PassiveDNS
    }
    

    每一個引擎就是一個寫好的類,我們把類作為數據保存到列表enums里面。最終我們通過for循環來依次調用每一個類。而這每一個類都對應了該網站的子域名查詢規則。就像BaiduEnum這個類,它里面就寫好了如何在百度上查找關鍵詞,以及如何篩選數據等等內容。

    類BaiduEnum

    因為文章篇幅有限,我們就不一個一個類講過去,就從BaiduEnum這個類為例子,其他的類處理方法也差不多。BaiduEnum這個類繼承與enumratorBaseThreaded類,而enumratorBaseThreaded類又繼承于multiprocessing.Process類以及enumratorBase類。這2個類是所有引擎的基類。它們把引擎的基本功能提取出來,比如網絡請求、http頭數據等等。如果不是很了解這個概念可以去查找一下面向對象編程中的繼承概念。我們來看看BaiduEnum的構造函數:

    def init(self, domain, subdomains=None, q=None, silent=False, verbose=True):
            subdomains = subdomains or []
            base_url = 'https://www.baidu.com/s?pn={page_no}&wd={query}&oq={query}'
            self.engine_name = "Baidu"
            self.MAX_DOMAINS = 2
            self.MAX_PAGES = 760
            enumratorBaseThreaded.init(self, base_url, self.engine_name, domain, subdomains, q=q, silent=silent, verbose=verbose)
            self.querydomain = self.domain
            self.q = q
            return
    

    重點在于base_url,url中的{page_no}以及{query}就是需要最終替換的目標,query表示查詢參數,page_no表示頁數,也許很多人會以為這個頁數是我們通俗意義上的1表示第一頁,2表示第二頁。但如果你去仔細看代碼,你會發現在默認情況下這個page_no是每一次加10。這是因為百度和谷歌對于page_no的處理并不是以頁數為基礎,而是以搜索條數為基礎。每一頁顯示10條搜索內容,那么下一頁就是源基礎上加上10。這樣說可能會有點抽象,說的簡單點,page_no為0的時候,搜索引擎默認顯示的是1-10條記錄,當page_no為10的時候,搜索引擎顯示的是第11-20條記錄,以此類推。這2個參數會在后續代碼中構造出來并替換。比如{query}在后續的代碼中它則會根據查找出來的子域名實時替換。

    def generate_query(self):
            if self.subdomains and self.querydomain != self.domain:
                found = ' -site:'.join(self.querydomain)
                query = "site:{domain} -site:www.{domain} -site:{found} ".format(domain=self.domain, found=found)
            else:
                query = "site:{domain} -site:www.{domain}".format(domain=self.domain)
            return query
    

    要理解這個,我們要先了解一下一個搜索的小技巧。如果我希望在搜索引擎上查找關于域名的知識,那我可以搜索域名。但是突然我覺得這樣搜索很不精準,我想把所有百度知道的內容排除掉,那我就可以搜索:域名 -百度知道。再進一步,我想把百度貼吧的數據也排除掉,那么我的搜索詞應該是這樣的:域名 -百度知道 -貼吧。最后我發現,只有知乎上的信息比較符合我的要求,那我就可以直接指定查詢條件:域名 site:zhihu.com來查詢。這個技巧也用到了{query}身上。假設我們想尋找freebuf.com的子域名,那么我們最開始的初始值應該是這樣的:site:freebuf.com,接著我們發現有一個子域名www.ndqr.tw,那么我們的查詢條件就變成了:site:freebuf.com -site:www.ndqr.tw。再接著,我們發現了一個新的子域名job.freebuf.com,那我們的查詢條件就變成了site:freebuf.com -site:www.ndqr.tw -site:job.freebuf.com以此類推,不斷的限制查詢條件直到找不到為止。好了,我們已經知道如何構建url鏈接去搜索,那么它sublist3r又是如何通過搜索結果獲取域名的呢?在類BaiduEnumextract_domains方法里就實現了url地址的解析,如下所示。

    def extract_domains(self, resp):
            links = list()
            found_newdomain = False
            subdomain_list = []
            link_regx = re.compile('?class="c-showurl".?>(.?)')
            try:
                links = link_regx.findall(resp)
                for link in links:
                    link = re.sub('<.?>|>|<| ', '', link)
                    if not link.startswith('http'):
                        link = "http://" + link
                    subdomain = urlparse.urlparse(link).netloc
                    if subdomain.endswith(self.domain):
                        subdomain_list.append(subdomain)
                        if subdomain not in self.subdomains and subdomain != self.domain:
                            foundnewdomain = True
                            if self.verbose:
                                self.print("%s%s: %s%s" % (R, self.engine_name, W, subdomain))
                            self.subdomains.append(subdomain.strip())
            except Exception:
                pass
            if not found_newdomain and subdomain_list:
                self.querydomain = self.findsubs(subdomain_list)
            return links
    

    其余的代碼都是為上下文服務的,我們就來看看幾個關鍵代碼就好,比如re.compile('?class="c-showurl".?>(.?)')這是一個正則表達式。找到所有class名稱為‘c-showurl’的a標簽。我們可以在百度上隨便搜索一點東西,然后右鍵源代碼,看到搜索記錄的html代碼。我這里的一個例子是這樣的:

    <a target=\"_blank\" href=\"http://www.baidu.com/link?url=Z6IgCu5XyBlPrQ5dB9aEMfc_kRh9NhHpI1LcsEe3xR4tfVp_VaDNr3kRUPzi88eGvokctArtiUoNh1ANE5BZM_\" class=\"c-showurl\" style=\"text-decoration:none;\">www.ndqr.tw/articl…&nbsp;/a>

    沒錯,這個a標簽的class是c-showurl,且我們可以看到在a標簽包含下,有一個若隱若現的網址,雖然被隱藏了后面一半,我們還是可以看出它的子域名。第二個正則表達式re.sub('<.*?>|>|<| ', '', link)的目的是把找到的文本的細枝末節砍掉,留下我們需要的那一段域名。就這樣我們我們通過正則表達式找到了搜索記錄里的子域名,最后經過去重,拿到最終的子域名列表。代碼的其他細節我就沒有很詳細的描述,我希望這里只是一個閱讀引導,更多代碼的細節由于篇幅有限就不在一行一行詳細描述。因為大部分的代碼寫出來是為了執行上下文而寫的,并不是為了原理而寫。我們只要知道代碼的執行原理,以及代碼的目的即可。

    暴力枚舉子域名

    暴力枚舉也是很普遍的一種方法。所謂的暴力枚舉真的很暴力,它是通過一個一個子域名試過去,看看目標子域名是否存在。暴力枚舉域名的代碼沒有寫在sublist3r.py代碼中,而是獨立包裝了一個庫,稱之為subbrute,代碼在/subbrute/subbrute.py文件中。它會根據你目前的參數去配置是否激活暴力枚舉。這一段代碼寫在main函數中,前面的main函數為了簡潔,我把暴力枚舉那一部分的代碼去掉了。如果要看完整版的可以去看官方的代碼。下面是通過sublist3r.py中main函數調用subbrute的代碼片段。

    if enable_bruteforce:
            if not silent:
                print(G + "[-] Starting bruteforce module now using subbrute.." + W)
            record_type = False
            path_to_file = os.path.dirname(os.path.realpath(file))
            subs = os.path.join(path_to_file, 'subbrute', 'names.txt')
            resolvers = os.path.join(path_to_file, 'subbrute', 'resolvers.txt')
            process_count = threads
            output = False
            json_output = False
            bruteforce_list = subbrute.print_target(parsed_domain.netloc, record_type, subs, resolvers, process_count, output, json_output, search_list, verbose)
        subdomains = search_list.union(bruteforce_list)
    

    你可以看到,先通過enable_bruteforce變量判斷是否要激活(默認是激活狀態)暴力枚舉。然后讀入‘names.txt’和‘resolvers.txt’文件,最終,傳給包subbrute,得到枚舉后的子域名列表。并存儲到bruteforce_list內。subbrute的代碼大家可以自己去瀏覽一下。但是這里可以簡單的講解一下它的原理。其實相對來說也是比較簡單的。暴力枚舉雖然暴力,但是并不傻。不會真的從a開始一直試到zzzzz……它是采用了把可能會使用到子域名都寫出來,然后再一個一個試過去。前面代碼中引入的‘names.txt’就是可能用到的子域名。不行大家可以打開看看,里面包含了大部分我們日常生活中會用到的子域名組合,比如’email,www’等等。至于‘resolvers.txt’文件其實就是開放的DNS查詢地址,類似于8.8.8.8這種DNS服務的集合。

    使用ssl證書枚舉子域名

    現在帶有的ssl證書的越來越多,如果目標站點是全站https,一般就只有一個證書,然后,各個子域名都包含在證書里面。我們可以通過對ssl證書的解析去獲取子域名數據。比如下面的這個例子:

    ssl.png
    你可以看到,這個證書頒發給了旗下的這么多域名。但是在sublist3r中沒有采用自己下載ssl證書解析的形式,而是直接通過一個在線服務https://crt.sh/ 來查詢,相對來說會比較方便,但是隱藏了ssl子域名枚舉的技術細節,對于想通過代碼了解實際原理的小伙伴來說不太友好。另外還有一個通過DNS枚舉子域名也是通過在線的服務https://dnsdumpster.com來達到目的。至于dnsdumpster的底層細節我猜測可能是通過一些我們熟知的域傳送漏洞,dns反查,暴力枚舉等方法去實現。

    總結

    sublist3r代碼量不多,很適合初學者學習。從寫文章的角度來說,實在是難以一行一行代碼展開講。這也是我第一次寫代碼閱讀的文章,沒有什么經驗,如果大家有什么建議歡迎大家理性的提出來。

    *本文作者:mscb,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。

    發表評論

    已有 2 條評論

    取消
    Loading...
    css.php 宁夏卫视在线直播观看