逆向Cloudflare 5秒盾:从JS挑战到获取cf_clearance的实战指南
1. 项目概述:当爬虫遇到“5秒盾”
做数据采集的朋友,对Cloudflare的“5秒盾”一定不陌生。你精心编写的爬虫脚本,满怀期待地发送请求,换来的却是一个冰冷的“503 Service Temporarily Unavailable”错误,或者是一个需要你手动点击“Verify you are human”按钮的页面,等待那令人焦躁的5秒钟。这个机制,就是Cloudflare用来区分真人用户和自动化脚本(尤其是恶意爬虫)的核心防线之一。它的官方名称是“Under Attack Mode”或“I‘m Under Attack”模式,但在我们爬虫工程师的圈子里,更习惯叫它“5秒盾”或“CF盾”。
这个项目的核心目标,就是彻底拆解这道防线。我们不止要绕过那个503页面,更要拿到通关密钥——cf_clearance这个Cookie。有了它,你的爬虫才能在后续的请求中被Cloudflare视为“已通过验证的人类”,从而稳定地访问目标网站的数据。这不仅仅是一个简单的“绕过”,而是一场涉及HTTP协议、JavaScript执行环境模拟、密码学挑战解答的综合性逆向工程实战。整个过程,就像是在和Cloudflare的安防系统进行一场无声的智力对决。
2. 逆向工程的核心思路与挑战拆解
要逆向“5秒盾”,我们首先得理解它的工作原理。Cloudflare并不是简单地用一个静态页面挡住你。当你首次访问一个受保护的站点时,会发生以下一连串事件:
- 初始拦截与挑战下发:你的请求(无论是浏览器还是爬虫)到达Cloudflare边缘节点。节点检测到请求特征可疑(如缺少合法Cookie、User-Agent非常见、请求频率异常等),不会直接将请求转发给源站,而是返回一个特殊的503状态码页面。这个页面内嵌了一段高度混淆、动态生成的JavaScript代码,这就是挑战的核心。
- 客户端执行与计算:浏览器(或我们需要模拟的环境)会执行这段JS代码。这段代码通常会做几件事:收集浏览器环境指纹(如WebGL、Canvas、AudioContext、字体列表、屏幕分辨率、插件信息等),进行一系列复杂的数学运算(可能涉及浮点数精度考验、内存操作模拟等),最终生成一个答案(
answer)。 - 提交答案与获取通行证:浏览器将计算出的
answer,连同收集到的一部分环境指纹数据,以特定格式(通常是POST请求)提交给Cloudflare的一个验证端点(如/cdn-cgi/challenge-platform/h/b/t/...或/cdn-cgi/challenge-platform/h/b/cv/...)。 - 验证通过与Cookie下发:Cloudflare后端验证
answer的正确性和环境数据的合理性。如果通过,它会返回一个302重定向,响应头里会包含一个Set-Cookie字段,用于设置cf_clearance这个Cookie。同时,它通常还会设置一个__cf_bmCookie(用于Bot管理)。 - 携带通行证访问:浏览器自动跟随重定向,并在新的请求中自动带上刚刚获得的
cf_clearanceCookie。此时,Cloudflare识别到此Cookie,认为你已通过人机验证,于是将你的请求转发给源站服务器,并返回正常的网页内容。
我们的核心挑战就在于第2和第3步:如何在不使用真实浏览器的情况下,正确执行那段混淆的JS,并生成能被Cloudflare接受的answer。这引出了几种主流思路:
- 思路一:无头浏览器自动化:使用Puppeteer、Playwright或Selenium等工具,启动一个完整的浏览器实例(如Chrome),模拟真人操作点击“Verify”按钮,等待JS执行完毕,然后提取Cookie。这是最模拟真人、最“笨”但通常最有效的方法,缺点是资源消耗大、速度慢、容易被高级指纹检测发现是自动化浏览器。
- 思路二:JS逆向与纯请求模拟:这是本项目的重点,也是技术含量最高的方法。核心是:
- 逆向挑战逻辑:通过静态分析、动态调试,理解Cloudflare下发的JS代码到底在计算什么。
- 补环境:由于Node.js或Python的默认环境与浏览器差异巨大,我们需要“补全”JS代码执行时所依赖的浏览器对象和方法,如
window、document、navigator、location,以及WebGLRenderingContext、CanvasRenderingContext2D等。 - 生成答案:在补全的环境下执行核心计算函数,得到
answer。 - 模拟提交:用HTTP客户端库(如Python的
requests)模拟浏览器提交answer的请求,获取cf_clearance。 这种方法速度快、资源占用低,但技术难度大,需要持续对抗Cloudflare的代码更新和混淆策略。
- 思路三:利用第三方库或服务:有一些开源库(如
cloudscraper、scrapy-cloudflare-middleware)或付费API服务尝试封装这部分逻辑。它们可能内部混合了上述两种方法。使用方便,但可能不稳定、有使用限制或成本。
本项目将深入思路二,带你走通从识别503页面到获取cf_clearance的全流程,理解每一个环节的细节与陷阱。
3. 实战流程:从503错误到获取cf_clearance
3.1 第一步:识别与捕获挑战页面
当你用requests.get(url)遇到503时,不要只看状态码。关键是要检查响应内容。
import requests headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' } response = requests.get('https://target-protected-site.com', headers=headers) print(f"状态码: {response.status_code}") # 很可能输出 503 print(f"响应头: {response.headers}") print(f"响应内容长度: {len(response.text)}") # 将响应内容保存到文件,方便查看 with open('challenge_page.html', 'w', encoding='utf-8') as f: f.write(response.text)打开保存的HTML文件,你会看到它不是一个简单的错误提示页。页面里通常包含:
- 一个标题为“Checking your browser before accessing...”的提示。
- 一个倒计时或“Verify you are human”按钮。
- 最重要的是,一大堆
<script>标签,里面是压缩和混淆过的JavaScript代码。其中往往有一个关键的脚本,其src属性可能指向一个包含/cdn-cgi/challenge-platform/的路径,或者直接内嵌了以(function(){...})()形式包裹的代码。 - 页面中通常还隐藏着一个包含挑战参数的
<input>标签,如name="jschl_vc"、name="pass"、name="jschl_answer"(有时是s、ray等,具体名称会变)。
实操心得:Cloudflare的挑战页面结构并非一成不变。有时挑战逻辑直接内嵌在HTML中,有时是通过外部JS文件加载。第一步一定要把完整的HTML和所有关联的JS文件都保存下来,因为挑战参数和逻辑可能分散在其中。
3.2 第二步:提取关键挑战参数与JS代码
我们需要从HTML中提取出几个关键元素,用于后续的请求构造和JS执行。
提取挑战参数:通常存在于表单或
<div>的>from bs4 import BeautifulSoup soup = BeautifulSoup(response.text, 'html.parser') # 查找方式可能随Cloudflare更新而变化,以下是常见示例 jschl_vc = soup.find('input', {'name': 'jschl_vc'}).get('value') pass_value = soup.find('input', {'name': 'pass'}).get('value') # 有时参数在div的data属性里 #># 示例:简单查找包含特定模式的script标签 scripts = soup.find_all('script') for script in scripts: if script.string and 'jschl_answer' in script.string: challenge_js = script.string break # 如果没找到,可能需要处理外部脚本
注意事项:Cloudflare的混淆技术很强,代码可能被“压扁”(移除空格换行)、变量名被混淆、控制流被平坦化。直接阅读几乎不可能。我们需要借助工具进行“反混淆”或直接动态执行。
3.3 第三步:逆向与执行挑战JS逻辑
拿到混淆的JS代码后,目标是在我们的Python(或Node.js)环境中,计算出正确的jschl_answer值。
方法A:使用Node.js环境执行(推荐)这是目前相对稳定和主流的方法。我们可以在Python中调用Node.js来执行这段浏览器环境的JS。
搭建Node.js执行环境:确保系统安装了Node.js。我们将使用
pyexecjs或node_vm2(通过subprocess调用)来桥接。补环境:这是成败的关键。直接将混淆的JS丢给Node.js执行肯定会报错,因为Node.js没有
window、document、location等浏览器对象。我们需要在执行前,向JS上下文中注入这些对象的模拟实现。import execjs import json # 1. 读取我们提取出的混淆JS代码 with open('challenge.js', 'r', encoding='utf-8') as f: obfuscated_js = f.read() # 2. 构建一个补丁环境的前置代码 # 这里是一个极简的示例,实际需要补的全得多,包括navigator.userAgent, screen, document.cookie等 env_patch = """ const window = this; window.document = { getElementById: function(id) { return { value: '' }; }, createElement: function() { return {}; }, // ... 其他必要属性和方法 }; window.location = { hostname: 'target-protected-site.com', // ... }; // 补全navigator, screen, WebGL, Canvas等 // 这是一个持续对抗的过程,Cloudflare会检测越来越多的指纹。 """ # 3. 将补丁和挑战代码拼接 full_js = env_patch + obfuscated_js # 4. 执行并提取答案 # 通常,原JS代码最后会将计算结果赋值给某个变量(如`a.value`),我们需要修改代码让它返回这个值。 # 假设我们通过分析,知道答案最终在变量 `answer` 中 extract_code = full_js + \nreturn a.value; // 或 return answer; 具体看原代码逻辑 try: ctx = execjs.compile(extract_code) jschl_answer = ctx.call('') # 如果代码是自执行函数,直接调用 # 或者 ctx.eval('...') print(f"计算出的答案: {jschl_answer}") except Exception as e: print(f"JS执行错误: {e}")补环境的深度:简单的补环境可能只能应对基础挑战。Cloudflare的“5秒盾”有不同等级。高级挑战会深度检测:
- Canvas指纹:调用
CanvasRenderingContext2D的API绘制文字或图形,然后toDataURL()获取哈希。你需要模拟实现这些API,并返回与真实浏览器一致的结果。 - WebGL指纹:检测WebGL渲染器和供应商字符串。
- 字体枚举:通过
document.fonts或span的offsetWidth检测已安装字体。 - 音频上下文指纹:
AudioContext的createOscillator和createDynamicsCompressor等。 - 浏览器特性:如
Notification,Permissions,WebRTC等。 这就像一场“军备竞赛”,你需要一个越来越完善的“浏览器环境模拟器”。一些开源项目如puppeteer-extra-plugin-stealth的思路就值得借鉴,但我们需要在Node.js的vm环境中实现类似功能。
- Canvas指纹:调用
方法B:纯Python解析与计算(高阶)对于某些特定时期、特定形式的挑战,其JS逻辑可能只是进行一系列固定的算术运算(如a = 12345; b = a + 18; c = b * 2; ... answer = c + len(hostname))。通过仔细逆向,你可以用Python重写这个计算过程。但这需要极高的逆向技巧,且一旦Cloudflare更换算法就失效,通用性差。
3.4 第四步:构造验证请求并获取Cookie
计算出jschl_answer后,我们还需要处理一个细节:Cloudflare经常要求answer加上当前域名hostname的长度。所以最终提交的答案可能是:final_answer = jschl_answer + len(“target-protected-site.com”)。
接下来,构造POST请求提交答案。这个请求的URL、参数名和格式需要从最初的挑战页面中分析得出。
import time import requests # 假设我们从页面中提取了以下信息 challenge_url = “https://target-protected-site.com/cdn-cgi/challenge-platform/h/b/cv/1234567890abcdef” # 这个URL需要从JS或页面中分析获取 jschl_vc = “extracted_vc_value” pass_value = “extracted_pass_value” ray_id = “extracted_ray_id” hostname = “target-protected-site.com” # 计算最终答案 (示例,具体逻辑看JS) final_answer = float(jschl_answer) + len(hostname) # 注意可能是浮点数 # 构造提交参数 payload = { ‘jschl_vc’: jschl_vc, ‘pass’: pass_value, ‘jschl_answer’: str(final_answer), # 有时需要保留多位小数 # 可能还有其他参数,如 ‘r’ } headers = { ‘User-Agent’: ‘Mozilla/5.0 ...‘, ‘Referer’: response.url, # 引用页是之前的503页面 ‘Content-Type’: ‘application/x-www-form-urlencoded’, } # **关键:模拟浏览器等待时间** # Cloudflare的JS中通常有 `setTimeout(function(){...}, 4000)`,要求计算后等待几秒再提交。 # 不等待或等待时间不对,都会导致验证失败。 wait_time = 4 # 具体时间从JS中的setTimeout参数获取 time.sleep(wait_time) # 发送验证请求 verify_response = requests.post(challenge_url, data=payload, headers=headers, allow_redirects=False) # 注意不要自动重定向 print(f”验证响应状态码: {verify_response.status_code}“) print(f”验证响应头: {verify_response.headers}“) # 如果成功,响应状态码通常是302,并且在响应头中会有Set-Cookie if ‘set-cookie’ in verify_response.headers: cookies = verify_response.headers[‘set-cookie’] print(f”获取到的Cookie: {cookies}“) # 从Cookie字符串中提取出cf_clearance的值 # 通常格式是:cf_clearance=abc123...; expires=...; path=/; ...重要提示:allow_redirects=False至关重要。我们需要手动处理302重定向,因为我们要从第一个验证响应的头部获取Set-Cookie。如果允许自动重定向,requests会跟随重定向发起新请求,但那个新请求可能不会包含cf_clearance的Set-Cookie头(因为浏览器会自动管理Cookie,但我们的requests会话还没拿到这个Cookie)。
3.5 第五步:使用cf_clearance访问目标内容
拿到cf_clearance后,将其加入到会话(Session)的Cookie中,然后重新访问最初的目标URL。
# 创建一个会话,保持Cookie session = requests.Session() # 解析Set-Cookie头,将其添加到会话中 # 简单处理:假设我们只关心cf_clearance import re match = re.search(r’cf_clearance=([^;]+)‘, cookies) if match: cf_clearance_value = match.group(1) session.cookies.set(‘cf_clearance’, cf_clearance_value, domain=’.target-protected-site.com’) # 通常也需要设置 __cf_bm # match_bm = re.search(r’__cf_bm=([^;]+)‘, cookies) # ... # 用携带了合法Cookie的会话访问原URL final_response = session.get(‘https://target-protected-site.com’, headers=headers) print(f”最终访问状态码: {final_response.status_code}“) if final_response.status_code == 200: print(“成功绕过5秒盾!”) # 现在可以正常解析final_response.text获取数据了 else: print(“绕过失败,可能原因:Cookie无效、环境检测未通过、挑战已更新。”)4. 常见问题、排查技巧与进阶对抗
4.1 为什么我的JS执行总是报错或答案错误?
这是最常见的问题,原因可能有多层:
- 环境补全不充分(最主要原因):
- 症状:执行时抛出
ReferenceError: window is not defined或TypeError: Cannot read property ‘getContext’ of undefined。 - 排查:仔细阅读错误栈,看是哪个浏览器特有的API未定义。使用浏览器开发者工具,在真实浏览器中触发挑战,在Console中查看
window、document、navigator等对象的具体结构和方法,然后在你的补环境代码中逐一模拟。重点关注navigator.userAgent、navigator.plugins、navigator.languages、screen.width/height、document.documentElement.clientWidth/Height。这些是基础指纹。
- 症状:执行时抛出
- 挑战JS代码被动态修改:
- 症状:你提取的JS代码执行后,计算结果与浏览器中运行的结果不一致。
- 排查:Cloudflare的JS可能包含“反调试”或“环境检测”代码。例如,它可能检查
Function.prototype.toString的输出、检查Error对象的栈轨迹长度,来判断是否在Node.js等非浏览器环境中运行。如果检测到,它会动态修改核心计算逻辑,导致你算出的答案错误。你需要逆向并绕过这些检测代码,或者在补环境时更彻底地模拟,例如重写Function.prototype.toString返回浏览器环境下的值。
- 答案提交格式或时机不对:
- 症状:提交答案后返回非302状态,或返回的Cookie无效。
- 排查:
- 等待时间:确认你模拟的等待时间(
time.sleep)是否与JS中的setTimeout延迟完全一致。有时是毫秒数,需要除以1000。 - 答案精度:
jschl_answer可能是浮点数,需要保留足够的小数位(如15位),直接str()转换可能丢失精度,导致验证失败。 - 请求参数:检查POST请求的URL和所有参数名(
jschl_vc,pass,jschl_answer,r,s等)是否与当前挑战页面中的完全一致。Cloudflare会不定期更换参数名。 - 请求头:检查
Referer、Origin、Content-Type等请求头是否与浏览器行为一致。有时缺少Origin头会导致失败。
- 等待时间:确认你模拟的等待时间(
- IP或会话被标记:
- 症状:即使流程完全正确,也频繁失败。
- 排查:你的服务器IP可能已经被Cloudflare列入高风险名单,触发更严格的验证(如Captcha图形验证码)。或者你的请求会话(由初始请求携带的Cookie如
__cf_bm关联)已被标记为异常。尝试更换IP地址,或确保整个流程(从首次503请求到提交答案)使用同一个requests.Session(),以维持会话状态。
4.2 如何应对更高级的挑战(如Canvas指纹)?
当基础算术挑战无法通过时,Cloudflare可能会下发包含环境检测的挑战。这时,你需要一个强大的“补环境”库。
策略:不要从零开始造轮子。可以研究或借鉴一些成熟项目的思路:
- Node.js VM 环境补全:寻找专门为Cloudflare反爬设计的Node.js库,它们通常提供了一个高度模拟的浏览器环境。
- 分离执行:将最核心、依赖浏览器环境最多的计算部分,通过一个无头浏览器(如Puppeteer)来执行,只获取计算结果,其他通信仍用
requests。这是一种混合方案。 - 指纹统一:确保你补的环境指纹(User-Agent, Accept-Language, Screen Resolution等)在所有请求中保持一致。不一致的指纹是强大的检测信号。
4.3 自动化与维护的考量
逆向“5秒盾”不是一个一劳永逸的方案。Cloudflare会持续更新其挑战机制和检测脚本。
- 监控与告警:在你的爬虫系统中,对503状态码和
cf_clearance获取失败建立监控。一旦失败率升高,立即告警。 - 动态解析:你的代码不能硬编码参数名(如
jschl_vc)和JS代码提取规则。必须编写健壮的解析器,能适应HTML结构的变化。 - 备用方案:始终准备一个备用方案,例如当纯请求模拟失败时,自动降级到使用无头浏览器方案(虽然慢,但成功率高)。
- 尊重
robots.txt与速率限制:即使你成功绕过了验证,也应遵守目标网站的robots.txt协议,并设置合理的请求间隔(如time.sleep(2))。过度频繁的请求即使有cf_clearance,也可能触发其他风控规则导致IP被封锁。
整个逆向过程,本质上是对Cloudflare安全模型的理解与对抗。它考验的不仅是编程和逆向技术,更是耐心、细致和对HTTP/浏览器细节的把握。每一次成功的绕过,都是对这些细节一次更深层次的掌握。记住,我们的目标不是攻击,而是在合理的范围内,让自动化的数据采集工作得以继续。