学工系统做得真丑,所以我对学校从登录到晚打卡的api调用流程,进行了抓包逆向的研究分析。
我们学校使用了SSO方案进行统一登录认证,也就是在访问学校各个系统之前,需要在一个 统一身份认证平台(即融合门户) 进行统一登录。只要一次登录就能访问各个系统(比如教务系统、学工系统)。要晚打卡,则需要先保证用户在CAS系统已登录的情况下,才能在学工系统登录并打卡。
本文还未编辑完全
因为我的账号开始总是不需要短信验证了,所以就不知道(丢了抓包日志)二重验证相关的API的响应完整该长啥样了。以后有机会短信验证了再补上。
开始分析
由于学工系统的定位功能似乎调用了微信sdk,所以电脑浏览器是定位不了的,而且不成功定位就不能打卡,也就不能抓包打卡api,也就意味着我必须用实体手机在微信内置浏览器里进行打卡。我试过这样一种方法:
- 电脑开启Charles抓包工具
- 手机安装movecerts这个Magisk模块安装Charles的SSL证书
- 手机LSPosed开启TrustMeAlready和JustTrustMe模块强制令软件信任Charles的证书
坑爹的是,手机其他任何程序都能抓包,只有微信内置浏览器不能抓包。后来发现可能是因为微信内置浏览器使用了SSL Pinning机制,Charles的自签名SSL证书无法绕过这个机制。
因此,我使用了DevTools的inspector工具对微信内置浏览器进行远程调试,在电脑Chrome的DevTools通过网络面板查看api调用情况。
首先,手机微信访问http://debugxweb.qq.com/?inspector=true激活微信的网页调试,然后用usb连接手机,手机打开usb调试,就可以在电脑上面使用chrome访问chrome://inspect/#devices进行远程调试了。

(图为晚打卡API请求体)
然而,在inspector下,某些请求只显示预配标头(后来发现,与真实请求头的区别只是少了Cookie字段):

按网上说的禁用缓存等方法依旧没啥用。幸运的是,打卡的关键api不会这样。所以,对于登录流程的逆向,还是得用Chrome的DevTools才行。
另外,我也依旧用了Charles进行抓包辅助,还通过Claude Code接入了chrome-devtools-mcp帮忙分析。
登录和打卡的api并不复杂,所以具体过程就不细说了。经过抓包逆向分析,API的流程大概如下。
API总体流程概述
登录融合门户,大体流程是:先调用/cas/mfa/detect检查是否需要双因素验证(一般是手机验证码) -> 调用/cas/mfa/initByType/securephone获取gid -> 调用/attest/api/guard/securephone/send发送短信验证码(传入gid) -> 调用/attest/api/guard/securephone/valid验证短信验证码(传入gid) -> 调用/cas/login登录
登录学工系统,大体流程是:访问学工系统 -> 302至CAS(附加查询参数service,值为来源url) -> CAS系统获取Cookie的TOC字段,判断是否登录 -> 再次302到学工系统(目标url追加ticket这个查询参数)-> 学工系统写入JSESSIONID这个cookie字段 -> 成功登录学工系统
进行晚打卡,则是直接POST https://xgyd.mku.edu.cn/acmc-weichat/wxapp/swkjjksb/mrdk_save.do即可。
具体原理
登录 - cas.mku.edu.cn
登录的具体逻辑,直接位于登录页面body里的script标签中。

POST /cas/mfa/detect
Content-Type: application/x-www-form-urlencoded
请求体:
1 | username=用户名&password=RSA加密后的密码 |
这里的密码,是通过/cas/jwt/publicKey提供的公钥使用PKCS1_v1_5算法加密的,并使用base64编码,之后在前面加上__RSA__前缀。
响应:
1 | {"code":0,"data":{"mfaTypeSecurePhone":true,"mfaTypeQrCode":false,"need":false,"mfaTypeAppPush":false,"mfaTypeFaceVerify":false,"mfaEnabled":true,"state":"TtMomI","mfaTypeSecureEmail":true}} |
如果”need”:true,则需要手机短信验证码验证。如果”need”:false,就可以直接登录,跳过以下的短信验证部分。
GET /cas/mfa/initByType/securephone
查询参数:
state: 在前面/cas/mfa/detect获取到的data.state
响应:
1 | (TODO) |
如果不需要短信验证,响应:
1 | {"code":-1,"error":{"message":"fail_0"}} |
POST /attest/api/guard/securephone/send
Content-Type: application/json
请求体:
1 | { |
响应:
1 | (TODO) |
POST /attest/api/guard/securephone/valid
请求体:
1 | { |
响应:
1 | (TODO) |
POST /cas/login
进行正式的登录
Content-Type: application/x-www-form-urlencoded
请求体(虽然是表单格式,但是为了看着清晰,这里改用字典表示):
1 | { |
execution的值在/cas/login登录页面(也就是说得先获取一下网页源码)的name为execution的表单字段里:
1 | <input type="hidden" name="execution" value="c5087400-a375-41ca-9118-100d9d1d284c_ZXlKaGJHY2lPaUpJVXpVeE1pSjkueXpaeG55NWh。。。此处省略一大坨。。。"> |
响应为 200 就是登录成功,返回登陆成功的html。
GET /cas/login
参数:
service:目标服务的url,比如说要访问的学工系统的URL
响应:302到service指定url上,附加ticket查询参数,例如?ticket=ST-237915-rHwNNRG72-W6VlZknzOE1gjtnO5qU0Du
实际上,直接访问学工系统就会302到当前所述的API上,然后带着ticket302到service指定url,最后写入cookie(JSESSIONID),并不带ticket原地重新302

打卡 - xgyd.mku.edu.cn
POST /acmc-weichat/wxapp/swkjjksb/mrdk_save.do
Content-Type: application/x-www-form-urlencoded
请求体(虽然是表单格式,但是为了看着清晰,这里改用字典表示):
1 | { |
xsid得先获取/acmc-weichat/wxapp/swkjjksb/mrdk_edit打卡页面的网页源码,在name为xsid的表单字段里:
1 | <input type="hidden" id="xsid" value="F8C8743F99********28D9831DCD81FF" autocomplete="off"> |
响应:
{"ret":"ok"}:打卡成功
{"ret":"more"}:重复打卡
打卡系统后端存在的问题
逆向后发现了这些毛病,特别是第一点,也是奇异搞笑。
- 后端没有对打卡时间做合法性验证,只有前端做了。导致可以做到一整天24h随时打卡,甚至还能给明天、后天、以后打卡
- 历史遗留疫情时期的体温相关字段,没啥意义
- 请求字段全是拼音缩写,毫不直观,全靠猜