T0级加密 企业信用接口校验:加速乐 → CTCT → 瑞数 → 极验4代,一条链路打穿

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”变成了“可复现、可调试、可维护”的工程流程。

十八、效果图

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值