poc_report.md 8.9 KB

996盒子 Web 通行证登录链路 POC 报告

调研时间:2026-09-17 目标:确认账号密码登录的技术细节 工作目录:D:\传奇


核心结论:技术门槛比预期低

关键问题 结论 难度
1. 签名算法 HmacSHA1(不是 SHA256),key 在前端 JS 硬编码 k1ebbkxlojjdbnffnp3w2ykk3p625xnn ✅ 低
2. 密码是否加密 明文传输(POST body 直接是 password: "你的密码") ✅ 低
3. 登录接口 POST https://user-sdkv2.dhsf.996db.cn/api/user/v5w/login ✅ 低
4. Token 存储 返回的 data.token 存到 sessionStorage ✅ 低
5. 业务请求鉴权 业务接口通过 query string 传 ?GET-X-Token=<token>(不是 header) ✅ 低
6. 滑块触发条件 jiyanStatus == 1 时才需要极验 GT4,条件触发,不是每次都要 ✅ 中

整体评估:如果你只是做"账号密码登录 + session 复用",滑块只有在新设备/新 IP 才会触发。日常使用下,滑块几乎不构成门槛。


详细技术细节

1. 签名算法(已确认)

keySort 函数(前端源码 1637-1648 行):

keySort: function(val) {
  var arr = [];
  for (key in val) arr.push(key);
  arr = arr.sort();
  var signStr = "";
  arr.forEach(function(item, index) {
    signStr += item + "=" + (val[item] || val[item] == 0 ? val[item] : "")
      + (arr.length - 1 <= index ? "" : "&");
  });
  return signStr;
}

Hmacstr 函数(前端源码 1651-1656 行):

Hmacstr: function(e, key) {
  var result = CryptoJS.HmacSHA1(e, key);
  var sig = result.toString();
  sig = encodeURI(sig);
  return sig;
}

关键点:

  • 哈希算法 = HmacSHA1(HMAC-SHA1)
  • key = k1ebbkxlojjdbnffnp3w2ykk3p625xnn(前端硬编码,可在任何客户端复用)
  • 输出:hex 字符串再 URL encode
  • keySort 按 JS 默认的字典序排序(注意:大小写敏感,与 Python sorted() 结果可能不同,需测试)

Python 复现代码:

import hmac, hashlib, urllib.parse

KEY = "k1ebbkxlojjdbnffnp3w2ykk3p625xnn"

def key_sort(params: dict) -> str:
    # JS 默认 sort() 是按 UTF-16 code unit 排序
    keys = sorted(params.keys())  # Python str 排序和 JS Array.sort() 一致
    return "&".join(f"{k}={params[k]}" for k in keys)

def hmac_str(sign_str: str, key: str) -> str:
    sig = hmac.new(key.encode(), sign_str.encode(), hashlib.sha1).hexdigest()
    return urllib.parse.quote(sig, safe="")

2. 登录接口(已确认)

请求:

POST https://user-sdkv2.dhsf.996db.cn/api/user/v5w/login
Content-Type: application/json

Body(JSON,来自前端源码 956-981 行):

{
  "username": "你的账号",
  "password": "你的明文密码",
  "game_id": "<URL参数>",
  "vType": "csvKey",
  "appid": "<URL参数>",
  "promote_id": <URL参数,数字>,
  "_device_info": "<encodeURIComponent后的JSON字符串>",
  "lot_number": "<极验通过时附加>",
  "captcha_output": "<极验通过时附加>",
  "pass_token": "<极验通过时附加>",
  "gen_time": "<极验通过时附加>",
  "sign": "<HmacSHA1(keySort(...), key) 后的 URL-encoded hex>"
}

响应(成功):

{
  "code": 200,
  "msg": "成功",
  "data": {
    "token": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
    "uid": "12345678",
    "username": "你的账号",
    "mobile": "138****1234",
    "is_set_password": true,
    "age_status": 1
  }
}

3. Token 鉴权方式(重要!)

前端用 query string 传 token,不是 header(前端源码 1223、1462、1542 行):

GET https://user-sdkv2.dhsf.996db.cn/api/user/identification?GET-X-Token=<token>

注:前端 axios 拦截器(472-487 行)全部被注释掉了,所以不会自动加 X-Token header。每个接口都要手动在 URL 上加 ?GET-X-Token=。

Session 有效性探测建议:

  • 调 GET /api/user/getUserInfo?GET-X-Token=<token> 或类似的轻量接口
  • 返回 200 = 有效;返回 401 或 code != 200 = 失效
  • (具体接口名需要在登录后调一次看 network 才知道)

