亿企赢逆向手记(一):登录链路,从 OAuth 回调到最终会话 Cookie
五请求跨两域的登录链路:设备 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