從工具的程式碼角度學安全:Sublist3r子域名列舉神器原始碼閱讀
*本文作者:mscb,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
前言
Sublist3r是一款簡單易用的子域名列舉利器。但是今天我不打算講解它的使用教程,而是深入一層,從它的程式碼中窺探原理。也許有人可能很不理解,為什麼不好好的使用工具而偏偏去深究它的程式碼實現細節。
從我個人角度來說,我覺得:
第一個層面,一位優秀的安全從業者,至少在讀/寫程式碼層面不能成為短柄。如果一味的只使用工具,不瞭解原理,那很容易成為大家口中的“指令碼小子(script kiddie)”。
第二個層面,這是我個人推崇的的學習方法,從自己的學習道路中,我喜歡遵循3個W的學習原則,即(What->Why->What):
第一個What表示在初學階段,需要學會 哪些東西 能 幹些什麼 ,比如知道了Sublist3r工具可以用來列舉子域名。
第二個Why則表示在中級階段,去了解這個東西的實現原理,以及為什麼要這麼實現,比如通過工具的程式碼學會它的底層細節,與此同時還能豐富知識體系。
第三個What表示在高階階段,知道要 幹什麼事情 需要 什麼工具 ,不在細究技術細節,而是通過前期的學習,形成自己的知識網路,能精準的根據當前情況定位到具體的工具。
Sublist3r工具介紹
從Sublist3r的GitHub主頁( ofollow" rel="nofollow,noindex" target="_blank">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.freebuf.com
,那麼我們的查詢條件就變成了: site:freebuf.com -site:www.freebuf.com
。再接著,我們發現了一個新的子域名 job.freebuf.com
,那我們的查詢條件就變成了 site:freebuf.com -site:www.freebuf.com -site:job.freebuf.com
以此類推,不斷的限制查詢條件直到找不到為止。好了,我們已經知道如何構建url連結去搜索,那麼它sublist3r又是如何通過搜尋結果獲取域名的呢?在類 BaiduEnum
的 extract_domains
方法裡就實現了url地址的解析,如下所示。
<em><code>def extract_domains(self, resp): links = list() found_newdomain = False subdomain_list = [] link_regx = re.compile('</code></em>?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.freebuf.com/articl… /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證書的解析去獲取子域名資料。比如下面的這個例子:
你可以看到,這個證書頒發給了旗下的這麼多域名。但是在sublist3r中沒有采用自己下載ssl證書解析的形式,而是直接通過一個線上服務 https://crt.sh/ 來查詢,相對來說會比較方便,但是隱藏了ssl子域名列舉的技術細節,對於想通過程式碼瞭解實際原理的小夥伴來說不太友好。另外還有一個通過DNS列舉子域名也是通過線上的服務 https://dnsdumpster.com 來達到目的。至於dnsdumpster的底層細節我猜測可能是通過一些我們熟知的域傳送漏洞,dns反查,暴力列舉等方法去實現。
總結
sublist3r程式碼量不多,很適合初學者學習。從寫文章的角度來說,實在是難以一行一行程式碼展開講。這也是我第一次寫程式碼閱讀的文章,沒有什麼經驗,如果大家有什麼建議歡迎大家理性的提出來。
*本文作者:mscb,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。