因为最近在用教研室的电脑挂 PT 站,然而学校拨号上网居然不给分配 IPV6 地址,只好连无线 AP 了。但是校园网总是没事就掉一下,有点恶心。

早些日子写了一个登录校园网的 Shell 脚本,本来是用在 OpenWrt 路由上的,这回把这脚本拿到了 win 上用。

校园网登录脚本

之前没事干看了很久校园网的登录界面代码,发现校园网的认证逻辑还是挺奇怪的。

校园网认证登录

  1. Post 到服务器,获取 Challenge 码
  2. 对 Challenge 码和 password 进行处理,得到一个 md5 加密后的密钥
  3. 将密钥和用户名、Challenge 码一起 Post 发给服务器,登录成功

获取 Challenge 码

照着浏览器抓取的请求一顿抄:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RESULT=$(curl 'http://m.njust.edu.cn/portal_io/getchallenge' \
  -X 'POST' \
  -H 'Accept: application/json, text/javascript, */*; q=0.01' \
  -H 'Accept-Language: zh-CN,zh-HK;q=0.9,zh;q=0.8,en;q=0.7' \
  -H 'Connection: keep-alive' \
  -H 'Content-Length: 0' \
  -H 'Cookie: zg_did=%7B%22did%22%3A%20%221830dcb70ae82e-07e49a52b9eb58-1b525635-384000-1830dcb70afe00%22%7D; zg_=%7B%22sid%22%3A%201662383780017%2C%22updated%22%3A%201662383858267%2C%22info%22%3A%201662383780017%2C%22superProperty%22%3A%20%22%7B%7D%22%2C%22platform%22%3A%20%22%7B%7D%22%2C%22utm%22%3A%20%22%7B%7D%22%2C%22referrerDomain%22%3A%20%22ehall.njust.edu.cn%22%2C%22cuid%22%3A%20%22122106010798%22%7D' \
  -H 'Origin: http://m.njust.edu.cn' \
  -H 'Referer: http://m.njust.edu.cn/portal/index.html?redirect=aHR0cDovL3d3dy50YW9iYW8uY29tLw==' \
  -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36' \
  -H 'X-Requested-With: XMLHttpRequest' )

echo ${RESULT} | jq '.challenge'
CHALLENGE=$(echo ${RESULT} | jq -r '.challenge')
echo ${CHALLENGE}

把 json 结果用 jq 抓一下,然后就会拿到 Challenge 码。

md5 加密 Challenge 和 password

这里校园网的前端写得特别清楚,但最大的问题是他们用 JavaScript 写的,但我可不会 JS,于是只好用 python 重写。其实大部分内容都是 chatgpt 帮我写的,感谢科技的力量。

但这里遇到一个坑,JS 代码处理数字用的都是 32 位,一开始没注意,加密出来的 md5 一直跟浏览器抓到的不一样。。。

整个过程大部分是一个重写的 md5 加密。最开始生成一个 0 到 255 的随机数,后面接上用户密码,然后对 Challenge 码每两位转换成一个 16 进制数,再跟在后面。最后将这一串数进行 md5 加密,前面补上随机数的值,得到加密后的密钥。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
import sys
import random
from ctypes import c_int32


def i32(n):
# 如果n是负数就转换为32位对应的数
if n < 0:
n = (1 << 32) + n
return c_int32(n).value


def createChapPassword(challenge):
id1 = random.randint(0, 255)
password = 'yourpasswd'
str1 = ''

str1 += chr(id1)
str1 += password

for i in range(0, len(challenge), 2):
hex_value = challenge[i:i+2]
dec_value = int(hex_value, 16)
str1 += chr(dec_value)

hash_value = md5(str1)

chappassword = ('0' if id1 < 16 else '') + hex(id1)[2:] + hash_value
return chappassword

def rotateLeft(lValue, iShiftBits):
return i32((lValue << iShiftBits)) | (lValue >> (32 - iShiftBits))

def addUnsigned(lX, lY):
lX4, lY4, lX8, lY8, lResult = 0, 0, 0, 0, 0
lX8 = (lX & 0x80000000)
lY8 = (lY & 0x80000000)
lX4 = (lX & 0x40000000)
lY4 = (lY & 0x40000000)
lResult = (lX & 0x3FFFFFFF) + (lY & 0x3FFFFFFF)
if (lX4 & lY4):
return (lResult ^ 0x80000000 ^ lX8 ^ lY8)
elif (lX4 | lY4):
if (lResult & 0x40000000):
return (lResult ^ 0xC0000000 ^ lX8 ^ lY8)
else:
return (lResult ^ 0x40000000 ^ lX8 ^ lY8)
else:
return (lResult ^ lX8 ^ lY8)

def F(x, y, z):
return (x & y) | ((~x) & z)

def G(x, y, z):
return (x & z) | (y & (~z))

def H(x, y, z):
return (x ^ y ^ z)

def I(x, y, z):
return (y ^ (x | (~z)))

def FF(a, b, c, d, x, s, ac):
a = addUnsigned(a, addUnsigned(addUnsigned(F(b, c, d), x), ac))
return addUnsigned(rotateLeft(a, s), b)

def GG(a, b, c, d, x, s, ac):
a = addUnsigned(a, addUnsigned(addUnsigned(G(b, c, d), x), ac))
return addUnsigned(rotateLeft(a, s), b)

def HH(a, b, c, d, x, s, ac):
a = addUnsigned(a, addUnsigned(addUnsigned(H(b, c, d), x), ac))
return addUnsigned(rotateLeft(a, s), b)

def II(a, b, c, d, x, s, ac):
a = addUnsigned(a, addUnsigned(addUnsigned(I(b, c, d), x), ac))
return addUnsigned(rotateLeft(a, s), b)

def convertToWordArray(string):
lWordCount = None
lMessageLength = len(string)
lNumberOfWordsTempOne = lMessageLength + 8
lNumberOfWordsTempTwo = int((lNumberOfWordsTempOne - (lNumberOfWordsTempOne % 64)) / 64)
lNumberOfWords = int((lNumberOfWordsTempTwo + 1) * 16)
lWordArray = [0] * (lNumberOfWords - 1)
lBytePosition = 0
lByteCount = 0
while lByteCount < lMessageLength:
lWordCount = int((lByteCount - (lByteCount % 4)) / 4)
lBytePosition = i32((lByteCount % 4) * 8)
lWordArray[lWordCount] = (
lWordArray[lWordCount] |
i32((ord(str(string[lByteCount])) << lBytePosition))
)
lByteCount += 1
lWordCount = int((lByteCount - (lByteCount % 4)) / 4)
lBytePosition = i32((lByteCount % 4) * 8)
lWordArray[lWordCount] = (
lWordArray[lWordCount] |
i32((0x80 << lBytePosition))
)
lWordArray[lNumberOfWords - 2] = i32(lMessageLength << 3)
lWordArray.append(0)
#lWordArray[lNumberOfWords - 1] = lMessageLength >> 29
return lWordArray

def wordToHex(lValue):
WordToHexValue = ''
WordToHexValueTemp = ''
for lCount in range(4):
lByte = (lValue >> (lCount * 8)) & 255
WordToHexValueTemp = '0' + "{:x}".format(lByte)
WordToHexValue = WordToHexValue + WordToHexValueTemp[-2:]
return WordToHexValue

def uTF8Encode(string):
string = string.replace('\x0d\x0a', '\x0a')
output = ''
for n in range(len(string)):
c = ord(string[n])
if c < 128:
output += chr(c)
elif c > 127 and c < 2048:
output += chr((c >> 6) | 192)
output += chr((c & 63) | 128)
else:
output += chr((c >> 12) | 224)
output += chr(((c >> 6) & 63) | 128)
output += chr((c & 63) | 128)
return output

def md5(string):
x = []
S11 = 7
S12 = 12
S13 = 17
S14 = 22
S21 = 5
S22 = 9
S23 = 14
S24 = 20
S31 = 4
S32 = 11
S33 = 16
S34 = 23
S41 = 6
S42 = 10
S43 = 15
S44 = 21
x = convertToWordArray(string)
a = 0x67452301
b = 0xEFCDAB89
c = 0x98BADCFE
d = 0x10325476
for k in range(0, len(x), 16):
AA, BB, CC, DD = a, b, c, d
a = FF(a, b, c, d, x[k+0], S11, 0xD76AA478)
d = FF(d, a, b, c, x[k+1], S12, 0xE8C7B756)
c = FF(c, d, a, b, x[k+2], S13, 0x242070DB)
b = FF(b, c, d, a, x[k+3], S14, 0xC1BDCEEE)
a = FF(a, b, c, d, x[k+4], S11, 0xF57C0FAF)
d = FF(d, a, b, c, x[k+5], S12, 0x4787C62A)
c = FF(c, d, a, b, x[k+6], S13, 0xA8304613)
b = FF(b, c, d, a, x[k+7], S14, 0xFD469501)
a = FF(a, b, c, d, x[k+8], S11, 0x698098D8)
d = FF(d, a, b, c, x[k+9], S12, 0x8B44F7AF)
c = FF(c, d, a, b, x[k+10], S13, 0xFFFF5BB1)
b = FF(b, c, d, a, x[k+11], S14, 0x895CD7BE)
a = FF(a, b, c, d, x[k+12], S11, 0x6B901122)
d = FF(d, a, b, c, x[k+13], S12, 0xFD987193)
c = FF(c, d, a, b, x[k+14], S13, 0xA679438E)
b = FF(b, c, d, a, x[k+15], S14, 0x49B40821)
a = GG(a, b, c, d, x[k+1], S21, 0xF61E2562)
d = GG(d, a, b, c, x[k+6], S22, 0xC040B340)
c = GG(c, d, a, b, x[k+11], S23, 0x265E5A51)
b = GG(b, c, d, a, x[k+0], S24, 0xE9B6C7AA)
a = GG(a, b, c, d, x[k+5], S21, 0xD62F105D)
d = GG(d, a, b, c, x[k+10], S22, 0x2441453)
c = GG(c, d, a, b, x[k+15], S23, 0xD8A1E681)
b = GG(b, c, d, a, x[k+4], S24, 0xE7D3FBC8)
a = GG(a, b, c, d, x[k+9], S21, 0x21E1CDE6)
d = GG(d, a, b, c, x[k+14], S22, 0xC33707D6)
c = GG(c, d, a, b, x[k+3], S23, 0xF4D50D87)
b = GG(b, c, d, a, x[k+8], S24, 0x455A14ED)
a = GG(a, b, c, d, x[k+13], S21, 0xA9E3E905)
d = GG(d, a, b, c, x[k+2], S22, 0xFCEFA3F8)
c = GG(c, d, a, b, x[k+7], S23, 0x676F02D9)
b = GG(b, c, d, a, x[k+12], S24, 0x8D2A4C8A)
a = HH(a, b, c, d, x[k+5], S31, 0xFFFA3942)
d = HH(d, a, b, c, x[k+8], S32, 0x8771F681)
c = HH(c, d, a, b, x[k+11], S33, 0x6D9D6122)
b = HH(b, c, d, a, x[k+14], S34, 0xFDE5380C)
a = HH(a, b, c, d, x[k+1], S31, 0xA4BEEA44)
d = HH(d, a, b, c, x[k+4], S32, 0x4BDECFA9)
c = HH(c, d, a, b, x[k+7], S33, 0xF6BB4B60)
b = HH(b, c, d, a, x[k+10], S34, 0xBEBFBC70)
a = HH(a, b, c, d, x[k+13], S31, 0x289B7EC6)
d = HH(d, a, b, c, x[k+0], S32, 0xEAA127FA)
c = HH(c, d, a, b, x[k+3], S33, 0xD4EF3085)
b = HH(b, c, d, a, x[k+6], S34, 0x4881D05)
a = HH(a, b, c, d, x[k+9], S31, 0xD9D4D039)
d = HH(d, a, b, c, x[k+12], S32, 0xE6DB99E5)
c = HH(c, d, a, b, x[k+15], S33, 0x1FA27CF8)
b = HH(b, c, d, a, x[k+2], S34, 0xC4AC5665)
a = II(a, b, c, d, x[k+0], S41, 0xF4292244)
d = II(d, a, b, c, x[k+7], S42, 0x432AFF97)
c = II(c, d, a, b, x[k+14], S43, 0xAB9423A7)
b = II(b, c, d, a, x[k+5], S44, 0xFC93A039)
a = II(a, b, c, d, x[k+12], S41, 0x655B59C3)
d = II(d, a, b, c, x[k+3], S42, 0x8F0CCC92)
c = II(c, d, a, b, x[k+10], S43, 0xFFEFF47D)
b = II(b, c, d, a, x[k+1], S44, 0x85845DD1)
a = II(a, b, c, d, x[k+8], S41, 0x6FA87E4F)
d = II(d, a, b, c, x[k+15], S42, 0xFE2CE6E0)
c = II(c, d, a, b, x[k+6], S43, 0xA3014314)
b = II(b, c, d, a, x[k+13], S44, 0x4E0811A1)
a = II(a, b, c, d, x[k+4], S41, 0xF7537E82)
d = II(d, a, b, c, x[k+11], S42, 0xBD3AF235)
c = II(c, d, a, b, x[k+2], S43, 0x2AD7D2BB)
b = II(b, c, d, a, x[k+9], S44, 0xEB86D391)
a = addUnsigned(a, AA)
b = addUnsigned(b, BB)
c = addUnsigned(c, CC)
d = addUnsigned(d, DD)
tempValue = wordToHex(a) + wordToHex(b) + wordToHex(c) + wordToHex(d)
return tempValue.lower()

校园网认证

在拿到密钥之后,发一个 Post 请求给服务器,校园网就认证成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE_NAME='.\md5_test.py'
PYTHON_PATH=${PWD}/$FILE_NAME
PASSWD=$(python $PYTHON_PATH $CHALLENGE)
echo ${PASSWD}


RES=$(curl -X POST "http://m.njust.edu.cn/portal_io/login" \
-H "Accept: application/json, text/javascript, */*; q=0.01" \
-H "Accept-Language: zh-CN,zh-HK;q=0.9,zh;q=0.8,en;q=0.7" \
-H "Connection: keep-alive" \
-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
-H "Cookie: zg_did=%7B%22did%22%3A%20%221830dcb70ae82e-07e49a52b9eb58-1b525635-384000-1830dcb70afe00%22%7D; zg_=%7B%22sid%22%3A%201662383780017%2C%22updated%22%3A%201662383858267%2C%22info%22%3A%201662383780017%2C%22superProperty% 22%3A%20%22%7B%7D%22%2C%22platform%22%3A%20%22%7B%7D%22%2C%22utm%22%3A%20%22%7B%7D%22%2C%22referrerDomain%22%3A%20%22ehall.njust.edu.cn%22%2C%22cuid%22%3A%20%22122106010798%22%7D" \
-H "Origin: http://m.njust.edu.cn" \
-H "Referer: http://m.njust.edu.cn/portal/index.html?redirect=aHR0cDovL3d3dy50YW9iYW8uY29tLw==" \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36" \
-H "X-Requested-With: XMLHttpRequest" \
--data-raw "username=122106010798&password=${PASSWD}&challenge=${CHALLENGE}" )

echo ${RES}

两段 Shell 可以写成一个脚本,这样运行一下就可以了。

这个脚本最开始是在 macOS 环境下写的。Linux 上运行是没啥问题,但是在 Windows 上就遇到麻烦了。

Windows 上运行 Shell 脚本

Windows 面临的第一个问题就是不能直接运行 Shell 脚本,因为没有 sh ,bash 或是 zsh 这样的终端。但好消息是,这是可以自己装的。更好的消息是,安装 Git 就直接装好了这些我们需要的东西。

安装 Git 会得到我们想得到的 Shell 程序。Git 自带有 sh 和 bash。他们的目录在 Git\bin\ 下面。找到 Git 的安装目录,把这个 bin 文件加入到系统变量里就行了。

另外我们在脚本里用了 jq,这个也是在 Windows 上没有的,需要自己安装。我这里用的是 scoop 来安装 jq。这里参考的是 scoop 在 gitee 上的镜像版。gitee 上好像有好几个,用哪个都可以。

scoop 是 win 上跟 apt,homebrew 一样的东西,是个强大的包管理工具。

我们在 PowerShell 中运行以下命令来安装 scoop。

1
2
3
4
5
6
# 脚本执行策略更改
> Set-ExecutionPolicy RemoteSigned -scope CurrentUser
# 输入Y或A,同意
> Y
# 执行安装命令
> iwr -useb scoop.201704.xyz | iex

然后换成国内源:

1
2
3
4
# 更换scoop的repo地址
> scoop config SCOOP_REPO "https://gitee.com/glsnames/scoop-installer"
# 拉取新库地址
> scoop update

然后执行安装命令就行了。

1
> scoop install jq

这样就 OK 啦!

Windows 上自动执行登录脚本

这一部分搞得我也有点头大,主要是 Windows 上这个任务计划程序太难用了。

然后我发现这个可以用 schtasks 命令行来解决,于是尝试了一下,确实可以。但是自动执行之后发现校园网没登录上去。又研究了一下,发现还是要回到任务计划程序改一下才行。

首先,先在 PowerShell 上执行一个 schtasks 命令:

1
> schtasks /create /sc minute /mo 30 /tn "autoLogin脚本" /tr C:\......\login.sh

具体的 schtasks 命令参数我也不太清楚,随用随查吧。总之这是个创建一个每 30 分钟执行一次 login.sh 这个文件的命令。

然后这时候跑到任务计划程序(直接在搜索里面搜就能找到,或者在控制面版里面找)就能看到我们刚刚创建的任务,名字就是 "autoLogin脚本"。然后编辑一下这个任务。

首先要把 常规 选项卡下面的 使用最高权限运行 这一项勾选。然后在 触发器 选项卡里面可以编辑一下触发器,把 重复时间间隔持续时间 这两项改一下。持续时间改成无限。

然后这个任务就创建成功了。正常的话每隔半小时就会执行一次。测试的时候可以先设置一个每三分钟执行一次的,看看有没有效果。