# 前言

本文包含个人主观情绪(被气坏了),非纯教程文章

之前做课表 APP 做上瘾了,但因为教务系统的功能主要还是看课表,抢课教评什么的一学期就那一两次,真正日常常用的功能还是在学校委托第三方(或者是自己开发的?)做的 APP 上。

当然,APP 还是继承国内 APP 的优良传统之典中典 Web APP,所以自己实现难度不大。

其实最主要还是 APP 太 shit 了,有时候启动卡半天不知道在干嘛,还有很多无用的信息(对于我自己来说),因此打算一不做二不休,自己做个第三方的

这次先打个头阵,对 APP 进行抓包分析。

# 初步分析

APP 采用统一 CAS 登录,和教务系统账号独立。

APP 内部所有页面无需重复登录,并且具有登录状态保持能力,因此一定使用了 cookie 或者其他技术保持登录状态,所有功能共享登录状态,而不是依靠浏览器 session

APP 内功能模块动态更新,考虑到是 Web APP,应该是通过更新 Web 页面或者相关 list 信息达到动态新增模块功能

# 抓包分析

# 登录模块

为保(liu)护(dian)隐(lian)私(mian)这里就不透露域名了,仅公布路径

md 一抓就绷不住了。。。

首先使用 POST 请求 /token/mfa/detect

header 为普通内容,不带任何具有 session 功能的内容

然后 body 为空

返回值如下

1
2
3
4
5
6
7
8
9
10
11
12
{
"code": 0,
"data": {
"mfaTypeSecurePhone": true,
"mfaTypeFedAuth": false,
"need": false,
"mfaTypeFaceVerify": false,
"mfaEnabled": false,
"state": "一串字符,没啥用",
"mfaTypeSecureEmail": true
}
}

通过路径和返回值可以得出此次请求是获取 mfa 信息的,以便进行 mfa 验证,但是我们学校 app 貌似没 mfa 这个功能,所以这个检查可有可无。

但目前有个巨大的问题,POST 请求 body 为空,那服务器怎么知道用户信息呢?

没错,md 用户名和密码在请求路径中。。。。

完整请求路径为: /token/mfa/detect?username=学号&deviceId=设备ID(暂时未知如何生成)&password=明文密码

看到这我都气笑了,还 POST 什么啊,干脆直接 GET 得了。。。还明文传输密码,我们教务系统虽然用的是 HTTP,但人家至少密码还是通过 自研 算法加密了(不知道是不是单向的,真不如哈希加盐)才传输的,HTTPS 都表示自己无能为力。

言归正传,下面是真正登录请求

使用 POST 请求 /token/password/passwordLogin

请求 header 还是正常普通的 header,密码仍然是内嵌路径(已经懒得喷了),请求参数表如下:

Name Value
username 学号
password 明文密码
appId 固定值,为 APP 包名
geo 空,不知道是干嘛的
deviceId 设备 ID,和前面一样,不知道是怎么生成的
osType 系统类型,这里是 Android
clientId 客户端 ID,不知道是怎么生成的
mfaState

返回值如下:

1
2
3
4
5
6
7
8
9
10
{
"code": 0,
"data": {
"userNonActivated": false,
"passwordStatus": 0,
"idToken": "JWT",
"userNonCompleted": true,
"refreshToken": "JWT"
}
}

就很好奇居然懂得用 JWT 了,为什么还会犯用 POST 请求路径传输明文密码这种低级错误,就很难蚌。

通过解析 JWT,可以得到下面信息:

JWT Header:

Name Value
alg (Algorithm)RS512 (RSASSA-PKCS1-v1_5 using SHA-512)

JWT Payload:

Name Value
ATTR_userNo 学号
sub (Subject) 学号
iss (Issuer) cas 系统 Host
deviceId 设备 ID
ATTR_identityTypeId 用不上
ATTR_accountId 用户 ID
ATTR_userId 用户 ID
ATTR_identityTypeCode S02
ATTR_identityTypeName 学生
ATTR_organizationName 班级
ATTR_userName 姓名
exp (Expiration Time) 过期时间,有效期一个月
ATTR_organizationId 用不上
iat (Issued At) 令牌创建时间
jti (JWT ID) Id-Token-xxxxxxxx
req 包名
ATTR_organizationCode 用不上

成功拿到 JWT 就简单很多了

后面还有个绑定 clientid 以及向 /openplartform/auth/api/token 的请求,不知道干啥用的,后面实测不请求也貌似没什么影响

openplartform 请求返回的 JWT 后面也没用上

# 拉取功能模块

在一堆没用的请求中终于找到了 server list 请求

/portal-api/v1/service/list 发送 POST 请求

header 中携带 JWT,具体如下:

1
2
3
4
5
6
X-Device-Info: 我手机的型号
Accept: application/json
X-Device-Infos: 简单的系统信息
X-Id-Token: JWT内容
X-Terminal-Info: app

主要还是携带 JWT,剩下那几个 X-xxx 的内容都没必要,当然如果是打算伪装的话还是需要一起携带

然后 body 是空。。。

不是,真就不知道 GET 和 POST 怎么用是吧???

建议重新学习前端

返回的 json 包含一个 services 数组,里面就是整个 APP 所有的功能模块

数组内一个对象包含的对象一堆,但有用的就几个:

Name Value
serviceName 功能名称
servicePicUrl 图标
serviceUrl 功能页面地址

简单试了一下,请求 header 中携带 X-Id-Token: JWT 即可正常访问页面

# 后记

APP 具体开发要等到寒假,最近忙着应付考试完全没有时间(今天才考完汇编,泪目了)