对学校新教务系统的浅显分析
前几天学校换了新的教务系统,虽然仍然是正方软件,但好歹界面比之前的好看多了,根据直觉推断应该是Bootstrap的UI库。而且这次也没有ASP加ViewState那种逻辑了,分析起来容易了一些。
唯一蛋疼的就是,界面的所有元素id,以及接口所有的字段名称全都是用的字母拼音,也不知道哪个人才搞出来的。。
放一张图自己体会:
(鬼知道知道我看到这一堆字段名时有多绝望)
登录逻辑
登录逻辑相比较之前可谓是复杂了不少,竟然还用到了RSA加密。说实话加密算法这块一直都是我的知识盲区,这次研究过一番后又get到了不少知识点。
登录界面没有验证码,但是当密码输错一定次数之后(三次?)就会出现验证码,不过通过清除cookie就可以绕过,所以应该还是等于没有验证码嘛。
CSRF Token的获取
大概查了一下,CSRF(Cross-site request forgery)是一种跨站攻击手段,这里的csrf_token就是为了预防跨站请求伪造攻击而设计的。初次请求登录页面时,这个token作为隐藏值写在了html表单中。登录时随xhr请求一同发送给服务器。
密码的RSA加密
根据/jwglxt/js/globalweb/login/login.js
这个脚本的逻辑来看,在进入登录页面时同时从服务器获取了RSA加密的公钥,具体路径是/xtgl/login_getPublicKey.html
,公钥有两个值:modulus和exponent,根据多次尝试的结果来看,exponent的值始终都是base64的AQAB
,也就是10进制的65537。而modulus的值就比较长了。
然后用modulus和exponent生成一个RSA公钥,使用改公钥来加密密码并转成base64。然后点击登录按钮时,学号,密码,以及csrf_token会发送给服务器器。不过同时还有一些其他的参数,比如language参数,不过经过实际测试,只有这三个值是必需的,其他值不带也可以成功登录。
登录请求
上面的三个参数是以WebForm的格式发送到服务器的,具体字段名如下:
- yhm: 学号
- mm: 加密过后的密码
- csrf_token
在页面上请求中,mm字段出现了两次,大概是因为页面上还有一个隐藏的mm输入框并与密码框的内容相同。根据login.js的注释来判断,是为了防止自动填充(不过我用的Chrome依然可以自动填充),而且经过实际测试,mm字段在请求只用出现一次就可以成功登录。
如果登录信息正确的话,服务器会返回302并在请求头中携带一个新的JSESSIONID,用来替换原先第一次加载登录页面时所产生的JSESSIONID。(从JSESSIONID这个名字来判断的话大致可以可以看出服务端是Java)。
选课逻辑
登录之后第一个关注的事当然就是选课了。经过研究之后发现选课的逻辑的逻辑也比较复杂,实际上请求中有一堆参数我都不知道是啥意思,参考了几次发现都没变化,后来写代码时索性就写死了,有空再去研究是啥意思吧。
后来,在我实际上都选完课之后,我在GitHub找到了不少关于选课的脚本,虽然不能直接用,但很有参考价值。这里附上我参考最多的一个:https://github.com/EddieIvan01/lessons-robber
不过选课那几天实在是太忙,没空研究这个东西,最终选课的时候进不去真是。。。
咳咳,下面开始说一下选课的大致逻辑吧。有些地方的具体含义我用不是很清楚,暂时能用就行。
选课页面虽然列出了所有课程的大致的信息,但是因为数据比较多,而且还有分页请求,要获取到目标的课程就要请求多次然后遍历,弄起来可能会很麻烦。于是这里就按照脚本的思路,通过搜索接口返回的结果来直接获取目标课程信息。
搜索目标课程
使用搜索接口之前,我们要知道下面的几个值:
- bh_id:含义未知,但根据值来看跟专业和班级有关。比如18届,专业代号1951,3班,值就是18195103。可以从学号里截取。
- jg_id:含义位置,但推测也是跟个人有关的定值,请自行确定。
- njdm_id:年级代码,比如18届就是2018。
- xh_id:你的学号。
- xkxnm:可能是当前学期的年份,比如2018
- xkxqm:含义未知,但根据参考脚本来看,如果当前月份介于5到8之间这个值就是3,否则为12。暂未验证。
- zyh_id:专业代号,比如1951
- filter_list[0]:你要搜索的关键字
下面就可以对搜索接口发起请求了,请求的路径为/jwglxt/xsxk/zzxkyzb_cxZzxkYzbPartDisplay.html?gnmkdm=N253512&su=学号
,下面给出完整的参数列表供参考:
1 | 'bh_id': 'xxxxx, |
其他的未说明的值暂时当定值处理。
返回结果:
搜索接口返回的是json数据,其中有一个tmpList的数组就是搜索结果。下面列出等会选课将会用到的值:
- kch:课程号,大概长这样:
201021211A
- jxb:含义未知,应该是与课程有关的值,大概是一个16位的大写16进制值。
- kcmc:课程名称。
- xf:学分。
获取课程信息
虽然上面已经有了搜索结果,但还是不能直接获取这门课程的所有数据。经过调试发现,要想选课,还要获取通过另一个接口获取该课程的do_jxb_id
(含义仍然未知)。然而要使用该接口,又得先获取一个叫xkkz_id
的东西(无限套娃)。而这个值跟选课的种类有关,在我测试时,选课界面一共有三种课程类型:公共基础课、大学体育、通识选修课。每一种课程都对应了一个xkkz_id。这个值的具体位置也有两种:
在选课类型的菜单栏的onClick事件中的回调函数的参数里面。比如:
onclick="queryCourse(this,'10','14564675131412412421','2020','2021')"
注意其中的第三个参数
14564675131412412421
(我随便打的,实际应该是一个32位的十六进制值),就是我们要的xkkz_id。另外,经过推测,第二个参数应该是返回结果的最大数量。
在搜索框(没太注意,大概是)附近的隐藏表单中。比如:
<input type="hidden" name="firstXkkzId" id="firstXkkzId" value="14564675131412412421"></input>
其中的value属性值就是xkkz_id。
如果两个地方都存在这个xkkz,以第一个地方的值优先。(这个是关键)
下面会给出使用这个接口需要知道的其他值(上面已经写过的未列出):
- kch_id:课程号。
- xkkz_id:刚刚从选课界面获取到的xkkz_id。
这个课程信息的请求路径:/jwglxt/xsxk/zzxkyzbjk_cxJxbWithKchZzxkYzb.html?gnmkdm=N253512&su=学号
完整的请求参数列表参考:(与上面的那个是不同的)
1 | 'bh_id': 'xxxxxxxx', |
这个接口返回的结果直接就是json数组(这个地方有点坑),拿到结果之后我们仅仅需要其中的do_jxb_id
即可。
选课
写了半天终于写到正头戏了。
先给出需要的可变参数:
jxb_ids:刚才拿到的
do_jxb_id
,只记得非常长,有多少位没注意,可能64位或者以上,也是16进制值。kch_id:课程号。
kcmc:课程的完整名称,大概张这样:
(201021211A)食品安全概论 - 2.0 学分
需要注意的是这个值里面有上面的kch、kcmc、xf三个值。而且并没有测试是否有严格判断(感觉应该没有吧
选课接口的请求路径:/jwglxt/xsxk/zzxkyzb_xkBcZyZzxkYzb.html?gnmkdm=N253512&su=学号
下面仍然是完整的参数列表:
1 | 'cxbj': '0', |
返回的结果很简单,只有flag
和msg
两个值。
如果选课成功,则flag的值为1,无msg值。如果选课失败,flag为0,msg是错误信息。
结语
由于这一次分析加上写博客用了将近两三天,导致最终开始写这篇文章时,学校的教务系统已经关闭选课了,所以一部分接口无法再次测试。
而且由于时间与水平有限,部分的参数值的定义或其他方面可能会有疏漏,如果可以的话欢迎指正。
另外,选课脚本暂不开源(写的什么破玩意也好意思开源),我的脚本目前只是勉强算是实现了选课功能,部分值都是按定值处理的。而且有将近一半的部分(登录部分)使用了别人的代码,直接说是我写的也不太好。
总之,等我后续优化吧,如果优化好了会放出来的。(咕咕咕)
附一张脚本测试运行时的图: