爲被動掃描器量身打造一款爬蟲 - LSpider

作者:LoRexxar'@知道創宇404實驗室
日期:2021年1月28日

前言

將時間還原到2019年6月之前,掃描器的世界大多還停留在AWVS這樣的主動掃描器,被動掃描曾被提出作爲瀏覽器插件火熱過一段時間,可惜效率太低等多種問題束縛着它的發展。隨着Xray的發佈,一款免費好用的被動掃描器從很多方面都帶火了自動化漏洞掃描的風潮。

其中有的人將Xray掛在自己的常用瀏覽器上以圖在使用的過程中撿漏,有的人只在日站的時候掛上以圖意外之喜,也有人直接操起自己塵封已久的爬蟲配合xray大範圍的掃描以圖撿個痛快。可以說配合xray日站在當時已經成了一股風潮。

市面上比較常見的組合crawlergo+xray中的crawlergo也正是在這種背景下被開放出來。

但可惜的是,建立在自動化漏洞挖掘的基礎上,當人們使用相同的工具,而我們又沒辦法自定義修改,是否能發現漏洞變成了是否能發現更多的資產的比拼。建立在這樣的背景下,我決定自己發起了一個開源的爬蟲項目,這就是LSpider誕生的背景。

LSpider作爲星鏈計劃的一員,已經開源,工具可能並不算成熟,但持續的維護以及更新是星鏈計劃的精神~

LSpider想要做到什麼?

在發起一個項目之初,我們往往忘記自己到底爲什麼開始,爲什麼要寫這個項目,重複造輪子,以及閉門造車從來都不是我們應該去做的事。

而LSpider發起的初衷,就是爲被動掃描器量身打造一款爬蟲

而建立在這個初衷的基礎上,我決定放棄傳統爬蟲的那些多餘的功能。

這是一個簡單的傳統爬蟲結構,他的特點是爬蟲一般與被動掃描器分離,將結果輸入到掃描器中。

image-20210126175759059

將被動掃描器直接代理到爬蟲上

image-20210127150819579

這樣一來,爬蟲的主要目標轉變爲了,儘可能的觸發更多的請求、事件、流量

建立在這個大基礎上,我們得到了現在的架構:

image-20210127160008583

由主控分配爬蟲線程,掃描目標域,並儘可能的觸發更多的請求、事件、流量。將被動掃描器通過代理的方式掛在爬蟲下並獨立的完成漏洞掃描部分。

除了爲被動掃描器服務以外,還有什麼是在項目發起時的初衷呢?

我的答案是,這個爬蟲+被動掃描器的目的是,能讓我不投入過多精力的基礎上,挖洞搞錢!!!

不在乎掃到什麼漏洞,不在乎掃到什麼廠商,只求最大限度的掃描目標相關所有站、所有域名、所有目標

爲了實現這個目標,我在爬蟲中內置了查詢子域名的api,內置了hackerone、bugcrowd目標爬蟲,在設計之初還添加了定時掃描功能。

image-20210127161615346

到目前爲止,我們設計了一個自動化無限制掃描目標,且爲被動掃描器而存在的爬蟲架構。

下面我們一起完成這個項目。

爬蟲基礎

首先爬蟲部分,爲了實現最大程度上觸發更多的請求、事件、流量,我們有且只有唯一的選擇爲Chrome Headless.

配置Chrome Headless

這裏我選擇了selenium來操作Chrome WebDriver。值得注意的是幾個比較重要的配置。

self.chrome_options.add_argument('--headless')
self.chrome_options.add_argument('--disable-gpu')
self.chrome_options.add_argument('--no-sandbox')
self.chrome_options.add_argument('--disable-images')
self.chrome_options.add_argument('--ignore-certificate-errors')
self.chrome_options.add_argument('--allow-running-insecure-content')
self.chrome_options.add_argument('blink-settings=imagesEnabled=false')
self.chrome_options.add_argument('--omnibox-popup-count="5"')
self.chrome_options.add_argument("--disable-popup-blocking")
self.chrome_options.add_argument("--disable-web-security")
self.chrome_options.add_argument("--disk-cache-size=1000")

