Python 的请求会触发 Cloudflare 的安全性,而 urllib 不会
Posted
技术标签:
【中文标题】Python 的请求会触发 Cloudflare 的安全性,而 urllib 不会【英文标题】:Python's requests triggers Cloudflare's security while urllib does not 【发布时间】:2020-10-22 08:26:35 【问题描述】:我正在为餐厅网站开发自动网络抓取工具,但遇到了问题。该网站使用 Cloudflare 的反机器人安全性,我想绕过它,不是攻击模式,而是一个仅在检测到非美国 IP 或机器人时触发的验证码测试。我正在尝试绕过它,因为当我清除 cookie、禁用 javascript 或使用美国代理时,不会触发 Cloudflare 的安全性。
知道了这一点,我尝试使用 python 的 requests 库:
import requests
headers = 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0'
response = requests.get("https://grimaldis.myguestaccount.com/guest/accountlogin", headers=headers).text
print(response)
但这最终会触发 Cloudflare,无论我使用什么代理。
但是在使用具有相同标头的 urllib.request 时:
import urllib.request
headers = 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0'
request = urllib.request.Request("https://grimaldis.myguestaccount.com/guest/accountlogin", headers=headers)
r = urllib.request.urlopen(request).read()
print(r.decode('utf-8'))
当使用相同的美国 IP 运行时,这一次它不会触发 Cloudflare 的安全性,即使它使用与请求库相同的标头和 IP。
所以我想弄清楚是什么在请求库中触发 Cloudflare 而不是在 urllib 库中。
虽然典型的答案是“然后就使用 urllib”,但我想弄清楚请求的确切不同之处以及如何解决它,首先要了解请求的工作原理和 Cloudflare 检测机器人,但是也让我可以将我能找到的任何修复应用到其他 httplibs(尤其是异步的)
编辑 N°2:到目前为止的进展:
感谢@TuanGeek,我们现在可以使用请求绕过 Cloudflare 块,只要我们直接连接到主机 IP 而不是域名(出于某种原因,带有请求的 DNS 重定向会触发 Cloudflare,但 urllib 不会) :
import requests
from collections import OrderedDict
import socket
# grab the address using socket.getaddrinfo
answers = socket.getaddrinfo('grimaldis.myguestaccount.com', 443)
(family, type, proto, canonname, (address, port)) = answers[0]
headers = OrderedDict(
'Host': "grimaldis.myguestaccount.com",
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0',
)
s = requests.Session()
s.headers = headers
response = s.get(f"https://address/guest/accountlogin", verify=False).text
注意:尝试通过 HTTP(而不是验证变量设置为 False 的 HTTPS)访问将触发 Cloudflare 的阻止
现在这很好,但不幸的是,我的最终目标是使这项工作与 httplib HTTPX 异步工作仍未达到,因为使用以下代码,即使我们直接通过主机 IP,具有正确的标头,并且验证设置为 False:
import trio
import httpx
import socket
from collections import OrderedDict
answers = socket.getaddrinfo('grimaldis.myguestaccount.com', 443)
(family, type, proto, canonname, (address, port)) = answers[0]
headers = OrderedDict(
'Host': "grimaldis.myguestaccount.com",
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0',
)
async def asks_worker():
async with httpx.AsyncClient(headers=headers, verify=False) as s:
r = await s.get(f'https://address/guest/accountlogin')
print(r.text)
async def run_task():
async with trio.open_nursery() as nursery:
nursery.start_soon(asks_worker)
trio.run(run_task)
编辑 N°1:有关更多详细信息,这是来自 urllib 和请求的原始 HTTP 请求
请求:
send: b'GET /guest/nologin/account-balance HTTP/1.1\r\nAccept-Encoding: identity\r\nHost: grimaldis.myguestaccount.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0\r\nConnection: close\r\n\r\n'
reply: 'HTTP/1.1 403 Forbidden\r\n'
header: Date: Thu, 02 Jul 2020 20:20:06 GMT
header: Content-Type: text/html; charset=UTF-8
header: Transfer-Encoding: chunked
header: Connection: close
header: CF-Chl-Bypass: 1
header: Set-Cookie: __cfduid=df8902e0b19c21b364f3bf33e0b1ce1981593721256; expires=Sat, 01-Aug-20 20:20:06 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Cache-Control: private, max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0
header: Expires: Thu, 01 Jan 1970 00:00:01 GMT
header: X-Frame-Options: SAMEORIGIN
header: cf-request-id: 03b2c8d09300000ca181928200000001
header: Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
header: Set-Cookie: __cfduid=df8962e1b27c25b364f3bf66e8b1ce1981593723206; expires=Sat, 01-Aug-20 20:20:06 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Vary: Accept-Encoding
header: Server: cloudflare
header: CF-RAY: 5acb25c75c981ca1-EWR
URLLIB:
send: b'GET /guest/nologin/account-balance HTTP/1.1\r\nAccept-Encoding: identity\r\nHost: grimaldis.myguestaccount.com\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0\r\nConnection: close\r\n\r\n'
reply: 'HTTP/1.1 200 OK\r\n'
header: Date: Thu, 02 Jul 2020 20:20:01 GMT
header: Content-Type: text/html;charset=utf-8
header: Transfer-Encoding: chunked
header: Connection: close
header: Set-Cookie: __cfduid=db9de9687b6c22e6c12b33250a0ded3251292457801; expires=Sat, 01-Aug-20 20:20:01 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Expires: Thu, 2 Jul 2020 20:20:01 GMT
header: Cache-Control: no-cache, private, no-store
header: X-Powered-By: Undertow/1
header: Pragma: no-cache
header: X-Frame-Options: SAMEORIGIN
header: Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com https://www.google-analytics.com/analytics.js https://use.typekit.net connect.facebook.net/ https://googleads.g.doubleclick.net/ app.pendo.io cdn.pendo.io pendo-static-6351154740266000.storage.googleapis.com pendo-io-static.storage.googleapis.com https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google.com/recaptcha/api.js apis.google.com https://www.googletagmanager.com api.instagram.com https://app-rsrc.getbee.io/plugin/BeePlugin.js https://loader.getbee.io api.instagram.com https://bat.bing.com/bat.js https://www.googleadservices.com/pagead/conversion.js https://connect.facebook.net/en_US/fbevents.js https://connect.facebook.net/ https://fonts.googleapis.com/ https://ssl.gstatic.com/ https://tagmanager.google.com/;style-src 'unsafe-inline' *;img-src * data:;connect-src 'self' app.pendo.io api.feedback.us.pendo.io; frame-ancestors 'self' app.pendo.io pxsweb.com *.pxsweb.com;frame-src 'self' *.myguestaccount.com https://app.getbee.io/ *;
header: X-Lift-Version: Unknown Lift Version
header: CF-Cache-Status: DYNAMIC
header: cf-request-id: 01b2c5b1fa00002654a25485710000001
header: Expect-CT: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
header: Set-Cookie: __cfduid=db9de811004e591f9a12b66980a5dde331592650101; expires=Sat, 01-Aug-20 20:20:01 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Set-Cookie: __cfduid=db9de811004e591f9a12b66980a5dde331592650101; expires=Sat, 01-Aug-20 20:20:01 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Set-Cookie: __cfduid=db9de811004e591f9a12b66980a5dde331592650101; expires=Sat, 01-Aug-20 20:20:01 GMT; path=/; domain=.myguestaccount.com; HttpOnly; SameSite=Lax; Secure
header: Server: cloudflare
header: CF-RAY: 5acb58a62c5b5144-EWR
【问题讨论】:
我知道requests
在后台使用urllib3
来执行连接。也许值得探索两个库中这种连接方式的差异(urllib
vs urllib3
)。我试图审视自己,但它超出了我的熟悉程度。
我猜这与 requests 如何设置请求有关。它在后台使用 urllib,但负责在幕后完成大部分肮脏的工作(这解释了为什么我必须使用 urllib 解压缩和解码响应,而请求会自动执行)。也许特定的编码或设置请求会自动设置 urllib 没有?
【参考方案1】:
这真的激起了我的兴趣。我能够开始工作的requests
解决方案。
解决方案
最后缩小问题的范围。当您使用请求时,它使用 urllib3 连接池。常规 urllib3 连接和连接池之间似乎存在一些不一致。一个可行的解决方案:
import requests
from collections import OrderedDict
from requests import Session
import socket
# grab the address using socket.getaddrinfo
answers = socket.getaddrinfo('grimaldis.myguestaccount.com', 443)
(family, type, proto, canonname, (address, port)) = answers[0]
s = Session()
headers = OrderedDict(
'Accept-Encoding': 'gzip, deflate, br',
'Host': "grimaldis.myguestaccount.com",
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0'
)
s.headers = headers
response = s.get(f"https://address/guest/accountlogin", headers=headers, verify=False).text
print(response)
技术背景
所以我通过 Burp Suite 运行了这两种方法来比较请求。以下是请求的原始转储
使用请求
GET /guest/accountlogin HTTP/1.1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0
Accept-Encoding: gzip, deflate
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Connection: close
Host: grimaldis.myguestaccount.com
Accept-Language: en-GB,en;q=0.5
Upgrade-Insecure-Requests: 1
dnt: 1
使用 urllib
GET /guest/accountlogin HTTP/1.1
Host: grimaldis.myguestaccount.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-GB,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Upgrade-Insecure-Requests: 1
Dnt: 1
区别在于标题的顺序。dnt
大小写的区别实际上不是问题。
所以我能够通过以下原始请求成功发出请求:
GET /guest/accountlogin HTTP/1.1
Host: grimaldis.myguestaccount.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0
所以Host
标头已发送到User-Agent
上方。所以如果你想继续使用请求。考虑使用 OrderedDict 来确保标题的顺序。
【讨论】:
但是你将如何解决这个问题?因为即使使用大写的 Dnt 和重新组织的标头,请求仍然会触发 cloudflare 的反机器人。更重要的是,通过一些测试,我发现 urllib 仍然能够通过两个标头绕过 cloudlfare 的检测:headers='User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0', 'Accept-Encoding': 'gzip, deflate, br'
。所以我仍然猜测应该有其他事情发生。编辑:实际上它只需要 User-Agent 标头仍然绕过它
标题的顺序很重要。我已经使用requests
添加了确切的解决方案。
哇,这很奇怪。我昨天运行了代码,它起作用了。回到绘图区!我想知道通过 Burp Suite 运行请求是否会影响它。
是的。刚刚加倍检查。当我通过 Burp Suite 编写代码时,它可以工作。但如果我在没有 Burp Suite 的情况下运行它,它就会失败。这很奇怪,因为 Burp Suite 根本不应该修改请求。
好的。更新了解决方案。 Cloudflare 似乎导致请求 DNS 查询出现问题。我将不得不深入研究 DNS 查询请求失败的原因。但解决方法是使用套接字获取 IP 地址并在请求中使用该地址。【参考方案2】:
经过一些调试,感谢@TuanGeek 的回答,我们发现请求库的问题似乎来自处理 cloudflare 时请求部分的 DNS 问题,对此问题的简单修复是这样直接连接到主机IP:
import requests
from collections import OrderedDict
from requests import Session
import socket
# grab the address using socket.getaddrinfo
answers = socket.getaddrinfo('grimaldis.myguestaccount.com', 443)
(family, type, proto, canonname, (address, port)) = answers[0]
s = Session()
headers = OrderedDict(
'Accept-Encoding': 'gzip, deflate, br',
'Host': "grimaldis.myguestaccount.com",
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:77.0) Gecko/20100101 Firefox/77.0'
)
s.headers = headers
response = s.get(f"https://address/guest/accountlogin", headers=headers, verify=False).text
print(response)
现在,这个修复在使用 httplib HTTPX 时不起作用,但是我找到了问题的根源。
问题来自 h11 库(HTTPX 使用它来处理 HTTP/1.1 请求),虽然 urllib 会自动修复标头的字母大小写,但 h11 采用了不同的方法,将每个标头小写。虽然理论上这不应该引起任何问题,因为服务器应该以不区分大小写的方式处理标头(并且在很多情况下他们这样做),但现实是 HTTP 是 Hard™️ 并且 Cloudflare 等服务不尊重RFC2616 并要求标头正确大写。
关于大小写的讨论已经在 h11 结束了一段时间:
https://github.com/python-hyper/h11/issues/31
并且“最近”也开始出现在 HTTPX 的仓库中:
https://github.com/encode/httpx/issues/538
https://github.com/encode/httpx/issues/728
现在,对于 Cloudflare 和 HTTPX 之间问题的不令人满意的答案是,在 h11 方面完成某些事情之前(或者直到 Cloudflare 奇迹般地开始遵守 RFC2616),HTTPX 和 Cloudflare 处理标头大写的方式不会有太大变化。
要么使用不同的 HTTPLIB,例如 aiohttp 或 requests-futures,尝试自己使用 h11 分叉和修补标头大写,或者等待并希望 h11 团队正确处理该问题。
【讨论】:
【参考方案3】:我在抓取一个电子商务网站(guess dot com)时遇到了同样的问题。更改标题顺序并没有为我解决。我的结论:显然,CloudFlare 会分析请求的 TLS 指纹并抛出 403 (1020) 代码,以防指纹与通常用于抓取的 node.js/python/curl 匹配。
解决方案是模拟一些流行浏览器的指纹 - 最明显的方法是使用 Puppeteer.js 和 puppeteer 额外隐身插件。但是..由于 Puppeteer 对我的用例来说不够快(我说得委婉些.. Puppeteer 在资源和迟缓方面很疯狂)我不得不构建一个使用无聊SSL(Chrome 使用的 SSL 库)的实用程序 - 因为编译 C/C++ 代码并找出某些 TLS 库的隐蔽编译错误对于大多数 Web 开发人员来说并不有趣 - 我将其包装为 API 服务器,您可以在这里尝试:https://rapidapi.com/restyler/api/scrapeninja
详细了解 CloudFlare 如何分析 TLS:https://blog.cloudflare.com/monsters-in-the-middleboxes/
【讨论】:
以上是关于Python 的请求会触发 Cloudflare 的安全性,而 urllib 不会的主要内容,如果未能解决你的问题,请参考以下文章