更新记录:

目前该文章只是大致分析,由于时间和个人能力导致还有许多地方并没分析或者分析的结果并不完全正确.

后续内容敬请期待 等有空了再更新

Todo

- [ ] MAC重绑定认证 MAC地址应该在网关处获取,和自己的设备应该无关

  • 完善细节

一些发现

2024.3.17 - 在认证时过程中iv值不使用表单所给的值似乎也可以填了16个f也能过

2024.3.6- 网页端认证发送的mac为空:
但是连hpu_wifi的时候就提示你已绑定其他MAC了(显示的自己WiFi的MAC)
因此猜测可能是获取报文中的MAC地址
先根据发送请求中的用户标识判断设备,然后再判断MAC地址.

2024.3.5- 有线换无线都能认证成功=>

有线和无线连接AP时分配的不在同一个网段:

无线分配的IP是10.45开头,网关是10.45.0.1
有线分配的IP是10.46开头,网关是10.46.0.1

前言

此内容可能只适合河南理工大学的GiWifi的校园网认证.
如果是其他的学校的GiWifi,可以参考这篇文章

目前该脚本已放到GitHub上面 Auto-Giwifi-Nodejs 等一个好心人写一个Python版本的

已有好心人帮忙写了个shell脚本,GiwifiScript

仅限于网页登陆

app端抓包有点麻烦,就懒得搞了.

准备

本脚本是利用nodejs来实现的.

引入的依赖有 axios cheerio crypto-js
然后使用的是vercel/ncc进行打包 因为webpack没整明白,而且还老是报错

分析

首先进入校园网的登陆界面:
http://10.53.1.3/gportal/web/login

此时我们按F12打开开发者工具

元素 可以查看元素(源代码里也能看)

源代码/来源可查看源代码

使用Ctrl+F 搜索 text/JavaScript 查看js脚本,
一共有16条结果,但是在这里就展示比较重要的部分,感兴趣的同学可以自己慢慢研究.

流程

从进登陆页面到完成认证的整个过程中一共有3次http请求

  1. 进入校园网登陆页面 get
  2. 输入账号密码之后点击登录 post
  3. 请求queryAuthState,查询账号状态 post

其中后面两次你可以通过浏览器的开发工具抓包得到.

(查询账号状态的请求用处不大,网页展示用的.)

加密参数

1
2
3
4
5
6
7
8
9
10
11
function cryptoEncode(data){
var iv = $("#iv").val();
//console.log('JS加密前:'+data);
var key = CryptoJS.enc.Utf8.parse('1234567887654321');
var ivv = CryptoJS.enc.Utf8.parse(iv);
var encrypted = CryptoJS.AES.encrypt(data, key, { iv: ivv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding });
data = encrypted.toString();
//console.log('JS加密后:'+data);
var msg = {'data':data,'iv':iv};
return msg;
}

通过以上片段可以看出此处使用了CryptoJS gitbook文档

ChatGpt解释
这段 JavaScript 代码是一个用于加密数据的函数。让我解释一下它的工作原理:

  1. cryptoEncode 函数接受一个参数 data,即待加密的数据。
  2. 通过 $("#iv").val() 获取了一个 IV(Initialization Vector)值,IV 用于增加加密算法的安全性,确保即使对同样的数据进行多次加密,每次生成的密文都不同。
  3. 使用 CryptoJS 库,将提供的密钥和 IV 转换成适合加密算法使用的格式。这里密钥是 ‘1234567887654321’,IV 是从 HTML 元素 #iv 中获取的值。
  4. 使用 AES 加密算法对数据进行加密,加密方式为 CBC 模式,使用 ZeroPadding 进行填充。
  5. 将加密后的数据转换成字符串格式,并赋值给 data 变量。
  6. 将加密后的数据和 IV 组装成一个对象 msg
  7. 返回包含加密数据和 IV 的对象 msg

个人补充:

$("#iv").val() 是jQuery的语法,能够获取指定标签中的值.

