使用selenium模擬瀏覽器進(jìn)行數(shù)據(jù)抓取無疑是當(dāng)下最通用的數(shù)據(jù)采集方案,它通吃各種數(shù)據(jù)加載方式,能夠繞過客戶JS加密,繞過爬蟲檢測,繞過簽名機制。它的應(yīng)用,使得許多網(wǎng)站的反采集策略形同虛設(shè)。由于selenium不會在HTTP請求數(shù)據(jù)中留下指紋,因此無法被網(wǎng)站直接識別和攔截。
這是不是就意味著selenium真的就無法被網(wǎng)站屏蔽了呢?非也。selenium在運行的時候會暴露出一些預(yù)定義的Javascript變量(特征字符串),例如"window.navigator.webdriver",在非selenium環(huán)境下其值為undefined,而在selenium環(huán)境下,其值為true(如下圖所示為selenium驅(qū)動下Chrome控制臺打印出的值)。

除此之外,還有一些其它的標(biāo)志性字符串(不同的瀏覽器可能會有所不同),常見的特征串如下所示:
webdriver
__driver_evaluate
__webdriver_evaluate
__selenium_evaluate
__fxdriver_evaluate
__driver_unwrapped
__webdriver_unwrapped
__selenium_unwrapped
__fxdriver_unwrapped
_Selenium_IDE_Recorder
_selenium
calledSelenium
_WEBDRIVER_ELEM_CACHE
ChromeDriverw
driver-evaluate
webdriver-evaluate
selenium-evaluate
webdriverCommand
webdriver-evaluate-response
__webdriverFunc
__webdriver_script_fn
__$webdriverAsyncExecutor
__lastWatirAlert
__lastWatirConfirm
__lastWatirPrompt
$chrome_asyncScriptInfo
$cdc_asdjflasutopfhvcZLmcfl_
了解了這個特點之后,就可以在瀏覽器客戶端JS中通過檢測這些特征串來判斷當(dāng)前是否使用了selenium,并將檢測結(jié)果附加到后續(xù)請求之中,這樣服務(wù)端就能識別并攔截后續(xù)的請求。
下面講一個具體的例子。
鯤之鵬的技術(shù)人員近期就發(fā)現(xiàn)了一個能夠有效檢測并屏蔽selenium的網(wǎng)站應(yīng)用:驗證碼表單頁,如果是正常的瀏覽器操作,能夠有效的通過驗證,但如果是使用selenium就會被識別,即便驗證碼輸入正確,也會被提示“請求異常,拒絕操作”,無法通過驗證(如下圖所示)。
可以看到它檢測了"webdriver", "__driver_evaluate", "__webdriver_evaluate"等等這些selenium的特征串。提交驗證碼的時候抓包可以看到一個_token參數(shù)(很長),selenium檢測結(jié)果應(yīng)該就包含在該參數(shù)里,服務(wù)端借以判斷“請求異常,拒絕操作”。
現(xiàn)在才進(jìn)入正題,如何突破網(wǎng)站的這種屏蔽呢?
我們已經(jīng)知道了屏蔽的原理,只要我們能夠隱藏這些特征串就可以了。但是還不能直接刪除這些屬性,因為這樣可能會導(dǎo)致selenium不能正常工作了。我們采用曲線救國的方法,使用中間人代理,比如fidder, proxy2.py或者mitmproxy,將JS文件(本例是yoda.*.js這個文件)中的特征字符串給過濾掉(或者替換掉,比如替換成根本不存在的特征串),讓它無法正常工作,從而達(dá)到讓客戶端腳本檢測不到selenium的效果。
下面我們驗證下這個思路。這里我們使用mitmproxy實現(xiàn)中間人代理),對JS文件(本例是yoda.*.js這個文件)內(nèi)容進(jìn)行過濾。啟動mitmproxy代理并加載response處理腳本:
mitmdump.exe -S modify_response.py
其中modify_response.py腳本如下所示:
# coding: utf-8
# modify_response.py
import re
from mitmproxy import ctx
def response(flow):
"""修改應(yīng)答數(shù)據(jù)
"""
if '/js/yoda.' in flow.request.url:
# 屏蔽selenium檢測
for webdriver_key in ['webdriver', '__driver_evaluate', '__webdriver_evaluate', '__selenium_evaluate', '__fxdriver_evaluate', '__driver_unwrapped', '__webdriver_unwrapped', '__selenium_unwrapped', '__fxdriver_unwrapped', '_Selenium_IDE_Recorder', '_selenium', 'calledSelenium', '_WEBDRIVER_ELEM_CACHE', 'ChromeDriverw', 'driver-evaluate', 'webdriver-evaluate', 'selenium-evaluate', 'webdriverCommand', 'webdriver-evaluate-response', '__webdriverFunc', '__webdriver_script_fn', '__$webdriverAsyncExecutor', '__lastWatirAlert', '__lastWatirConfirm', '__lastWatirPrompt', '$chrome_asyncScriptInfo', '$cdc_asdjflasutopfhvcZLmcfl_']:
ctx.log.info('Remove "{}" from {}.'.format(webdriver_key, flow.request.url))
flow.response.text = flow.response.text.replace('"{}"'.format(webdriver_key), '"NO-SUCH-ATTR"')
flow.response.text = flow.response.text.replace('t.webdriver', 'false')
flow.response.text = flow.response.text.replace('ChromeDriver', '')
在selnium中使用該代理(mitmproxy默認(rèn)監(jiān)聽127.0.0.1:8080)訪問目標(biāo)網(wǎng)站,mitmproxy將過濾JS中的特征符串,如下圖所示:

