亿企赢逆向手记(一):登录链路,从 OAuth 回调到最终会话 Cookie

张 张站长 · 2026-09-08 17:55 · 技术-逆向 · 36 次浏览 · 0 条评论

五请求跨两域的登录链路:设备 cookie+预检+MD5 口令+OAuth 授权码换会话+弱密码才触发的验证码,每个接口的完整入参与响应分支。

「亿企赢逆向手记」第一篇。基于我自己账号的数据导出自动化,把亿企赢(17dz.com)的整条登录链路逐个接口拆开。文中真实手机号、密码、企业名均已用占位符替换,其余接口/参数/结构都是抓包实测的原样。

亿企赢的登录不是「一个接口丢账号密码换 token」,而是一条五请求的链路,跨了两个域(ucapi 登录域、xqy-portal-web 业务域),核心是一次 OAuth2.0 authorization-code 风格的跨域信任传递。这篇把每个接口的方法、URL、完整入参、返回结构、以及每条响应分支都写全。

全链路一览

步骤 方法 端点 作用
1 GET www.17dz.com/home/login.html 拿初始会话 cookie
2 GET www.17dz.com/ucapi/uic/protocol/user 按手机号预检
3 POST www.17dz.com/ucapi/auth/v1/oauth2.0/doLogin 真正登录,返回授权码 code
(3.5) — 弱密码账号在此被要求先过验证码(见第二篇)
4 GET 17dz.com/xqy-portal-web/login/v2/yqdz/callback 用 code 换最终会话 cookie

贯穿全平台的一个约定:返回体里 head.code == '00000000'(或 head.msg == '成功')才算成功,body 里才是数据。这个 00000000 后面每个采集接口都要判,先记住。

第一步:初始 cookie,两个自造 uuid

请求登录页之前,先自己造两个 cookie:

self.cookies = {
    'WDSESSIONID': str(uuid.uuid4()),
    'DEVICEID':    str(uuid.uuid4()),
}
resp1 = self.session.get(
    "https://www.17dz.com/home/login.html",
    headers=self._headers_page(referer="https://www.17dz.com/manage/index.html",
                               url="https://www.17dz.com/home/login.html"),
    cookies=self.cookies,
)
merged_cookies = {**self.cookies, **resp1.cookies.get_dict()}   # 合并服务端下发的

WDSESSIONID / DEVICEID 是前端 JS 用 uuid() 生成后写进 cookie 的,服务端只校验存在性与格式,不校验来历——自己 uuid.uuid4() 一个就能过。这是设备指纹的第一道、也是最脆的一道。_headers_page 带的那个 X-Irc-Data 指纹头,第三篇专门讲。

第二步:预检——一个「无用但必须」的请求

GET /ucapi/uic/protocol/user
    ?appId=19060100010000&scene=login&mobile=<手机号>&ucFrontUrlEnv=prod&_t=<毫秒时间戳>

它的返回内容用不上,但这个请求必须发生。前端在你输完手机号、还没点登录时就打了它(查协议/账号状态)。逆向时最常见的坑就是只挑「看起来有用」的接口复现,把这种铺垫请求省掉,于是请求序列和真实浏览器对不上,被风控识别。规则:完整复现浏览器实际发出的序列,而不是只挑关键接口。

appId=19060100010000 是这个前端应用的固定标识,全链路每个 ucapi 接口都带它;ucFrontUrlEnv=prod 指明走生产环境。

第三步:doLogin,密码 MD5,返回的是授权码

login_data = {
    'appId':       '19060100010000',
    'username':    _user_name,
    'password':    hashlib.md5(_password.encode()).hexdigest(),   # 前端就是 md5(明文)
    'autoLogin':   'true',
    'redirectUri': (
        'https://17dz.com/xqy-portal-web/login/v2/yqdz/callback'
        '?loginParams=%7B%22scope%22:%22top%22,%22line%22:%22hd%22,'
        '%22host%22:%22https://www.17dz.com%22,'
        '%22redirectUri%22:%22https://www.17dz.com/manage/index.html%22%7D'
        f'&_={ts}'
    ),
    'ucFrontUrlEnv': 'prod',
    '_t': ts,
}
resp = self.session.post(
    "https://www.17dz.com/ucapi/auth/v1/oauth2.0/doLogin",
    headers=self._headers_api(referer="https://www.17dz.com/home/login.html"),
    cookies=merged_cookies, data=login_data,   # 注意是 form 表单,不是 json
)