数据经过AES加密重新赋值给data,然后把获得到的iv值一起封装到msg对象里面并返回.

重点在这里,这里就是河理的Giwfi和其他学校的Giwif不同的地方.
之前在网上查找发现大多数学校的认证时的数据直接就能抓包到,
而河理的则是先把数据在本地加密然后再进行发送.

登录

总片段

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
function login(){
var btn = $("#first_button");
var name = $('#first_name').val();
if(!name){
showmessage("账号不能为空!", 4,null);
return false;
}
/*if (!reg.test(name)) {
showmessage("账号格式无效!", 4,null);
return false;
}*/
var password = $('#first_password').val();
if(!password){
showmessage("密码不能为空!", 4,null);
return false;
}
if($('#serverAgreement').length > 0){
var checkAgreement = $('#serverAgreement').prop("checked");
if(!checkAgreement){
showmessage("请同意并勾选用户服务协议。", 4,null);
return false;
}
}

//BEGIN:fengjing add 2020-11-26
if($('#login-protocol').length > 0){
var agreement = $('#login-protocol').data('selected');
if (agreement != 2) {
showmessage("请同意并勾选用户服务协议。", 4,null);
return false;
}
}
//END:fengjing add 2020-11-26

btn.attr('disabled', 'disabled');

//
showWait();

// 加密
var msg = cryptoEncode($("#frmLogin").serialize());
$.ajax({
url : "/gportal/web/authLogin?round=" + Math.round(Math.random() * 1000),
data : msg,
type : "post",
async : false,
dataType : "json",
success : function(data) {
closeWait();
if (data.status === 1) {
showmessage(data.info, 4, data.data.redirectUrl);
$("#first_button").removeAttr('disabled');
} else {
$("#first_button").removeAttr('disabled');
//doFailedLogin(data, "frmLogin");
if (data.data.reasoncode == "40") {
art.dialog({
content: '您需要先完善自己的个人信息!',
okValue: '确定',
ok: function () {
//window.location.href = c.data.complete_data_url;
window.open(data.data.complete_data_url);
}
});
}else if(data.data.reasoncode == "32"){
var name = data.data.name;
showmessage(data.info,4,null);
resetpage();
$("#reset_name").attr("value",name);
} else if(data.data.reasoncode == "41"){
art.dialog({
content: data.info,
okValue: '确定',
ok: function () {
var name = data.data.name;
bindpage();
var identitytype = data.data.identitytype;
var hotspotgroupid = data.data.hotspotgroupid;
var hotspotid = data.data.hotspotid;
var password = data.data.password;
$("#bind_identity_type").attr("value",identitytype);
$("#bind_identity_id").attr("value",name);
$("#bind_hotspot_group_id").attr("value",hotspotgroupid);
$("#bind_hotspot_id").attr("value",hotspotid);
$("#bind_password").attr("value",password);
}
});
} else if (data.data.reasoncode == "43") {
art.dialog({
content: data.info,
okValue: '确定',
cancelValue: '取消',
ok: function () {
this.close();
rebindMac("frmLogin","",data.data.bindmac, 1);
return false;
},
cancel: function () {
this.close();
return false;
}
});
}else {
showmessage(data.info, 6, null);
}

}
return false;
},error:function(jqXHR,textStatus,errorThrown){
closeWait();
showmessage("网络出错,请联系客服", 4, "http://1.2.3.4");
},beforeSend:function(){
showWait();
}
});
}

由于代码比较长,下面只展示有用的.

账号密码判断

1
2
3
4
5
var name = $('#first_name').val();
if(!name){
showmessage("账号不能为空!", 4,null);
return false;
}
1
2
3
4
5
var password = $('#first_password').val();
if(!password){
showmessage("密码不能为空!", 4,null);
return false;
}

比较简单.

发送请求