經(jīng)多次測試,該方法可以有效的繞過selenium檢測,成功提交驗證碼表單。
抄自:http://www.site-digger.com/html/articles/20180821/653.html
成功
附上mitmproxy簡單介紹(格式渣,可以直接訪問原文)
抄自 https://blog.wolfogre.com/posts/usage-of-mitmproxy/
本文是一個較為完整的 mitmproxy 教程,側(cè)重于介紹如何開發(fā)攔截腳本,幫助讀者能夠快速得到一個自定義的代理工具。
本文假設(shè)讀者有基本的 python 知識,且已經(jīng)安裝好了一個 python 3 開發(fā)環(huán)境。如果你對 nodejs 的熟悉程度大于對 python,可移步到 anyproxy,anyproxy 的功能與 mitmproxy 基本一致,但使用 js 編寫定制腳本。除此之外我就不知道有什么其他類似的工具了,如果你知道,歡迎評論告訴我。
本文基于 mitmproxy v4,當(dāng)前版本號為 [v4.0.1]
(https://blog.wolfogre.com/redirect/v3/Ax7R98RpJpVlv3sgL6mHzyQSAwM8Cv46xcU7LxImWv3FLS8tPHP6U8UtLy08c_pTxSFXLjbFyDvFbf40bv4wbv4xMRIDAzwK_jrFxVoWBjtuQQYW3Dsh_cU8Bk0KxaQE-cwFzC0vLTxz-lPF)。
顧名思義,mitmproxy 就是用于 MITM 的 proxy,MITM 即中間人攻擊(Man-in-the-middle attack)。用于中間人攻擊的代理首先會向正常的代理一樣轉(zhuǎn)發(fā)請求,保障服務(wù)端與客戶端的通信,其次,會適時的查、記錄其截獲的數(shù)據(jù),或篡改數(shù)據(jù),引發(fā)服務(wù)端或客戶端特定的行為。
不同于 fiddler 或 wireshark 等抓包工具,mitmproxy 不僅可以截獲請求幫助開發(fā)者查看、分析,更可以通過自定義腳本進(jìn)行二次開發(fā)。舉例來說,利用 fiddler 可以過濾出瀏覽器對某個特定 url 的請求,并查看、分析其數(shù)據(jù),但實現(xiàn)不了高度定制化的需求,類似于:“截獲對瀏覽器對該 url 的請求,將返回內(nèi)容置空,并將真實的返回內(nèi)容存到某個數(shù)據(jù)庫,出現(xiàn)異常時發(fā)出郵件通知”。而對于 mitmproxy,這樣的需求可以通過載入自定義 python 腳本輕松實現(xiàn)。
但 mitmproxy 并不會真的對無辜的人發(fā)起中間人攻擊,由于 mitmproxy 工作在 HTTP 層,而當(dāng)前 HTTPS 的普及讓客戶端擁有了檢測并規(guī)避中間人攻擊的能力,所以要讓 mitmproxy 能夠正常工作,必須要讓客戶端(APP 或瀏覽器)主動信任 mitmproxy 的 SSL 證書,或忽略證書異常,這也就意味著 APP 或瀏覽器是屬于開發(fā)者本人的——顯而易見,這不是在做黑產(chǎn),而是在做開發(fā)或測試。
那這樣的工具有什么實際意義呢?據(jù)我所知目前比較廣泛的應(yīng)用是做仿真爬蟲,即利用手機模擬器、無頭瀏覽器來爬取 APP 或網(wǎng)站的數(shù)據(jù),mitmproxy 作為代理可以攔截、存儲爬蟲獲取到的數(shù)據(jù),或修改數(shù)據(jù)調(diào)整爬蟲的行為。
事實上,以上說的僅是 mitmproxy 以正向代理模式工作的情況,通過調(diào)整配置,mitmproxy 還可以作為透明代理、反向代理、上游代理、SOCKS 代理等,但這些工作模式針對 mitmproxy 來說似乎不大常用,故本文僅討論正向代理模式。
安裝
“安裝 mitmproxy”這句話是有歧義的,既可以指“安裝 mitmproxy 工具”,也可以指“安裝 python 的 mitmproxy 包”,注意后者是包含前者的。
pip install mitmproxy (windows)
運行
要啟動 mitmproxy 用 mitmproxy、mitmdump、mitmweb 這三個命令中的任意一個即可,這三個命令功能一致,且都可以加載自定義腳本,唯一的區(qū)別是交互界面的不同。(我一般使用 mitmdump -s 腳本.py 命令運行)
腳本
腳本的編寫需要遵循 mitmproxy 規(guī)定的套路,這樣的套路有兩個,使用時選其中一個套路即可。
第一個套路是,編寫一個 py 文件供 mitmproxy 加載,文件中定義了若干函數(shù),這些函數(shù)實現(xiàn)了某些 mitmproxy 提供的事件,mitmproxy 會在某個事件發(fā)生時調(diào)用對應(yīng)的函數(shù),形如:
import mitmproxy.http
from mitmproxy import ctx
num = 0
def request(flow: mitmproxy.http.HTTPFlow):
global num
num = num + 1
ctx.log.info("We've seen %d flows" % num)
第二個套路(我沒成功)是,編寫一個 py 文件供 mitmproxy 加載,文件定義了變量 addons,addons 是個數(shù)組,每個元素是一個類實例,這些類有若干方法,這些方法實現(xiàn)了某些 mitmproxy 提供的事件,mitmproxy 會在某個事件發(fā)生時調(diào)用對應(yīng)的方法。這些類,稱為一個個 addon,比如一個叫 Counter 的 addon:
import mitmproxy.http
from mitmproxy import ctx
class Counter:
def __init__(self):
self.num = 0
def request(self, flow: mitmproxy.http.HTTPFlow):
self.num = self.num + 1
ctx.log.info("We've seen %d flows" % self.num)
addons = [
Counter()
]
事件
上述的腳本估計不用我解釋相信大家也看明白了,就是當(dāng) request 發(fā)生時,計數(shù)器加一,并打印日志。這里對應(yīng)的是 request 事件,那攏共有哪些事件呢?不多,也不少,這里詳細(xì)介紹一下。
事件針對不同生命周期分為 5 類?!吧芷凇边@里指在哪一個層面看待事件,舉例來說,同樣是一次 web 請求,我可以理解為“HTTP 請求 -> HTTP 響應(yīng)”的過程,也可以理解為“TCP 連接 -> TCP 通信 -> TCP 斷開”的過程。那么,如果我想拒絕來個某個 IP 的客戶端請求,應(yīng)當(dāng)注冊函數(shù)到針對 TCP 生命周期 的 tcp_start 事件,又或者,我想阻斷對某個特定域名的請求時,則應(yīng)當(dāng)注冊函數(shù)到針對 HTTP 聲明周期的 http_connect 事件。其他情況同理。
下面一段估計會又臭又長,如果你沒有耐心看完,那至少看掉針對 HTTP 生命周期的事件,然后跳到示例。
1,針對 HTTP 生命周期
def http_connect(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 收到了來自客戶端的 HTTP CONNECT 請求。在 flow 上設(shè)置非 2xx 響應(yīng)將返回該響應(yīng)并斷開連接。CONNECT 不是常用的 HTTP 請求方法,目的是與服務(wù)器建立代理連接,僅是 client 與 proxy 的之間的交流,所以 CONNECT 請求不會觸發(fā) request、response 等其他常規(guī)的 HTTP 事件。
def requestheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自客戶端的 HTTP 請求的頭部被成功讀取。此時 flow 中的 request 的 body 是空的。
def request(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自客戶端的 HTTP 請求被成功完整讀取。
def responseheaders(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自服務(wù)端的 HTTP 響應(yīng)的頭部被成功讀取。此時 flow 中的 response 的 body 是空的。
def response(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 來自服務(wù)端端的 HTTP 響應(yīng)被成功完整讀取。
def error(self, flow: mitmproxy.http.HTTPFlow):
(Called when) 發(fā)生了一個 HTTP 錯誤。比如無效的服務(wù)端響應(yīng)、連接斷開等。注意與“有效的 HTTP 錯誤返回”不是一回事,后者是一個正確的服務(wù)端響應(yīng),只是 HTTP code 表示錯誤而已。
- 針對 TCP 生命周期
(懶得抄) - 針對 Websocket 生命周期
(懶得抄) - 針對網(wǎng)絡(luò)連接生命周期
(懶得抄) - 通用生命周期
(懶得抄)
事實上考慮到 mitmproxy 的實際使用場景,大多數(shù)情況下我們只會用到針對 HTTP 生命周期的幾個事件。再精簡一點,甚至只需要用到 http_connect、request、response 三個事件就能完成大多數(shù)需求了