除了設置headless模式以外,還關閉了一些無意義的設置。

if os.name == 'nt':
    chrome_downloadfile_path = "./tmp"
else:
    chrome_downloadfile_path = '/dev/null'

prefs = {
    'download.prompt_for_download': True,
    'profile.default_content_settings.popups': 0,
    'download.default_directory': chrome_downloadfile_path
}

設置好文件下載的目錄,如果沒設置的話會自動下載大量的文件在當前文件夾。

desired_capabilities = self.chrome_options.to_capabilities()
if IS_OPEN_CHROME_PROXY:
    logger.info("[Chrome Headless] Proxy {} init".format(CHROME_PROXY))

    desired_capabilities['acceptSslCerts'] = True
    desired_capabilities['acceptInsecureCerts'] = True
    desired_capabilities['proxy'] = {
        "httpProxy": CHROME_PROXY,
        "ftpProxy": CHROME_PROXY,
        "sslProxy": CHROME_PROXY,
        "noProxy": None,
        "proxyType": "MANUAL",
        "class": "org.openqa.selenium.Proxy",
        "autodetect": False,
    }

通過org.openqa.selenium.Proxy來設置瀏覽器代理,算是比較穩定的方式。

self.driver.set_page_load_timeout(15)
self.driver.set_script_timeout(5)

這兩個配置可以設置好頁面加載的超時時間,在大量的掃描任務中,這也是必要的。

除了基礎配置以外,有個值得注意的點是:

你必須在訪問頁面之後纔可以設置cookie,且cookie只能設置當前域,一旦涉及到跳轉,這種cookie設置方式就不會生效。

self.origin_url = url
self.driver.implicitly_wait(5)
self.driver.get(url)

if cookies:
    self.add_cookie(cookies)
    self.driver.implicitly_wait(10)
    self.driver.get(url)

模擬點擊以及智能填充

在配置好chrome headless之後,爲了模擬人類的使用,我拋棄了傳統爬蟲常用的攔截、hook等獲取請求並記錄的方式,轉而將重心放在模擬點擊以及智能填充上。

模擬點擊

這裏拿a標籤舉例子

links = self.driver.find_elements_by_xpath('//a')
link = links[i]

href = link.get_attribute('href')
self.driver.execute_script(
    "atags = document.getElementsByTagName('a');for(i=0;i<=atags.length;i++) { if(atags[i]){atags[i].setAttribute('target', '')}}")

if link.is_displayed() and link.is_enabled():
    link.click()

    self.check_back()

當獲取到對應標籤時,首先將對應的標籤屬性taget置空(不打開新的標籤頁),然後模擬點擊按鈕,之後檢查是否發生跳轉,並返回原頁面。

同樣的邏輯被複用在了button,input@type=submit和擁有onclick事件標籤上,值得注意的是。button這樣的標籤還加入模擬鼠標移動的操作。

if submit_button.is_displayed() and submit_button.is_enabled():
    action = ActionChains(self.driver)
    action.move_to_element(submit_button).perform()

    submit_button.click()

智能表單填充

這裏我把智能表單填充分爲兩部分,首先我們需要判斷當前頁面是否存在一個登錄框,這意味着當前頁面並沒有登錄(如果登錄狀態,一般登錄框會消失)。

這裏把可能出現登錄框的頁面分爲4種

form    ->  文本中出現login、登錄、sign等
button  ->  outerHTML出現login、user、pass等
input   ->  outerHTML出現登錄、註冊等關鍵詞
a       ->  文本出現login、登錄、sign等

噹噹前頁面滿足上述任一條件時,則會記錄下來到相應的位置(後續會提到)

然後會嘗試填充頁面的所有框框。

inputs = self.driver.find_elements_by_xpath("//input")
self.driver.execute_script(
    "itags = document.getElementsByTagName('input');for(i=0;i<=itags.length;i++) { if(itags[i]){itags[i].removeAttribute('style')}}")

input_lens = len(inputs)

if not inputs:
    return

