T0级加密 企业信用接口校验:加速乐 → CTCT → 瑞数 → 极验4代,一条链路打穿
概要
提示:仅供学习,不得用做商业交易,如有侵权请及时联系
网站:aHR0cHM6Ly9zaGltaW5nLmdzeHQuZ292LmNuL2NvcnAtcXVlcnktaG9tZXBhZ2UuaHRtbA==
很多人分析 企业信用 这类站点时,会先打开浏览器,然后从 Network 里复制 Cookie、复制 Header、复制表单参数。
这样可以临时跑通一次,但问题很明显:
Cookie 会过期;
接口路径会变;
412/521 会反复出现;
极验票据只能用一次;
复制出来的请求很难长期复现。
真正稳定的方式不是“复制一次浏览器请求”,而是把整条接口校验链路拆出来。
本次链路顺序是:
接口请求
↓
加速乐 JS Cookie
↓
CTCT 环境校验
↓
瑞数动态加密/动态路径
↓
极验4代 GT4
↓
业务接口/结果页
本文只讲核心思路和部分真实代码。站点统一称为 企业信用。文中所有网址均使用 Base64 编码,不直接展示明文 URL。
一、先把 URL 全部 Base64 化
为了避免文章里直接出现目标地址,我把所有关键地址统一做 Base64 编码。
示例:
import base64
URLS_B64 = {
"HOME": "aHR0cHM6Ly9zaGltaW5nLmdzeHQuZ292LmNuL2NvcnAtcXVlcnktaG9tZXBhZ2UuaHRtbA==",
"SEARCH_TEST": "aHR0cHM6Ly9zaGltaW5nLmdzeHQuZ292LmNuL2NvcnAtcXVlcnktc2VhcmNoLXRlc3QuaHRtbA==",
"SEARCH_RESULT": "aHR0cHM6Ly9zaGltaW5nLmdzeHQuZ292LmNuL2NvcnAtcXVlcnktc2VhcmNoLTEuaHRtbA==",
"GT4_LOAD": "aHR0cHM6Ly9nY2FwdGNoYTQuZ2VldGVzdC5jb20vbG9hZA==",
"GT4_VERIFY": "aHR0cHM6Ly9nY2FwdGNoYTQuZ2VldGVzdC5jb20vdmVyaWZ5",
"GT4_STATIC": "aHR0cHM6Ly9zdGF0aWMuZ2VldGVzdC5jb20v",
}
def b64_url(name: str) -> str:
return base64.b64decode(URLS_B64[name]).decode("utf-8")
后续代码里只使用:
home_url = b64_url("HOME")
search_test_url = b64_url("SEARCH_TEST")
result_url = b64_url("SEARCH_RESULT")
这样文章中不会直接暴露明文网址,同时代码仍然可以正常运行。
二、第一层:接口请求先遇到加速乐
直接请求 企业信用 接口时,第一层常见响应是:
521
或者 HTML 里出现:
document.cookie
location.href
go({...})
__jsl_clearance
这就是加速乐 JS Cookie 校验。
识别函数可以这样写:
def response_has_jsl(response):
return (
response.status_code == 521
or "__jsl_clearance" in response.text
or "document.cookie" in response.text and "location.href" in response.text
or "go({" in response.text
)
加速乐一般有两种形态。
三、加速乐第一种:document.cookie 表达式
这种页面里会有一段 JS,大概逻辑是:
计算 cookie -> 写 document.cookie -> 跳转
处理方式很直接:
提取 script
截出 document.cookie 表达式
用 JS 环境执行
拿到 cookie
写入请求 Cookie
重试原接口
真实代码片段:
def solve_jsl_cookie(html, current_url):
scripts = re.findall(r'<script[^>]*>(.+?)</script>', html, re.S | re.I)
if not scripts:
raise RuntimeError("没有在 JSL 页面找到 script")
script = scripts[0]
if "document.cookie" in script:
match = re.search(r'document\.cookie\s*=\s*(.*?);location\.href', script, re.S)
if not match:
raise RuntimeError("没有识别出 document.cookie 表达式")
context = js2py.EvalJs()
context.execute("cookie_value = " + match.group(1))
return str(context.cookie_value).split(";", 1)[0]
拿到结果后合并 Cookie:
def apply_cookie_pair(cookie_items, pair):
if "=" not in pair:
return
name, value = pair.split("=", 1)
name = name.strip()
if name and name.lower() != "httponly":
cookie_items[name] = value.strip()
注意:
HttpOnly 是属性,不是 Cookie 键值。
四、加速乐第二种:go({…}) 混淆计算
另一种常见形式是:
go({...})
这类不能只截表达式,需要模拟最小浏览器环境。
真实处理代码如下:
if "go({" in script:
parsed = urlparse(current_url)
pathname = parsed.path or "/"
search = f"?{parsed.query}" if parsed.query else ""
jsdom_env = (
'const jsdom = require("jsdom");'
'const {JSDOM} = jsdom;'
f'const dom = new JSDOM("", {{url: "{current_url}"}});'
"window = dom.window;"
"document = window.document;"
f'location = {{pathname: "{pathname}", search: "{search}", href: "{current_url}"}};'
"setTimeout = function(fn) {fn();};"
"alert = function() {};"
f'Object.defineProperty(window.navigator, "userAgent", {{value: "{USER_AGENT}"}});'
'Object.defineProperty(window.navigator, "webdriver", {value: undefined});'
)
js2 = jsdom_env + script
request_arg = re.findall(r'go\({(.*?)}\)', js2, re.S)[-1]
js3 = re.sub(r'go\(\{.*?\}\)\s*$', '', js2, flags=re.S)
final_js = (
js3
+ "\nfunction request() {var cookie = go({"
+ request_arg
+ "}); return cookie || document.cookie;}"
)
return execjs.compile(final_js, cwd=str(PROJECT_DIR)).call("request").split(";", 1)[0]
这一步的关键是:
不需要真的打开浏览器;
只要补齐 window/document/location/navigator;
让 go({...}) 正常执行即可。
五、第二层:CTCT 环境校验
过完加速乐后,接口可能返回:
412
或者页面里出现:
ctct_bundle
1eeb27c_
努力加载中
Environment Checking
识别函数:
def should_solve_ctct(response):
text = response.text
if response.status_code in (403, 412) and ("ctct_bundle" in text or "1eeb27c_" in text):
return True
return "努力加载中" in text or "Environment Checking" in text
CTCT 页面一般会引用两类脚本:
<script src="/1eeb27c_xxxxx.js"></script>
<script src="/ctct_bundle_xxxxx.js"></script>
处理思路:
提取两个脚本地址
下载配置脚本
下载 bundle 脚本
丢给 Node/jsdom 执行
输出 CT_* Cookie
更新 Cookie 后重试接口
真实代码片段:
def solve_ctct(html, current_url, referer, cookie_header):
script_srcs = re.findall(r'<script[^>]+src=["\']([^"\']+)', html, re.I)
script_urls = [urljoin(current_url, src) for src in script_srcs]
config_url = next((item for item in script_urls if "1eeb27c_" in item), None)
bundle_url = next((item for item in script_urls if "ctct_bundle" in item), None)
if not config_url or not bundle_url:
raise RuntimeError("没有在 412 页面找到 CTCT 脚本")
config_js = fetch_script(config_url, referer, cookie_header)
bundle_js = fetch_script(bundle_url, referer, cookie_header)
payload = {
"url": current_url,
"html": html,
"configJs": config_js,
"bundleJs": bundle_js,
"bundleUrl": bundle_url,
"userAgent": USER_AGENT,
"waitMs": 8000,
}
result = subprocess.run(
["node", str(PROJECT_DIR / "ctct_solver.js")],
input=json.dumps(payload, ensure_ascii=False),
text=True,
encoding="utf-8",
errors="replace",
capture_output=True,
cwd=PROJECT_DIR,
timeout=25,
check=False,
)
if result.returncode != 0:
raise RuntimeError((result.stderr or "").strip() or "CTCT solver failed")
return json.loads(result.stdout).get("cookies") or {}
更新 Cookie:
ctct_cookies = solve_ctct(response.text, current_url, referer, build_cookie_header(cookie_items))
for name, value in ctct_cookies.items():
if name.startswith("CT_"):
cookie_items[name] = value
这一步重点不是硬编码某个 CT_*,而是让当前页面引用的脚本自己跑出本轮会话需要的 Cookie。
六、第三层:瑞数动态路径与加密数据
CTCT 之后,有些接口还会叠一层瑞数。
瑞数的典型特点是:
接口路径不是固定的;
请求路径可能由 JS 动态生成;
请求参数可能加密;
响应内容也可能是加密字符串;
状态码 400/412 后需要刷新环境 Cookie。
常见处理方式是:
初始化页面
执行环境 JS 拿 Cookie
调用 JS 生成动态后缀
AES/RSA 处理请求参数
请求接口
解密响应
1. AES 加密参数
真实代码片段:
def aes_encrypt(self, plaintext, key):
text_bytes = plaintext.encode("utf-8")
key_bytes = key.encode("utf-8")
cipher = AES.new(key_bytes, AES.MODE_ECB)
padded_text = pad(text_bytes, AES.block_size)
encrypted_bytes = cipher.encrypt(padded_text)
encrypted_b64 = base64.b64encode(encrypted_bytes).decode("utf-8")
return f"AES({quote(encrypted_b64, safe='')})"
2. RSA 加密 AES Key
def rsa_encrypt_aes_key(self, key):
public_key_b64 = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJyQCGUo+5Hg1dtOjzXC5GTUlWgiVg2UggYlPSgM4fE39wfJ3qJWOLYKSzGxpLhZtBSbMBJfJja60nSLdBJpAKcCAwEAAQ=="
public_key = RSA.import_key(base64.b64decode(public_key_b64))
cipher = PKCS1_v1_5.new(public_key)
encrypted = cipher.encrypt(key.encode("utf-8"))
return base64.b64encode(encrypted).decode("utf-8")
3. 动态接口后缀
瑞数经常不会让你直接请求固定路径,而是通过 JS 算出后缀。
真实代码片段:
def _call_houzui(self, path):
js_content = open(self.new_js_path, "r", encoding="utf-8").read()
return execjs.compile(js_content).call("houzui", path)
使用方式:
houzui = self._call_houzui("/api/path")
full_url = base_origin + houzui
文章里不展示明文接口地址,实际代码中可以继续使用 Base64 存储。
4. 解密响应
瑞数接口有时返回的不是直接 JSON,而是加密字符串。
真实处理代码:
def _decrypt_response_text(self, encrypted_text):
raw_text = (encrypted_text or "").strip()
if not raw_text:
raise ValueError("response body is empty")
candidates = [raw_text]
if len(raw_text) >= 2 and raw_text[0] == raw_text[-1] and raw_text[0] in ("'", '"'):
candidates.append(raw_text[1:-1].strip())
try:
parsed = json.loads(raw_text)
except Exception:
parsed = None
if isinstance(parsed, str):
candidates.append(parsed.strip())
seen = set()
for candidate in candidates:
candidate = (candidate or "").strip()
if not candidate or candidate in seen:
continue
seen.add(candidate)
if self.decrypt_ctx is None:
self.decrypt_ctx = execjs.compile(self.decrypt_js_code)
decrypted_text = self.decrypt_ctx.call("loadAESDecryptStr", candidate)
if decrypted_text:
return decrypted_text
raise ValueError("JS decrypt returned empty text.")
这一步的关键是:
不要假设响应一定是 JSON;
先判断是否字符串;
再尝试 JS 解密;
最后再 json.loads。
七、第四层:极验4代 GT4
前面三层解决的是接口链路环境问题。
但真正提交搜索或结果页时,还会遇到极验 4 代。
极验 4 代核心两个接口:
/load
/verify
本文里也不展示明文地址,仍然使用 Base64:
GT4_LOAD_URL = b64_url("GT4_LOAD")
GT4_VERIFY_URL = b64_url("GT4_VERIFY")
GT4_STATIC_URL = b64_url("GT4_STATIC")
八、GT4 /load:加载验证码题目
真实代码:
def _load(session: requests.Session, captcha_id: str, risk_type: str) -> dict[str, Any]:
cb = _callback()
params = {
"captcha_id": captcha_id,
"challenge": str(uuid4()),
"client_type": "web",
"risk_type": risk_type,
"lang": "zh",
"callback": cb,
}
response = session.get(
b64_url("GT4_LOAD"),
params=params,
timeout=20,
verify=False,
proxies={"http": "", "https": ""},
)
parsed = _parse_jsonp(response.text, cb)
data = parsed.get("data")
if not isinstance(data, dict):
raise GeetestSolveError(f"/load 无 data:{json.dumps(parsed, ensure_ascii=False)[:300]}")
return data
返回里重点看:
captcha_type
lot_number
payload
process_token
pow_detail
imgs
ques
当前主要处理:
word
icon
遇到:
nine
phrase
先跳过,重新加载。
九、word/icon:图像识别得到 userresponse
极验 4 代不是直接提交图片,而是提交点击坐标。
这个坐标字段叫:
userresponse
处理流程:
下载背景图 imgs
下载题目图 ques
用 ddddocr 检测背景候选框
用 OpenCV 匹配题图和候选框
生成多套 candidate
逐个提交 verify
真实代码片段:
def _solve_by_image_match_candidates(
session: requests.Session,
data: dict[str, Any],
scale_x: float,
scale_y: float,
) -> list[list[list[float]]]:
bg_bytes = _download(session, data["imgs"])
bg = cv2.imdecode(np.frombuffer(bg_bytes, np.uint8), cv2.IMREAD_COLOR)
boxes = dddd_service.detection(bg_bytes)
if not boxes or len(boxes) < len(data.get("ques") or []):
return []
questions = []
for q_path in data["ques"]:
q_bytes = _download(session, q_path)
questions.append(cv2.imdecode(np.frombuffer(q_bytes, np.uint8), cv2.IMREAD_COLOR))
crops = []
for x1, y1, x2, y2 in boxes:
crops.append(bg[y1:y2, x1:x2])
score_matrices = [
[[_similarity_raw(q, crop) for crop in crops] for q in questions],
[[_similarity(q, crop) for crop in crops] for q in questions],
]
这里用两套相似度:
原始灰度匹配
灰度增强 + 边缘匹配
这样比单模板匹配稳定。
十、同一道题要多 candidate,不要失败就换题
一开始只取最优匹配,很容易失败。
原因是:
3 个点里只要一个顺序错,整题就失败。
所以我改成多候选组合。
真实代码:
def _rank_assignments(score_matrix: list[list[float]], topn: int = 5) -> list[list[int]]:
score = np.array(score_matrix, dtype=float)
row_count, col_count = score.shape
if row_count == 0 or col_count < row_count:
return []
ranked: list[tuple[float, list[int]]] = []
for cols in permutations(range(col_count), row_count):
vals = [float(score[row, col]) for row, col in enumerate(cols)]
total = sum(vals) / len(vals) + min(vals) * 0.25
ranked.append((total, list(cols)))
ranked.sort(reverse=True, key=lambda item: item[0])
result: list[list[int]] = []
seen = set()
for _, cols in ranked:
key = tuple(cols)
if key in seen:
continue
seen.add(key)
result.append(cols)
if len(result) >= topn:
break
return result
成功日志会变成:
geetest_fail: attempt=4, type=icon, candidate=1/4
geetest_fail: attempt=4, type=icon, candidate=2/4
geetest_success: attempt=4, type=icon, candidate=3/4
这比“第一套失败就整题作废”稳定很多。
十一、GT4 的 w 参数:复用 GeekedTest 核心逻辑
识别只是得到坐标。
但 /verify 不接收裸坐标,它要加密后的:
w
这里复用 GeekedTest 的核心:
from geeked.sign import Signer, lotParser
真实代码:
def _build_w(captcha_id: str, data: dict[str, Any], risk_type: str, userresponse: Any) -> str:
lot_number = data["lot_number"]
pow_detail = data["pow_detail"]
base = {"ZAhG": "MwHu"}
base |= Signer.generate_pow(
lot_number,
captcha_id,
pow_detail["hashfunc"],
pow_detail["version"],
pow_detail["bits"],
pow_detail["datetime"],
"",
)
base |= lotParser.get_dict(lot_number)
base |= {
"biht": "1426265548",
"device_id": "",
"em": {"cp": 0, "ek": "11", "nt": 0, "ph": 0, "sc": 0, "si": 0, "wd": 1},
"ep": "123",
"geetest": "captcha",
"lang": "zh",
"lot_number": lot_number,
"passtime": random.randint(650, 1400),
"userresponse": userresponse,
}
return Signer.encrypt_w(json.dumps(base, separators=(",", ":")), data["pt"])
核心点:
Signer.generate_pow 生成 PoW;
lotParser 生成 lot_number 派生字段;
userresponse 放入点击答案;
encrypt_w 生成最终 w。
十二、GT4 /verify:提交答案拿票据
真实代码:
def _verify(
session: requests.Session,
captcha_id: str,
data: dict[str, Any],
risk_type: str,
userresponse: Any,
) -> dict[str, Any]:
cb = _callback()
params = {
"callback": cb,
"captcha_id": captcha_id,
"client_type": "web",
"lot_number": data["lot_number"],
"risk_type": risk_type,
"payload": data["payload"],
"process_token": data["process_token"],
"payload_protocol": "1",
"pt": "1",
"w": _build_w(captcha_id, data, risk_type, userresponse),
}
response = session.get(
b64_url("GT4_VERIFY"),
params=params,
timeout=25,
verify=False,
proxies={"http": "", "https": ""},
)
parsed = _parse_jsonp(response.text, cb)
return parsed.get("data") if isinstance(parsed.get("data"), dict) else parsed
成功后会有:
{
"seccode": {
"lot_number": "...",
"pass_token": "...",
"gen_time": "...",
"captcha_output": "..."
}
}
这些字段就是最终业务请求要带的东西。
十三、最终接口调度顺序
整个调度逻辑可以抽象成:
for attempt in range(max_retry):
response = request_business_api(cookie_items)
if response_has_jsl(response):
pair = solve_jsl_cookie(response.text, response.url)
apply_cookie_pair(cookie_items, pair)
continue
if should_solve_ctct(response):
ctct_cookies = solve_ctct(
response.text,
response.url,
referer,
build_cookie_header(cookie_items),
)
cookie_items.update(ctct_cookies)
continue
if should_solve_ruishu(response):
refresh_ruishu_env()
continue
if response_has_geetest(response):
captcha_params = solve_geetest_v4(
captcha_id=CAPTCHA_ID,
referer=referer,
save_file="captcha_token.json",
)
response = request_business_api(cookie_items, captcha_params)
break
break
真实工程中,最重要的是不要把四层混在一起。
应该分成四个独立处理器:
JSLHandler
CTCTHandler
RuishuHandler
GT4Handler
每层只做自己的事。
十四、业务提交前一定要重新解极验
极验票据不是长期令牌。
如果复用旧 captcha_token.json,很容易出现:
302 -> 某异常地址
curl: (52) Empty reply from server
这是因为旧票据已经失效,业务结果页不接受。
正确方式:
搜索条件检测通过后;
真正提交结果页前;
重新走一次 GT4 /load + /verify;
拿新票据提交。
示例:
captcha_params = solve_geetest_v4(
captcha_id=CAPTCHA_ID,
referer=referer,
save_file="captcha_token.json",
)
form_data = {
"searchword": keyword,
"tab": "ent_tab",
"province": "",
"page": "1",
}
form_data.update(captcha_params)
十五、推荐运行参数
如果追求稳定:
$env:GSXT_GEETEST_MAX_ATTEMPTS="40"
$env:GSXT_GEETEST_CANDIDATES_PER_CHALLENGE="4"
$env:GSXT_GEETEST_RISK_TYPES="ai,icon,word"
如果想减少单题验证次数:
$env:GSXT_GEETEST_CANDIDATES_PER_CHALLENGE="2"
如果想提高单题通过率:
$env:GSXT_GEETEST_CANDIDATES_PER_CHALLENGE="5"
如果想只刷 word:
$env:GSXT_GEETEST_RISK_TYPES="word"
十六、这条链路里最容易踩的坑
1. 521 不是接口挂了
521 通常是加速乐 JS Cookie 没过。
看响应里有没有:
document.cookie
__jsl_clearance
go({...})
2. 412 不一定是业务错误
412 很多时候是 CTCT 或瑞数环境校验。
看响应里有没有:
ctct_bundle
1eeb27c_
Environment Checking
3. 瑞数接口不要硬拼路径
路径可能是 JS 动态算出来的。
应该调用:
houzui = execjs.compile(js_content).call("houzui", path)
4. 极验票据不要复用
lot_number / pass_token / captcha_output 强时效,最好每次提交前新生成。
5. word/icon 不要只试一套点
多候选能显著提高通过率。
十七、总结
企业信用 这条链路不是单一验证码问题,而是多层接口校验叠加:
加速乐:计算 JS Cookie
CTCT:执行环境检测脚本,生成 CT_* Cookie
瑞数:动态路径、加密参数、加密响应
极验4代:/load 拿题,/verify 换 seccode
真正稳定的实现思路是:
不要复制浏览器一次性结果;
要还原每一层的生成逻辑;
每层失败就只刷新对应层;
最终业务请求只接收最新有效的 Cookie 和验证码票据。
做到这一步后,整条接口链路就从“碰运气复制 Cookie”变成了“可复现、可调试、可维护”的工程流程。
十八、效果图


2135

被折叠的 条评论
为什么被折叠?