三个要点:

① 密码是 MD5,不是加密。 前端 md5(明文) 再上传。这不提供任何安全性——抓包拿到的 MD5 值可以直接重放当口令用,它唯一的作用是「明文不出现在网络里」。逆向照抄 hashlib.md5(pwd).hexdigest() 即可。

② redirectUri 里 URL 编码的 loginParams 解开是这样:

{"scope":"top","line":"hd","host":"https://www.17dz.com",
 "redirectUri":"https://www.17dz.com/manage/index.html"}

这个值要和第四步 callback 用的逐字一致,否则 code 换不出会话——它是 OAuth 里防止 code 被重定向到别处的绑定参数。

③ 返回不是会话,是授权码。 成功时 resp.json()['data']['code'] 才是要的东西。

响应分支必须全处理(success=false 时看 message 原文):

条件 message 原文 处理
账号密码错 手机号或者密码有误,请核对后重新输入 直接终止
弱密码 检测到您的密码较为常见或复杂度较低 触发验证码:先过验证码拿 id,带上它重登一轮
成功 — 取 data.code

第二条很关键:验证码不是每次都要,而是「弱密码账号」这个风险维度才触发。逆向时若发现验证码时有时无,先别怀疑抓错包,看是不是账号维度的风险补偿。这条分支的闭环是第二篇的主题。

第四步:授权码换最终会话

raw_code = resp.json()['data']['code']
resp_final = self.session.get(
    "https://17dz.com/xqy-portal-web/login/v2/yqdz/callback",
    params={
        'loginParams': '{"scope":"top","line":"hd","host":"https://www.17dz.com",'
                       '"redirectUri":"https://www.17dz.com/manage/index.html"}',
        '_': ts, 'code': raw_code,
    },
    allow_redirects=False,          # ← 关键
)
self.final_cookies  = {**merged_cookies, **resp_final.cookies.get_dict()}
self.companyId      = self.final_cookies.get('companyId')
self.journalIdList  = self.final_cookies.get('accountId', '')

两个最容易翻车的点:

① allow_redirects=False 不能省。 callback 返回 302,真正有价值的是这一跳 302 响应头里 Set-Cookie 种下的会话 cookie。若让 requests 自动跟随,跳到下个页面时这批 cookie 可能已被后续响应覆盖、你根本读不到。关掉自动跳转、手动读 resp_final.cookies,是这类 OAuth 回跳的通用正确姿势。

② 业务身份藏在 cookie,不在响应体。 换回来的会话 cookie 里有三样关键值,后面所有采集接口都要用:

  • companyId——当前公司/平台 id(代码里也当 platformId 用);
  • accountId——账套/记账人 id(代码里存成 journalIdList);
  • xsrf-token——CSRF 令牌(第四篇讲怎么回填)。

逆向登录态站点,别只盯响应 JSON,会话状态经常就摊在 cookie 里。 一个稳妥习惯:登录后把 final_cookies 全量打印,逐个对照浏览器请求,确认哪些字段被业务接口消费了。

小结

亿企赢登录链路的本质:自造设备 cookie 兜底 → 预检铺垫 → MD5 口令 → OAuth 授权码跨域换会话 → 弱密码才触发验证码。全程几乎没有真正的加密,逆向难点不在破解,而在三件事:完整复现请求序列、穷尽每条响应分支、知道会话身份最终落在 cookie。

下一篇:那个「弱密码才触发」的图形验证码,取图/校验被拆成两个接口、只靠一个可重试的 id 兜底,是怎么用 ddddocr 自动闭环的。

评论 0

还没有评论
← 全部文章

找站长

站长微信二维码 扫码加微信
售后群二维码 扫码进售后群
站长收款码 建站不易,感谢打赏
微信 18581469390
本站不支持自助充值;需要充值或开通额度,请直接联系站长。
加微信可以找我重置密码、充值、问工具怎么用