for i in range(input_lens):
    try:
        input = inputs[i]

        # 移動鼠標
        # 如果標籤沒有隱藏,那麼移動鼠標
        if input.is_enabled() and input.is_displayed():

            action = ActionChains(self.driver)
            action.move_to_element(input).perform()

首先去掉表單框上的所有CSS,並將鼠標放置到對應的位置。

緊接着通過判斷表單的key值,滿足條件的對應填入部分預設的用戶名,密碼,郵箱,地址,手機號,並勾選所有的單選多選框,其餘輸入框則生成隨機字符串填入。

結果去重

到目前爲止,我們至少觸發了屬於頁面中大量的請求,接下來我們就遇到了另一個問題,如何對流量去重?

除了儘可能的觸發請求以外,爬蟲也並不是單一執行流程的,每個爬蟲需要不斷地從某個主控(RabbitMQ)獲取新的目標,而爬蟲也需要不斷地返回目標,但我們就需要去重邏輯來完成新目標的處理,這樣可以最大限度的減少無意義的請求。

這裏我使用了一套比較簡單的url泛化邏輯來做去重。首先我將獲取的返回和歷史記錄中相同域的url提取出來。

BLACK_DOMAIN_NAME_LIST = ['docs', 'image', 'static', 'blogs']

首先,如果域名中存在上面4個黑名單詞語,則只接受200條不重複請求。

然後將,所有的路徑泛化,按照/做分割,用A來代表純數字段,用B來代表字母混合段。

https://lorexxar.cn/2020/10/30/whitebox-2/

就會被泛化爲AAAB

如果歷史記錄已經存在flag爲AAAB的鏈接,則會進入更深層次的判斷。

緊接着會提取比較的兩個鏈接的B段內容。

首先判斷當前路徑下是否附和一些特殊路徑,如

BLACK_PATH_NAME = ['static', 'upload', 'docs', 'js', 'css', 'font', 'image']

如果滿足,那麼直接限制該路徑下只收錄100個鏈接,如果不滿足,那麼會提出兩個url的B段進行比較。也就是whitebox-2,如果不同,則判定爲擁有1個不同點。

且如果當前B段爲路徑的最後一部分,舉例子爲

https://lorexxar.cn/2020/10/30/whitebox-2/a.php

這裏的a.php就是路徑的最後一部分,如果兩個鏈接的最後一部分不爲靜態資源,則會被直接認定爲不同請求。

如果最後一部分相同,且不同點不超過1個,那麼會進入參數判斷。

這裏我們直接簡單粗暴的獲取所有請求的key,如果兩個請求都擁有相同的參數列表,則兩個鏈接爲不同請求。(會剔除沒有value的參數,如?20210127這類時間戳)

直接經過所有的判斷,該鏈接纔會被加入到新的目標列表當中,等待下一次被塞入任務隊列中。

完成整體架構

在經歷了爬蟲基礎架構以及模擬點擊、智能填充之後,我們順利的完成了爬蟲最基礎的部分,接下來我們需要完成藍框部分的邏輯。

image-20210127180633742

對於一個掃描器來說,最重要的就是穩定以及速度

在設計之初,我本來覺得LSpider是個私人使用的小工具,於是就簡單的使用隊列+多線程來做爬蟲的調度,但是在使用的過程中,我逐漸發現Python隊列會簡單的將數據存在內存中,而Chrome Headless會消耗大量的機器資源,如果內存不夠,就會陷入爬蟲端越來越慢,內存卻越來越大的境地,於是我將架構換成了用RabbitMQ做任務管理。

image-20210127183134976

這樣一來,我們爬蟲主控與數據處理的部分分離開來,兩個部分都只需要負責自己的任務即可。爬蟲線程可以直接監聽RabbitMQ來獲取任務。

加入登錄頁面檢查以及緊急隊列

在LSpider的使用過程中,由於我設計了發散式的掃描方案。針對A域名的掃描往往會蔓延到上百個新的域名(包括子域名),其中單靠添加任務時候添加的鑑權驗證遠遠不夠,再加上我逐漸發現這種發散式的掃描方案,很容易發現特別多目標資產,比如某OA、某郵件系統等。