4. 极验触发逻辑(核心风险点)

触发流程(前端源码 690-700 行):

// 进入页面就调
axios.post(registerUrl + "/api/user/appParam", params)
  .then(res => {
    this.captchaId = res.data.data.captcha_id;
    this.jiyanStatus = res.data.data.jiyan_status;  // 1=需要滑块,0=不需要
    initGeetest4({captchaId, product: 'bind'}, callback);
  });

关键点:

  • 极验走的是标准 GT4(gcaptcha4.geetest.com/load,SDK 版本 v4.1.5)
  • jiyanStatus 是动态下发的,不是写死的
  • 服务端根据 IP/设备指纹/历史行为决定要不要弹滑块
  • 新设备/新 IP 大概率 = jiyanStatus=1;老登录环境可能 = 0

_device_info 字段(来自 Buryingpoint.js 的 Escalation 函数):

function Escalation(adkappid, sdkchannel, sdkId) {
  let data = {
    net_type, app_ver, appid, channel, sdk_ver,
    device: {
      type, model, id, id1, id2, id3, os,
      width, height, lang, net_type
    }
  }
  return encodeURIComponent(JSON.stringify(data))
}

注意:_device_info 中有些字段直接复用了 URL 参数(sdkappid、sdkchannel、sdkid),其他从浏览器环境取。这意味着:

  • width/height/os 等用 window.innerWidth 等浏览器 API
  • 在 Python 里没有浏览器,需要自己造一份合理的 _device_info(取 PC 常见值即可)
  • 第一次做的时候可能因为 _device_info 异常触发风控

5. 极验字段解释(极验通过后必须带)

字段 来源 说明
lot_number 极验 load 接口返回 任务流水号
captcha_output captcha.getValidate() 返回 极验 GT4 验证输出
pass_token captcha.getValidate() 返回 验证通过凭证
gen_time captcha.getValidate() 返回 验证时间戳

没有过滑块时这四个字段不传也能登录(如果 jiyanStatus=0)。


给工程实现的建议

最小可行方案(推荐先做这个)

┌──────────────────────────────────────────────┐
│ Step 1:requests 模拟登录(仅做签名)         │
│   - 拼 _device_info                          │
│   - keySort + HmacSHA1 签名                  │
│   - POST /api/user/v5w/login                 │
│   - 不处理滑块(jiyanStatus=0 时就够了)      │
├──────────────────────────────────────────────┤
│ Step 2:保存 token 到本地(加密)             │
│   - 路径:%APPDATA%\chuanshi_login\session.enc│
│   - 用本地机器指纹作为加密 key               │
│   - 存:token, uid, username, expire_estimate│
├──────────────────────────────────────────────┤
│ Step 3:每次使用:                           │
│   - 读本地 session                          │
│   - 调业务接口试 ?GET-X-Token=xxx            │
│   - 有效 → 继续                             │
│   - 失效 → 走 Step 1 重登                   │
└──────────────────────────────────────────────┘

滑块触发了怎么办(Plan B)

如果服务端风控变严,jiyanStatus 总是 1,再考虑:

  • 方案 A:弹浏览器让用户手动过滑块(最简单)
  • 方案 B:requests + wulu007/geetest-bypass(纯 Python)
  • 方案 C:完整 GT4 逆向(最后手段)

实施风险提示

风险 概率 影响 缓解
_device_info 异常触发风控 中 登录一直要滑块 第一次手动浏览器登录抓一份真实值,后续复用
token 有效期短(天级别) 中 频繁触发重登 加探测逻辑,自动重登
签名 key 被服务端改 低 脚本失效 程序启动时做一次接口校验
极验 GT4 算法升级 低 重登失败 走 Playwright 兜底

文件清单

文件 用途
debug_logs/login_page.html 登录页完整源码(已抓取到本地,1676 行)
debug_logs/buryingpoint.js _device_info 的生成逻辑(已抓取)
gt4.js / validateLogin.js / crypto.js / md5.js 已分析过,逻辑清楚

下一步

  1. 如果接受"最小方案":我可以先写一个 login.py 跑通一次"不触发滑块"场景下的登录
  2. 如果接受"混合方案":把"重登弹浏览器"作为兜底,主路径走 requests
  3. 如果坚持做"完整 GT4 逆向":建议另开一个项目专门研究,不和这个混在一起