本文作者: fatez3r0 已獲得作者授權轉載
本文博客地址:http://blog.fatezero.org/2018/03/05/web-scanner-crawler-01/
點擊 閱讀原文 直達博客
Web 漏掃的爬蟲和其他的網絡爬蟲的技術挑戰不太一樣,漏掃的爬蟲不僅僅需要爬取網頁內容、分析連結信息, 還需要儘可能多的觸發網頁上的各種事件,以便獲取更多的有效連結信息。 總而言之,Web 漏掃的爬蟲需要不擇手段的獲取儘可能多新的連結信息。
在這篇博客文章中,我打算簡單地介紹下和爬蟲瀏覽器相關內容,爬蟲基礎篇倒不是說內容基礎,而是這部分內容在漏掃爬蟲中的地位是基礎的。
0x01 QtWebkit or Headless ChromeQtWebkit or Headless Chrome, that is a question
QtWebkit 還是 Headless Chrome,我們一個一個分析。
QtWebkit
我們先說一下在漏掃爬蟲和 QtWebkit 相關的技術:
1、使用 QtWebkit
2、使用 PhantomJS (基於 Qt 編寫)
3、使用 PyQt (一個 Python 的 Qt bindings)
這也是我之前在 TangScan 調研 QtWebkit 系列的時候面對的技術,當時就首先就排除了 PyQt, 因為在 PyQt 中沒有辦法自定義 QPA 插件,如果不借用 xvfb, 是沒法在沒有 X Server 的伺服器上跑起來的,本來 PyQt 已經夠慢,再加上一個 xvfb,那就更慢了,所以直接排除 PyQt。
接著討論 PhantomJS,PhantomJS 的優點是簡單,不需要再次開發,直接使用 js 就可以操作一個瀏覽器, 所以 TangScan 內部的第一個版本也選擇了 PhantomJS,但後面也發現了 PhantomJS 的不足。
首先 PhantomJS 可以使用 js 操作瀏覽器是個優點,但也必須多出一個 js context (QWebPage) 開銷,而且有時候 js 的 callback 在一些情況下沒有被調用。 其次我所需要的功能 PhantomJS 並沒有提供,然而在 QtWebkit 中可以實現。
所以 TangScan 內部的第二版,我選擇了使用 QtWebkit 來重新寫一個類似 PhantomJS 的東西 (內部名為 CasterJS,AWVS 也是用 QtWebkit 寫了個名為 marvin 的爬蟲)。
但是直接使用 QtWebkit 還是有問題。 首先自從 Qt5.2 之後,對應的 WebKit 引擎就沒有再更新過,別說支持 ES6 了,函數連 bind 方法都沒有。 其次內存洩漏問題嚴重,最明顯的情況就是設置默認不加載圖片 QWebSettings::AutoLoadImages 的時候,內存使用率蹭蹭地往上漲。 最後也是最嚴重的問題,穩定性欠缺,也是自己實現了 CasterJS 之後才知道為什麼 PhantomJS 上為什麼會有那麼多沒處理的 issue, 這個不穩定的原因是第三方庫不穩定 (老舊的 Webkit),自己還不能更換這個第三方庫。 當時在 TangScan 的時候,就非常頭疼這些明知道不是自己的鍋、解決起來特麻煩、還必須得解決的問題。
所以如果沒有其他選擇,QtWebkit 忍一忍還是能繼續使用下去,但是 Headless Chrome 出現了。
Headless Chrome
Chrome 的 Headless 模式在 2015-08 開始低調開發,2016-06 開始對外公開,2017-04 在 M59 上正式發布。
後來 PhantomJS 的開發者 Vitaly Slobodin 在 PhantomJS 郵件組發出了公告 \[Announcement\] Stepping down as maintainer:
https://groups.google.com/forum/#!topic/phantomjs/9aI5d-LDuNE
聽到這個消息我真的一點都不意外,在 TangScan 中,也是使用 Qt 從頭開發起 CasterJS 的我來說, 已經受夠了由於老舊的 Webkit 版本帶來的各種 crash,內存洩漏,QtWebkit 這個坑實在是太坑了。
Vitaly Slobodin 在當時作為 PhantomJS 唯一的主要開發者, 面對著 PhantomJS 項目上那接近 2k 的 issue,心有餘而力不足,而且 crash 問題佔多數。
雖然說很多問題上遊的 Webkit 已經解決了,但偏偏 Qt 一直綁定的都是 Webkit 幾年前的版本, 所以 Vitaly Slobodin 就真的自個單獨維護一個 QtWebkit 倉庫,用於專門解決這樣的問題。
但是作為 PhantomJS 唯一的開發者,既要開發新功能,又要持續跟進 QtWebkit 各種 BUG,力不從心。 然後雪上加霜的是 Qt 在 Qt 5.2 的時候宣布打算放棄 QtWebkit,不在進行更新,轉而使用基於 Chromium 的 QWebEngine 取代 QtWebkit。
雖然後來 annulen
https://github.com/annulen/webkit/releases
扛起了大旗,說要繼續維護 QtWebkit,要從 Webkit 那裡一點一點地更新代碼, 但個人開發速度還是比不上一個團隊。
這個時候 Headless Chrome 出來了,Vitaly Slobodin 在這個時候退出 PhantomJS 的開發是最好的選擇了。
Headless Chrome 的出現也讓我哭笑不得,哭的原因是因為 Headless Chrome 讓我在 TangScan 開發的 CasterJS 變得毫無意義, 笑的原因因為 Headless Chrome 比其他基於 QtWebkit 寫的 Headless 瀏覽器更快速、穩定、簡單,讓我跳出了 QtWebkit 這個坑。
誇了那麼久 Headless Chrome 不過也並不代表 Headless Chrome 毫無缺點, 首先 Chrome 的 Headless 模式算是一個比較新的特性,一些功能還不算完善,只能等官方實現或者自行實現(比方說 interception 這個功能我就等了幾個月)。
其次 CDP 所提供的 API 不穩定,還會存在變動(比方說 M63 和 M64 中 Network.continueInterceptedRequest 的變動), 所以在使用 Headless Chrome 的時候,一定要先確定的 Chrome 版本,再編寫和 CDP 相關的代碼。
當然我也發現有些公司內部掃描器在使用 IE,大致過了一遍代碼,我個人並不覺得這是個好方案,所以我還是堅持使用 Headless Chrome。
0x02 小改 ChromiumOK,既然我們已經選定了 Headless Chrome,是不是可以擼起袖子開始幹了呢,很抱歉, 目前 Headless Chrome 還是不太滿足我們掃描器爬蟲的需求,我們還需要對其代碼進行修改。
官方文檔很詳細的介紹了如何編譯、調試 Chromium,只要網絡沒問題,一般也不會遇到什麼大問題,所以這裡也沒必要介紹相關知識。
hook location
我們先看一下這個例子
<script>
window.location = '/test1';
window.location = '/test2';
window.location = '/test3';
</script>這個場景面臨的問題和 wivet - 9.php:
https://github.com/bedirhan/wivet/blob/master/pages/9.php
有點兒類似: 怎麼樣把所有跳轉連結給抓取下來?
可能熟悉 QtWebkit 的同學覺得直接實現 QWebPage::acceptNavigationRequest 虛函數就可以攔截所有嘗試跳轉的請求。
可能熟悉 Headless Chrome 的同學會說在 CDP 中也有 Network.continueInterceptedRequest 可以攔截所有網絡請求,當然也包括跳轉的請求。
但實際上這兩種方法都只能獲取到最後一個 /test3 連結,因為前面兩次跳轉都很及時的被下一次跳轉給中斷了, 所以更不會嘗試發出跳轉請求,類似 intercept request 的 callback 就不可能獲取到所有跳轉的連結。
要是 location 能夠使用 defineProperty 進行修改,那問題就簡單多了。但是在一般的瀏覽器中 location 都是 unforgeable 的,也就是不能使用 defineProperty 進行修改, 不過現在 Chromium 代碼在我們手上,所以完全可以將其修改為可修改的,直接修改 location 對應的 idl 文件即可:
測試代碼:
<script>
Object.defineProperty(window, "location", {
set: function (newValue) {
console.log("new value: " + newValue);
},
get: function () {
console.log("get value");
return 123;
}
})
console.log(document.location);
console.log(window.location);
</script>
修改前:
修改後:
我之所以覺得偽造 location 這個特性很重要,是因為不僅僅在爬蟲中需要到這個特性,在實現 DOM XSS 檢測的時候這個特性也非常重要, 雖然說 Headless Chrome 官方文檔上有提過將來可能會讓用戶可以自由 hook location 的特性 (官方文檔上也是考慮到 DOM XSS 這塊), 但過了將近一年的時間,也沒有和這個特性的相關消息,所以還是自己動手豐衣足食。
popups 窗口
我們再來看看這個例子
<form action="http://www.qq.com" method="post" name="popup" target="_blank">
<input type="submit" style="display:none"/>
</form>
<script type="text/javascript">
document.getElementsByName("popup")[0].submit();
</script>在 Headless Chrome 中會直接彈出一個 popups 窗口,CDP 只能禁止當前 page 跳轉,但是沒辦法禁止新 page 創建, 在 QtWebkit 中並沒有這樣的煩惱,因為所有的跳轉請求都由 QWebPage::acceptNavigationRequest 決定去留。
這個問題是 DM 同學提出來的,當時他和我討論該問題。其實當時我也沒有解決方法,於是我跑去 Headless Chrome 郵件組問了開發人員
Is there any way to block popups in headless mode?
https://groups.google.com/a/chromium.org/forum/#!topic/headless-dev/gmZU6xBv3Jk
開發人員建議我先監聽 Page.windowOpen 或 Target.targetCreated 事件,然後再使用 Target.closeTarget 關閉新建的 popups 窗口。
那麼問題就來了,首先如果等待 page 新建之後再去關閉,不僅僅浪費資源去新建一個無意義的 page,而且 page 對應的網絡請求已經發送出去了,如果該網絡請求是一個用戶退出的請求,那麼事情就更嚴重了。 其次開發人員推薦的方法是沒法區分新建的 page 是在某個 page 下新建的,還是通過 CDP 新建的。所以 Headless Chrome 開發人員的建議我並不是特別滿意。
得,最好的辦法還是繼續修改代碼,使其在 page 中無法新建 page:
忽略 SSL 證書錯誤
在 Headless Chrome 對外公開之後很長一段時間內,是沒法通過 devtools 控制忽略 SSL 證書錯誤的,也沒辦法去攔截 Chrome 的各種網絡請求。
直到 2017-03
https://bugs.chromium.org/p/chromium/issues/detail?id=659662
才實現了在 devtools 上控制忽略 SSL 證書錯誤的功能。
直到 2017-06
https://groups.google.com/a/chromium.org/forum/#!topic/headless-dev/uvms04dXTIM
才實現了在 devtools 可以攔截並修改 Chrome 網絡請求的功能。
這兩個特性對於掃描器爬蟲來說非常重要,尤其是攔截網絡請求的功能,可偏偏這兩功能結合在一起使用的時候,就會出現 BUG, 在 puppeteer 上也有人提了 ignoreHTTPSErrors is not working when request interception is on
https://github.com/GoogleChrome/puppeteer/issues/1159
直至現在(2018-03-05),Google 也並沒有修復該 BUG,我還能說啥呢,還是自己動手,豐衣足食。
最簡單的方法就是修改 Chromium 代碼直接忽略所有的網站的 SSL 證書錯誤,這樣也省了一個 CDP 的 callback,修改如下:
測試環境 badssl.com
https://badssl.com/
在 Chromium 中還有一些其他可改可不改的地方,這裡就不繼續吐槽了。
0x03 總結OK,折騰了那麼久,終於把一個類似 wget 的功能實現好了(笑, 相對於我之前基於 QtWebkit 從頭實現一個類似 PhantomJS 的 CasterJS 來說, 目前使用 Headless Chrome 穩定、可靠、快速,簡直是漏掃爬蟲的不二選擇。
這篇博客就簡單講了一下和漏掃爬蟲相關的 Headless 瀏覽器的知識,接下來就到了漏掃爬蟲中最為重要的一點, 這一點也就決定了漏掃爬蟲連結抓取效果是否會比其他掃描器好,能好多少,這都會在掃描器的下一篇文章中繼續介紹。