於是我添加了登錄頁面的檢查(前文提到),一旦識別到登陸頁面,我將會保留當前頁面的url以及title,並進行一次指紋識別,並將相應的結果保存下來。

當我們收到了需要登錄頁面的推送時,我們又遇到了新的問題,假設任務列表已經陸陸續續儲存了幾十萬條請求,當我們完成了賬號的註冊和登錄,並將cookie設置好後,這個任務被重新加入到隊列的尾,等到任務再次跑到時,可能cookie已經失效了,怎麼解決呢?

這裏我嘗試引用了緊急隊列來作爲解決方案。這裏涉及到一個概念是RabbitMQ的消息優先級,隊列可以設置最大優先級,消息可以設置自己的優先級。在從隊列中獲取數據的時候,RabbitMQ會優先取出優先級更高的消息。

這裏我們直接新建一個單獨的線程和隊列來輪詢檢查登錄狀態。一旦滿足登錄成功,那麼該任務就會以高優先級的權重被加入到主隊列中。通過這種方式來實現緊急任務的管理。

image-20210128144340359

通過webhook+企業微信應用管理任務進度

當我們完成了整個工具的主線架構之後,就需要完善一些無關竟要但是又對體驗幫助很大的小功能了。

當掃描器掃描到漏洞的時候,我需要第一時間獲得消息。當爬蟲發現需要登錄的站點時,我需要第一時間去加上鑑權信息,說不定還剛好是我有的0day資產,我也必須第一時間拿到結果。

爲了實現這個需求,我必須將工具和"宇宙第一通訊工具"聯動起來,這裏我們直接通過webhook和企業微信的小應用聯動起來。

這裏直接用django起一個webhook接口,配合一些簡單的解析對接到被動掃描器上。然後將結果通過webchatpy推送到企業微信的應用上,效果是這樣的:

登錄推送: image-20210128151542873

每時推送+漏洞推送 image-20210128151641586

在經歷了前面多步的完善,我們終於完成了LSpider,他的架構也變成了這樣:

image-20210128151757684

寫在LSpider之後

LSpider是在疫情期間發起的項目,選擇Django+python是因爲這個項目最初更像是一個小玩具,再加上爬蟲本來就是應該長期跑在服務端的工具,所以沒太考慮就寫起來用起來了。

幸運的是,在整個工具實現的過程中,我陸陸續續挖掘了超過150+個漏洞,也完成了設計之初對工具的預想,實現了長時間自動化挖掘的目的。但也可惜的是,大多數的漏洞都沒有找到對應的src,於是也就丟在一邊不管了。在優化的過程中,也越來越感受到侷限。

在項目發起的時候,心裏覺得項目最麻煩的肯定是去重、爬蟲部分,但沒想到在邊寫邊用邊修的過程中就零零散散的度過了1年,到項目的後期之後,越來越發現由於Xray本身並不開源,再加上Xray這個工具重心就不在對外漏洞掃描上。越來越多的問題湧現出來。

在項目發起的時候,心裏覺得項目最麻煩的肯定是去重、爬蟲部分,但沒想到在邊寫邊用邊修的過程中就零零散散的度過了1年,到項目的後期之後,越來越發現侷限越來越多。由於Xray本身並不開源,再加上Xray這個工具重心就不在對外漏洞掃描上。越來越多的問題湧現出來。

且不提許多自己的漏掃想法沒辦法優化,光是xray本身就存在諸多無意義的掃描(比如服務器配置錯誤等,對於挖洞來說毫無意義)。尤其是在閱讀到許多曝光的漏洞信息之後,80%的時間你都沒辦法做任何事。

也正是在這樣的背景下,我們意識到哪怕是背靠一個優秀的被動掃描器,我們也沒辦法將爬蟲和被動掃描器拆分開來。於是我們又發起了第二個項目HeLuo,如果有機會的話,可能還有機會開源出來~

希望越來越多的朋友願意試用LSpider,就像星鏈計劃的初衷一般,開源項目的意義便是參與就會變得越來越好,我也會長期堅持維護,也歡迎大家的Issue和PR :>


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1473/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章