用户访问 cursor.com
│
▼
┌─────────────────────────────────────────────────────┐
│ KasadaBot Protection Layer │
│ (149e9513-01fa-4fb0-aad4-566afd725d1b 路径标识) │
└──────────────┬──────────────────────────────────────┘
│
┌──────────┴──────────┐
▼ ▼
/fp 指纹采集 /tl 令牌提交
(fingerprint) (token layer)
│
▼
┌─────────────────────┐
│ KP_UIDz Cookie │ → 携带至业务请求
└──────────┬──────────┘
│
▼
POST /api/chat (SSE流式响应)
┌─────────────────────────────────────────────────────────────────────┐
│ 数据流转图 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 环境指纹采集 J() │
│ ┌───────────────────────────────────────────┐ │
│ │ p: 是否自动化浏览器检测 (devtools/phantom)│ │
│ │ S: 0.4043... (固定混淆数值) │ │
│ │ w: WebGL渲染器信息 t(window) │ │
│ │ s: 导航器/驱动器检测 W(window) │ │
│ │ h: UserAgent检测 N(window) │ │
│ │ b: console检测 e() │ │
│ │ d: devtools开启状态 j() │ │
│ └──────────────────┬────────────────────────┘ │
│ │ │
│ ▼ │
│ 密钥生成 │
│ ┌──────────────────────────────┐ │
│ │ k = btoa('juvsu') + ... │ ← RC4解混淆后的硬编码密钥片段 │
│ │ a = btoa('EoXvw') + ... │ ← 多段字符串拼接 │
│ │ password = k + a (concat) │ │
│ └──────────────────┬───────────┘ │
│ │ │
│ ▼ │
│ PBKDF2 密钥派生 │
│ ┌────────────────────────────────────────┐ │
│ │ 输入: password (UTF-8) │ │
│ │ salt: crypto.getRandomValues(16B) │ │
│ │ iter: 100,000 次 │ │
│ │ hash: SHA-256 │ │
│ │ 输出: AES-256-CBC Key │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ AES-256-CBC 加密 │
│ ┌────────────────────────────────────────┐ │
│ │ 明文: JSON.stringify(fingerprint_obj) │ │
│ │ IV: crypto.getRandomValues(16B) │ │
│ │ Key: 上步派生结果 │ │
│ └──────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ 拼接编码 │
│ ┌─────────────────────────────────────┐ │
│ │ output = Base64(salt + iv + cipher)│ 共 32 + len(cipher) 字节 │
│ └──────────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ POST /tl (octet-stream 二进制请求体) │
│ ← 响应: {"reload": true} 表示令牌有效 │
│ │
└─────────────────────────────────────────────────────────────────────┘
/fp 请求
└─→ 加载 ips.js (携带 KP_UID 参数)
└─→ Set-Cookie: KP_UIDz-ssn = <会话令牌>
└─→ Set-Cookie: KP_UIDz = <持久令牌>
└─→ 后续所有请求携带此Cookie
└─→ /api/chat 等业务接口鉴权
二进制请求体布局 (推测结构):
┌─────────────────────────────────────────┐
│ [0-3] Magic / Version Header │
│ [4-7] Timestamp (Unix ms) │
│ [8-23] KP_UIDz Token Binding │
│ [24-39] HMAC-SHA256 of above │
│ [40+] AES加密的指纹JSON │
│ [40-55] salt (16 bytes) │
│ [56-71] IV (16 bytes) │
│ [72+] ciphertext │
└─────────────────────────────────────────┘
服务端 客户端
│ │
│── GET /fp?x-kpsdk-v=j-1.2.308 ──→│
│ │ 解析响应中的 KPSDK:MC:... 消息
│ │ 包含: challenge_token
│ │
│← POST x-kpsdk-im (challenge) ────│
│ │ 执行 c.js 中的 PoW 计算
│ │ 采集浏览器指纹
│ │ 加密指纹数据
│ │
│← POST /tl (binary payload) ──────│
│ Headers: │
│ KP_UIDz-ssn, KP_UIDz cookies │
│ │
│── Set-Cookie: KP_UIDz=... ───────→│
│ {"reload": true} │
#!/usr/bin/env python3
"""
Cursor.com Kasada 绕过 + AI Chat 接口调用复现
注意: 本代码仅用于安全研究和学习目的
依赖: pip install httpx cryptography
"""
import os
import json
import base64
import struct
import time
import asyncio
import hashlib
import secrets
from typing import Optional
import httpx
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes, padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
# ============================================================
# 1. 常量与配置
# ============================================================
BASE_URL = "https://cursor.com"
USER_AGENT = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/134.0.6998.205 Safari/537.36"
)
# Kasada 路径标识符 (从请求日志中提取)
KASADA_UUID_1 = "149e9513-01fa-4fb0-aad4-566afd725d1b"
KASADA_UUID_2 = "2d206a39-8ed7-437e-a3be-862e0f06eea3"
# PBKDF2 密码 (从 c.js 混淆代码中提取的半硬编码值)
# 注意: 实际值需要运行 c.js 才能得到完整字符串
# 以下是根据代码分析的近似结构
PBKDF2_PASSWORD_K_SEED = "juvsu" # juvsu → btoa → "anV2c3U="
PBKDF2_PASSWORD_A_SEED = "EoXvw" # mgiimgtq7 相关
PBKDF2_ITERATIONS = 100_000
# ============================================================
# 2. 加密工具函数
# ============================================================
class AESCBCEncryptor:
"""AES-256-CBC 加密器,对应 c.js 中的 D() 函数"""
@staticmethod
def derive_key(password: str, salt: bytes) -> bytes:
"""
PBKDF2-SHA256 密钥派生
对应: crypto.subtle.deriveKey({name:'PBKDF2', hash:'SHA-256', iterations:100000})
"""
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # AES-256 = 32 bytes
salt=salt,
iterations=PBKDF2_ITERATIONS,
backend=default_backend()
)
return kdf.derive(password.encode('utf-8'))
@staticmethod
def encrypt(password: str, plaintext: str) -> str:
"""
完整加密流程:
password + random_salt → PBKDF2 → AES_key
plaintext → PKCS7 padding → AES-CBC(key, random_iv) → ciphertext
output = Base64(salt[16] + iv[16] + ciphertext)
"""
# 生成随机 salt 和 IV
salt = secrets.token_bytes(16)
iv = secrets.token_bytes(16)
# 派生 AES 密钥
aes_key = AESCBCEncryptor.derive_key(password, salt)
# PKCS7 填充
padder = padding.PKCS7(128).padder()
padded_data = padder.update(plaintext.encode('utf-8')) + padder.finalize()
# AES-CBC 加密
cipher = Cipher(
algorithms.AES(aes_key),
modes.CBC(iv),
backend=default_backend()
)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(padded_data) + encryptor.finalize()
# 拼接: salt + iv + ciphertext → Base64
combined = salt + iv + ciphertext
return base64.b64encode(combined).decode('ascii')
@staticmethod
def decrypt(password: str, encoded: str) -> str:
"""解密验证函数"""
data = base64.b64decode(encoded)
salt = data[:16]
iv = data[16:32]
ciphertext = data[32:]
aes_key = AESCBCEncryptor.derive_key(password, salt)
cipher = Cipher(
algorithms.AES(aes_key),
modes.CBC(iv),
backend=default_backend()
)
decryptor = cipher.decryptor()
padded = decryptor.update(ciphertext) + decryptor.finalize()
unpadder = padding.PKCS7(128).unpadder()
plaintext = unpadder.update(padded) + unpadder.finalize()
return plaintext.decode('utf-8')
# ============================================================
# 3. 指纹数据构造
# ============================================================
class FingerprintCollector:
"""
模拟 c.js 中 J() 函数的指纹采集
对应字段: p, S, w, s, h, b, d
"""
@staticmethod
def check_automation() -> bool:
"""p: 检测自动化工具特征"""
return False # 正常浏览器返回 False
@staticmethod
def get_fixed_value() -> float:
"""S: 固定混淆数值"""
return 0.4043074801008981
@staticmethod
def get_webgl_info() -> Optional[dict]:
"""
w: WebGL渲染器信息
对应 t(window) 函数,获取 UNMASKED_VENDOR/RENDERER
"""
return {
"v": "Google Inc. (NVIDIA)",
"r": "ANGLE (NVIDIA, NVIDIA GeForce RTX 3080 Direct3D11 vs_5_0 ps_5_0, D3D11)"
}
@staticmethod
def check_navigator() -> str:
"""
s: 导航器/WebDriver检测结果
对应 W(window) 函数
"""
# btoa("5p9?en") 相关
return base64.b64encode(b"5p9?en").decode() + \
base64.b64encode(b"EoXvw").decode() + \
"false"
@staticmethod
def check_useragent(ua: str) -> bool:
"""h: UA 中是否包含 'headless'"""
return "headless" in ua.lower()
@staticmethod
def check_console() -> bool:
"""b: console 对象检测"""
return False
@staticmethod
def get_devtools_state() -> dict:
"""d: DevTools 开启状态"""
return {
"isOpen": False,
"orientation": None
}
def collect(self) -> dict:
"""收集完整指纹"""
ua = USER_AGENT
return {
"p": self.check_automation(),
"S": self.get_fixed_value(),
"w": self.get_webgl_info(),
"s": self.check_navigator(),
"h": self.check_useragent(ua),
"b": self.check_console(),
"d": self.get_devtools_state()
}
# ============================================================
# 4. 密码生成 (还原 c.js 中 k 和 a 的生成逻辑)
# ============================================================
class PasswordGenerator:
"""
还原 c.js 中 k (来自t函数) 和 a (来自W函数) 的生成
"""
@staticmethod
def _rc4_decrypt(ciphertext_b64: str, key: str) -> str:
"""
还原 c.js 中的 y() 函数 (RC4-like KSA/PRGA)
用于解密混淆字符串
"""
# Base64 解码
decoded = base64.b64decode(ciphertext_b64).decode('latin-1')
# KSA
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + ord(key[i % len(key)])) % 256
S[i], S[j] = S[j], S[i]
# PRGA + XOR
result = []
i = j = 0
for char in decoded:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
result.append(chr(ord(char) ^ S[(S[i] + S[j]) % 256]))
return ''.join(result)
@staticmethod
def generate_k_fragment() -> str:
"""
t() 函数末尾生成的 k 值
基于 btoa("juvsu") 和一系列 String.fromCharCode 调用
"""
# btoa("juvsu") = "anV2c3U="
part1 = base64.b64encode(b"juvsu").decode()
# 以下 fromCharCode 值从混淆代码的数学表达式计算
# 示例: String.fromCharCode(0x1*0x9bf + -0xa*-0x36f + -0x2be0 - ...)
# 实际需要执行JS才能获得精确值,以下是近似还原
char_codes_k = [
102, 51, 56, 100, 118, # "f38dv" → juvsu相关
55, 98, 106, 88, 105, 115 # "7bjXis" → yHLWo相关
]
part2 = ''.join(chr(c) for c in char_codes_k)
# btoa("7bjXis")
part3 = base64.b64encode(b"7bjXis").decode()
return part1 + part2 + part3
@staticmethod
def generate_a_fragment() -> str:
"""
W() 函数末尾生成的 a 值
基于 btoa("5p9?en") 和 btoa("mgiimgtq7") 等
"""
part1 = base64.b64encode(b"5p9?en").decode()
char_codes_a = [
109, 103, 105, 109, 103, 116, 113, 55 # "mgiimgtq7"
]
part2 = ''.join(chr(c) for c in char_codes_a)
part3 = base64.b64encode(b"EoXvw").decode() # btoa("mgiimgtq7")
return part1 + part2 + part3
def get_password(self) -> str:
"""获取完整 PBKDF2 密码"""
k = self.generate_k_fragment()
a = self.generate_a_fragment()
return k + a
# ============================================================
# 5. Kasada 请求处理
# ============================================================
class KasadaClient:
"""处理 Kasada bot-protection 的完整流程"""
def __init__(self):
self.session = httpx.Client(
base_url=BASE_URL,
headers={"User-Agent": USER_AGENT},
follow_redirects=True,
timeout=30.0
)
self.kp_uid = None
self.kp_uid_ssn = None
self.fp_gen = FingerprintCollector()
self.pw_gen = PasswordGenerator()
def get_fingerprint_page(self) -> str:
"""
步骤1: GET /fp 获取挑战页面
提取 KPSDK:MC:... 消息中的 challenge token
"""
url = f"/{KASADA_UUID_1}/{KASADA_UUID_2}/fp"
params = {"x-kpsdk-v": "j-1.2.308"}
resp = self.session.get(url, params=params)
resp.raise_for_status()
# 提取 challenge token
html = resp.text
import re
match = re.search(r"postMessage\('KPSDK:MC:([^']+)'", html)
if match:
challenge_token = match.group(1)
print(f"[+] Challenge token: {challenge_token[:50]}...")
return challenge_token
raise ValueError("无法提取 challenge token")
def build_tl_payload(self, challenge_token: str) -> bytes:
"""
步骤2: 构造 /tl 的二进制请求体
结构: 版本头 + 时间戳 + 挑战响应 + 加密指纹
"""
# 收集指纹
fingerprint = self.fp_gen.collect()
# 获取加密密码
password = self.pw_gen.get_password()
# 加密指纹数据
encrypted_fp = AESCBCEncryptor.encrypt(
password=password,
plaintext=json.dumps(fingerprint, separators=(',', ':'))
)
# 构造时间戳
ts = int(time.time() * 1000)
# 解析 challenge token 中的各字段
# 格式: AALsWax...:XgpV...:Rgxn...:Wwtb...:VwtX...
token_parts = challenge_token.split(':')
# 构造二进制 payload
# 注意: 实际格式是 Kasada 私有协议,以下为近似结构
payload_json = {
"st": ts,
"d": encrypted_fp,
"ct": challenge_token,
"v": "j-1.2.308"
}
# Kasada 使用自定义二进制序列化,这里用 JSON 作为近似
return json.dumps(payload_json).encode('utf-8')
def submit_token(self, challenge_token: str) -> bool:
"""
步骤3: POST /tl 提交令牌
"""
url = f"/{KASADA_UUID_1}/{KASADA_UUID_2}/tl"
payload = self.build_tl_payload(challenge_token)
headers = {
"Content-Type": "application/octet-stream",
"Origin": BASE_URL,
"Referer": f"{BASE_URL}/{KASADA_UUID_1}/{KASADA_UUID_2}/fp?x-kpsdk-v=j-1.2.308",
}
# 添加已有 Cookie
if self.kp_uid:
self.session.cookies.set("KP_UIDz", self.kp_uid)
if self.kp_uid_ssn:
self.session.cookies.set("KP_UIDz-ssn", self.kp_uid_ssn)
resp = self.session.post(url, content=payload, headers=headers)
# 提取新的 KP_UIDz Cookie
if "KP_UIDz" in resp.cookies:
self.kp_uid = resp.cookies["KP_UIDz"]
print(f"[+] KP_UIDz: {self.kp_uid[:40]}...")
if "KP_UIDz-ssn" in resp.cookies:
self.kp_uid_ssn = resp.cookies["KP_UIDz-ssn"]
result = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {}
if result.get("reload"):
print("[+] Token accepted (reload=true)")
return True
print(f"[-] Token response: {resp.text[:100]}")
return False
def setup(self) -> bool:
"""完整的 Kasada 初始化流程"""
try:
challenge_token = self.get_fingerprint_page()
return self.submit_token(challenge_token)
except Exception as e:
print(f"[-] Kasada setup failed: {e}")
return False
# ============================================================
# 6. Cursor Chat API 调用
# ============================================================
class CursorChatClient:
"""调用 cursor.com/api/chat 接口"""
def __init__(self, kasada_client: KasadaClient):
self.kasada = kasada_client
self.conversation_id = self._generate_id()
@staticmethod
def _generate_id(length: int = 16) -> str:
"""生成类似 'ZkU2LSyKjo9UIoTo' 的随机ID"""
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return ''.join(secrets.choice(chars) for _ in range(length))
def build_chat_payload(
self,
user_message: str,
context_path: str = "/docs/api",
turn_number: int = 1
) -> dict:
"""
构造 /api/chat 请求体
对应 #81 请求的 Body 结构
"""
return {
"context": [
{
"type": "file",
"content": "",
"filePath": context_path
}
],
"id": self.conversation_id,
"messages": [
{
"parts": [
{
"type": "text",
"text": user_message
}
],
"id": self._generate_id(),
"role": "user"
}
],
"trigger": "submit-message"
}
def chat(self, message: str, context_path: str = "/docs/api") -> str:
"""
发送消息并接收 SSE 响应
"""
payload = self.build_chat_payload(message, context_path)
cookies = {
"generaltranslation.locale-routing-enabled": "true",
"generaltranslation.referrer-locale": "cn",
}
if self.kasada.kp_uid:
cookies["KP_UIDz"] = self.kasada.kp_uid
if self.kasada.kp_uid_ssn:
cookies["KP_UIDz-ssn"] = self.kasada.kp_uid_ssn
headers = {
"Content-Type": "application/json",
"Origin": BASE_URL,
"Referer": f"{BASE_URL}/cn{context_path}",
"User-Agent": USER_AGENT,
"Accept": "text/event-stream",
}
full_text = []
with httpx.Client(timeout=60.0) as client:
with client.stream(
"POST",
f"{BASE_URL}/api/chat",
json=payload,
headers=headers,
cookies=cookies
) as resp:
resp.raise_for_status()
# 解析 SSE 流
for line in resp.iter_lines():
if not line.startswith("data: "):
continue
data_str = line[6:] # 去掉 "data: " 前缀
try:
data = json.loads(data_str)
except json.JSONDecodeError:
continue
event_type = data.get("type")
if event_type == "text-delta":
delta = data.get("delta", "")
full_text.append(delta)
print(delta, end="", flush=True)
elif event_type == "text-end":
print() # 换行
elif event_type == "end":
break
return ''.join(full_text)
def send_analytics_event(
self,
message: str,
page: str,
turn_number: int
):
"""
发送 Vercel Analytics 事件 (对应 #82 请求)
无加密,仅记录用户行为
"""
payload = {
"o": f"{BASE_URL}/cn{page}",
"sv": "0.1.3",
"sdkn": "@vercel/analytics/next",
"sdkv": "1.6.1",
"ts": int(time.time() * 1000),
"dp": "/[lang]/docs/[...slug]",
"r": "",
"en": "Submit prompt",
"ed": {
"message": message,
"page": page,
"turnNumber": turn_number
}
}
cookies = {}
if self.kasada.kp_uid:
cookies["KP_UIDz"] = self.kasada.kp_uid
with httpx.Client() as client:
resp = client.post(
f"{BASE_URL}/_vercel/insights/event",
json=payload,
headers={
"Content-Type": "application/json",
"Origin": BASE_URL,
"Referer": f"{BASE_URL}/cn{page}",
},
cookies=cookies
)
return resp.text
# ============================================================
# 7. 加密验证测试
# ============================================================
def test_aes_encryption():
"""验证 AES-CBC 加密/解密的正确性"""
print("\n=== AES-256-CBC 加密测试 ===")
password = "test_password_from_js"
plaintext = json.dumps({
"p": False,
"S": 0.4043074801008981,
"w": {"v": "Google Inc.", "r": "ANGLE (NVIDIA)"},
"s": "dGVzdA==",
"h": False,
"b": False,
"d": {"isOpen": False, "orientation": None}
})
# 加密
encrypted = AESCBCEncryptor.encrypt(password, plaintext)
print(f"加密结果 (Base64): {encrypted[:60]}...")
print(f"总长度: {len(base64.b64decode(encrypted))} bytes")
print(f" - salt: 16 bytes [0:16]")
print(f" - iv: 16 bytes [16:32]")
print(f" - data: {len(base64.b64decode(encrypted)) - 32} bytes [32:]")
# 解密验证
decrypted = AESCBCEncryptor.decrypt(password, encrypted)
assert json.loads(decrypted) == json.loads(plaintext), "解密验证失败!"
print(f"[✓] 解密验证通过")
return encrypted
def test_password_generation():
"""测试密码生成器"""
print("\n=== 密码生成测试 ===")
pw_gen = PasswordGenerator()
password = pw_gen.get_password()
print(f"生成的密码: {password[:40]}...")
print(f"密码长度: {len(password)} chars")
return password
def test_fingerprint_collection():
"""测试指纹采集"""
print("\n=== 指纹采集测试 ===")
collector = FingerprintCollector()
fp = collector.collect()
print(f"指纹数据:")
for k, v in fp.items():
print(f" {k}: {v}")
return fp
# ============================================================
# 8. 主程序入口
# ============================================================
def main():
print("=" * 60)
print("Cursor.com 加密机制复现 - 仅用于研究目的")
print("=" * 60)
# 运行单元测试
test_aes_encryption()
test_password_generation()
test_fingerprint_collection()
# 初始化 Kasada 客户端
print("\n=== Kasada 初始化流程 ===")
kasada = KasadaClient()
# 注意: 实际 Kasada 需要完整的浏览器环境执行 JS
# 这里展示流程,实际 /tl 请求体格式是私有协议
print("[!] 注意: 完整绕过需要执行 c.js 中的 PoW 计算")
print("[!] 建议使用 Playwright/Puppeteer 执行真实 JS")
# 模拟已有 Cookie 的情况
# 实际使用时替换为真实从浏览器获取的 Cookie
kasada.kp_uid = "your_kp_uid_cookie_here"
# 初始化 Chat 客户端
chat_client = CursorChatClient(kasada)
print("\n=== Chat API 调用演示 ===")
print("发送消息: 'hi'")
print("响应: ", end="")
# 注意: 需要有效的 KP_UIDz Cookie 才能成功调用
# response = chat_client.chat("hi", "/docs/api")
# print(f"\n完整响应: {response}")
print("\n[i] 请先通过浏览器获取有效的 KP_UIDz Cookie")
print("[i] 然后替换 kasada.kp_uid 的值再运行")
if __name__ == "__main__":
main()
层级 1 (最外层): Kasada Bot Protection
├─ 采集 JavaScript 指纹 (12+项检测)
├─ AES-256-CBC 加密指纹 + PBKDF2 密钥派生
├─ 二进制 PoW 令牌提交
└─ 颁发 KP_UIDz Cookie
层级 2: 业务接口鉴权
└─ 依赖 KP_UIDz Cookie (无独立签名)
层级 3: 行为追踪
└─ Vercel Analytics (明文,无加密)