1
2
3
4
5
6
7
8
9
10
11
var msg = cryptoEncode($("#frmLogin").serialize());
$.ajax({
url : "/gportal/web/authLogin?round=" + Math.round(Math.random() * 1000),
data : msg,
type : "post",
async : false,
dataType : "json",
success : function(data) {
closeWait();
if (data.status === 1) {
showmessage(data.info, 4, data.data.redirectUrl);
1
var msg = cryptoEncode($("#frmLogin").serialize());

在你发送请求之后,如果成功,会提示你认证成功,稍后跳转,并且还会有一个重定向的Url

分析
此处分为两部分

  1. $("#frmLogin").serialize() : jQuery中用于将id为frmLogin的表单元素的值序列化为一个URL编码的字符串的方法

    下面是例子:

    1
    2
    3
    4
    <form id="frmLogin">
    <input type="text" name="username" value="John">
    <input type="password" name="password" value="123456">
    </form>

    使用serialize() 会得到username=John&password=123456

  2. cryptoEncode(data): 数据加密

1
2
3
4
5
6
7
8
9
10
$.ajax({
url : "/gportal/web/authLogin?round=" + Math.round(Math.random() * 1000),
data : msg,
type : "post",
async : false,
dataType : "json",
success : function(data) {
closeWait();
if (data.status === 1) {
showmessage(data.info, 4, data.data.redirectUrl);

此处是发送http异步请求

  1. url : 请求的url,并且后面加一个随机数(防止浏览器缓存该请求)
  2. data: 传送的数据,在这里传送的是对象msg
  3. type: 请求类型为post
  4. async: false,该请求为同步进行,收到结果后才会执行后面的代码
  5. dataType: 指定服务器返回的数据类型
  6. success: 如果请求发送成功之后就执行这个函数

函数中的内容是: 如果返回的数据中的status 的值等于1,则进行showmessage函数,重定向至服务器传过来的url.

你理的登录接口参数里重要的东西不多,没有 GiWiFI校园网认证过程分析与模拟登录 这里的详细.大概是为了防止学生抓包罢.好的不学,非得这个
盲猜是根据客户端发送的请求再服务端进行判断,然后只返回相关的内容

绑定MAC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function rebindMac(div,paramStr,bindmac,operType){
var round = Math.round(Math.random()*1000);
//加密
var userMac = $("#userMac").val();
if (isEmpty(userMac) && !isEmpty(bindmac)) {
$("#userMac").val(bindmac);
//$("#userMac").attr("value",bindmac);
}
var params = $("#"+div).serialize();
var msg = cryptoEncode(params);
$.post("/gportal/web/reBindMac?round="+round+paramStr, msg, function(result) {
//result = eval('('+result+')');
showmessage(result.info, 2,null);
if (result.status === 0) {
return false;
} else {
return true;
}
});
}

div 就是获取表单中的值然后进行url格式编码,
paramStr给的是空字符串
operType没用,没找到用到的第二个地方.

当检测到登录MAC和绑定的MAC不同时,会跳出弹窗提示
然后当你点击确定时,会把userMac的值设成bindMac的值,然后再将表单中元素的值序列化成url编码格式,然后再加密,加密之后向上面的连接中发送post请求.
(没搞懂paramStr传的时候明明时空字符串,为什么在模拟的时候却说param is unknown)
(还有这个operType翻网页源码就找到一条匹配结果)

下为判断mac地址方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function isEmpty(v) {
switch (typeof v) {
case 'undefined':
return true;
case 'string':
if (v.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!v) return true;
break;
case 'number':
if (0 === v || isNaN(v)) return true;
break;
case 'object':
if (null === v || v.length === 0) return true;
for (var i in v) {
return false;
}
return true;
}
return false;
}

这里只解释类型为字符的情况
if (v.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true
说的是去掉字符串中去除首位空格,制表符,换行符之后,如果长度为0,就返回true,否则返回false

信息返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var showmessage = function(info, sec, url) {
art.dialog({
id : 'testID',
content : info,
lock : true,
init : function() {
var that = this, i = sec;
var fn = function() {
that.title(i + '秒后关闭');
!i && that.close();
i--;
};
timer = setInterval(fn, 1000);
fn();
},
close : function() {
clearInterval(timer);
if (url != null) {
window.location.href = url;
}
}
}).show();
}

根据对应操作,给showmessage函数传入不同的参数

  1. info 展示的信息
  2. sec 弹窗展示的秒数
  3. url 重定向链接

如果传来的url不为空,则重定向至传来的url

下图为效果:

查询登录状态

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
function checkStatus(){
var sign = $("input[name='sign']").val();;
var reset = $(".long").data("reset");
onlineFlag=true;
$.ajax({
"type": "POST",
"url": "/gportal/web/queryAuthState",
"dataType": "json",
"async":false,
"data": {"sign":sign},
"success": function (c) {
if(c.status == 1){
onlineFlag = true;
if(reset == 1){
var duration = c.data.data.onlineTime;
var h = parseInt(duration/3600);
var m = parseInt((duration-h*3600)/60);
var s = (duration - h*3600 - m*60);
h = h.toString();
m = m.toString();
s = s.toString();
if(h.length<2){
h = "0"+h;
}
if(m.length<2){
m = "0"+m;
}
if(s.length<2){
s = "0"+s;
}
$(".long").html(h+":"+m+":"+s);
var start = new Date();
var timestamp = parseInt(start.getTime()/1000)-parseInt(duration);
$(".long").data("timestamp",timestamp);
$(".long").data("reset","2");
}
}else {
onlineFlag = false;
}
}
});
if(!onlineFlag){
$("#offline").css("background-color","grey");
$("#offline").css("disabled","disabled")
$("#offline").html("已下线");
showmessage("已下线,稍后跳转", 4, 'http://1.2.3.4');
}
}

此处是验证设备的登陆状态的函数
var sign = $("input[name='sign']").val();
获取签名值.
然后发送post请求给queryAuthState页面,同时把sign值传过去.
等待结果返回(JSON格式)
如果返回的结果中的status的值为1,同时reset的值也为1,则展示你的在线时间.

如果status的值不为1,则把onlineFlag设为false.
跳出弹窗,提示已下线.

注意:
获取的sign值在模拟登录的时候需要再进行解码处理,否则会报错误代码

这个对于脚本登录没啥用,给浏览器登录的用户进行状态展示用的

登出:

登出是发送post请求给/gportal/web/authLogout
传输的参数是nasName=&nasIp=&userIp=&userMac=&ssid=&apMac=&pid=23&vlan=&sign=%2BooXpv2DSYg1wA%3D%3D&iv=(sign已处理)

验证:

通过一次手动认证,抓包一下认证时传送的数据

dataiv的值复制下来

这是解密函数

1
2
3
4
5
6
7
8
9
10
11
const CryptoJS = require('crypto-js');
var iv = CryptoJS.enc.Utf8.parse("");
var key = CryptoJS.enc.Utf8.parse("1234567887654321"); //HPU的是这个,其他学校的就不知道了.需要自己找

function Decrypt(word) {
let decrypt = CryptoJS.AES.decrypt(word, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.ZeroPadding });
let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}
var result1 = Decrypt("");
console.log("解密后的数据"+result1)

iv的值填到CryptoJS.enc.Utf8.parse("");的双引号里面

data的值填到Decrypt("");的双引号里面

解密后的数据就是你在点击登录之后提交的数据项及它们的值,里面包含了你的账号和密码.

(其实这些数据项直接用f12查看元素就能找到,不过你会发现登录页面只展示了账号和密码,别的nasName,nasIp,userIp这些都被隐藏起来了)

这里解密用来验证一下自己的猜想(

模拟登录

登录相关的接口和参数都了解的差不多了.
可以总结出大致的流程.

  1. 向登陆页面发送一次get请求,得到返回的html页面
  2. 使用cheerio来获得登录所需要的数据
  3. 填充好账号密码之后本地加密数据
  4. 把加密好的数据和iv值用post请求发送过去
  5. 发送post请求给queryAuthState页面,来获取登录状态(非必需)