好久没更新博客了,主要是学习太忙了嘛~

前几天学校换了新的教务系统,虽然仍然是正方软件,但好歹界面比之前的好看多了,根据直觉推断应该是Bootstrap的UI库。而且这次也没有ASP加ViewState那种逻辑了,分析起来容易了一些。

唯一蛋疼的就是,界面的所有元素id,以及接口所有的字段名称全都是用的字母拼音,也不知道哪个人才搞出来的。。

放一张图自己体会:

1624876290824.png

(鬼知道知道我看到这一堆字段名时有多绝望)

登录逻辑

登录逻辑相比较之前可谓是复杂了不少,竟然还用到了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的格式发送到服务器的,具体字段名如下:

  1. yhm: 学号
  2. mm: 加密过后的密码
  3. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
'bh_id': 'xxxxx,
'bklx_id': '0',
'ccdm': '3',
'filter_list[0]': 搜索关键字,
'jg_id': '0102',
'jspage': '10',
'kkbk': '0',
'kkbkdj': '0',
'kklxdm': '10',
'kspage': '1',
'njdm_id': 'xxxx',
'rwlx': '2',
'sfkcfx': '0',
'sfkgbcx': '0',
'sfkknj': '0',
'sfkkzy': '0',
'sfkxq': '0',
'sfrxtgkcxd': '1',
'sfznkx': '0',
'tykczgxdcs': '10',
'xbm': '1',
'xh_id': 学号,
'xkly': '0',
'xkxnm': 'xxxx',
'xkxqm': '3',
'xqh_id': '1',
'xsbj': '4294967296',
'xslbdm': '421',
'zdkxms': '0',
'zyfx_id': 'wfx',
'zyh_id': '专业代号'

其他的未说明的值暂时当定值处理。

返回结果:

搜索接口返回的是json数据,其中有一个tmpList的数组就是搜索结果。下面列出等会选课将会用到的值:

  • kch:课程号,大概长这样:201021211A
  • jxb:含义未知,应该是与课程有关的值,大概是一个16位的大写16进制值。
  • kcmc:课程名称。
  • xf:学分。

获取课程信息

虽然上面已经有了搜索结果,但还是不能直接获取这门课程的所有数据。经过调试发现,要想选课,还要获取通过另一个接口获取该课程的do_jxb_id(含义仍然未知)。然而要使用该接口,又得先获取一个叫xkkz_id的东西(无限套娃)。而这个值跟选课的种类有关,在我测试时,选课界面一共有三种课程类型:公共基础课、大学体育、通识选修课。每一种课程都对应了一个xkkz_id。这个值的具体位置也有两种:

  1. 在选课类型的菜单栏的onClick事件中的回调函数的参数里面。比如:

    onclick="queryCourse(this,'10','14564675131412412421','2020','2021')"

    注意其中的第三个参数14564675131412412421(我随便打的,实际应该是一个32位的十六进制值),就是我们要的xkkz_id。

    另外,经过推测,第二个参数应该是返回结果的最大数量。

  2. 在搜索框(没太注意,大概是)附近的隐藏表单中。比如:

    <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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
'bh_id': 'xxxxxxxx',
'bklx_id': '0',
'ccdm': '3',
'filter_list[0]': 搜索关键字,
'jg_id': '0102',
'kkbk': '0',
'kkbkdj': '0',
'kklxdm': '10',
'njdm_id': 'xxxx',
'rwlx': '2',
'sfkcfx': '0',
'sfkknj': '0',
'sfkkzy': '0',
'sfkxq': '0',
'sfkkjyxdxnxq': '0',
'sfznkx': '0',
'xbm': '1',
'xkly': '0',
'xkxnm': 'xxxx',
'xkxqm': '3',
'xqh_id': '1',
'xsbj': '4294967296',
'xslbdm': '421',
'zdkxms': '0',
'zyfx_id': 'wfx',
'zyh_id': 'xxxx',
'rlkz': '0',
'kch_id': kch,
'xkkz_id': xkkz_id,
'cxbj': '0',
'fxbj': '0',
'kzybkxy': '0'

这个接口返回的结果直接就是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
'cxbj': '0',
'jxb_ids': do_jxb_id,
'kch_id': kch,
'kcmc': 课程完整名称,
'kklxdm': '10',
'njdm_id': '20xx',
'qz': '0',
'rlkz': '0',
'rlzlkz': '1',
'rwlx': '2',
'sxbj': '1',
'xkkz_id': xkkz,
'xklc': '2',
'xkxnm': '2021',
'xkxqm': '3',
'xxkbj': '0',
'zyh_id': 专业代号

返回的结果很简单,只有flagmsg两个值。

如果选课成功,则flag的值为1,无msg值。如果选课失败,flag为0,msg是错误信息。

结语

由于这一次分析加上写博客用了将近两三天,导致最终开始写这篇文章时,学校的教务系统已经关闭选课了,所以一部分接口无法再次测试。

而且由于时间与水平有限,部分的参数值的定义或其他方面可能会有疏漏,如果可以的话欢迎指正。

另外,选课脚本暂不开源(写的什么破玩意也好意思开源),我的脚本目前只是勉强算是实现了选课功能,部分值都是按定值处理的。而且有将近一半的部分(登录部分)使用了别人的代码,直接说是我写的也不太好。

总之,等我后续优化吧,如果优化好了会放出来的。(咕咕咕)

附一张脚本测试运行时的图:

1624875854504.png