GiWiFI校园网认证过程分析与模拟登录

GiWiFI校园网认证过程分析与模拟登录

前言GiWiFi是由上海寰创网络科技有限公司基于wifidog项目开发的一套网关管理系统, 通常被应用于校园网行业.

认证机制由于GiWiFi是基于wifidog项目的, 所以大致流程也差不多

基本流程用户上线

用户访问网络,通过iptables将未认证的用户dnat到wifidog进程,wifidog通过307报文将用户重定向到认证服务器

用户打开认证服务器登录页面,输入用户名密码,发送认证请求

认证成功的话服务器会发送302报文,携带token信息重定向到wifidog页面。认证失败的话会返回失败页面

用户携带token信息向wifidog发起认证请求,wifidog再向认证服务器发起请求,认证成功后授权,并将用户重定向到成功页面

保活和下线

wifidog会定时向认证服务器发送保活消息(相当于心跳)

当用户主动请求下线后,wifidog此时并没有下线

当wifidog再次发起保活请求时,认证服务器会告诉它用户已下线,此时wifidog会将用户下线

网页登录客户端引导下载页对于不同的设备和系统使用了UA进行区分,经过测试,提供的客户端有Windows、macOS、iOS以及Android版本(果然Linux又被忽视了,显示的是Android版的下载按钮🌝)

由于对此类认证客户端的排斥和心理洁癖,我便开始寻找使用网页认证,甚至是使用脚本模拟认证的方法,果不其然,经过一番分析与搜寻,我找到了一些东西

你藏得好深啊,登录框此前网页的认证过程为

打开任意http页面后被劫持至172.17.1.1:8062/redirect

返回307跳转至认证页

在认证页里输入账号密码登录认证

关闭认证页,一段时间内即可正常上网

而现在跳转后仅显示客户端的下载按钮,直觉意识到这是一个专用于客户端引导下载的页面,真正的认证页仍另藏他处

事实正是如此,对比历史记录,现在的跳转页面的域名是172.17.1.1(校园网网关IP),此前的认证页则是http://login.gwifi.com.cn/cmps/admin.php/api/login/

修改UA先修改UA为ipad(使用UA浏览器插件即可,或者浏览器里的自定义UA)

1Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25

将客户端引导下载页的URL参数粘贴到认证页的URL后并打开,就能看到之前的认证页了(单纯打开首页会显示It works! ,不加任何参数打开的话则是跳转至新浪首页,阿巴阿巴~)

1http://login.gwifi.com.cn/cmps/admin.php/api/login/?gw_address=172.17.1.1&gw_port=8060&gw_id=GWIFI-zhongbeixinshang01&ip=10.16.100.174&mac=你的MAC地址

认证页打开之后仍是一个大大的客户端下载按钮,但不要慌。打开审查元素就会发现,所有的登录框、重设密码框、注册框等都在,只是被隐藏掉了

去掉隐藏样式,正常输入账号密码登录即可,和之前的操作一模一样(登录后会跳转至百度首页,看来这两个页面是同一位鬼才写的🌝)

登录接口及参数分析找到了登录框之后,就可以开始分析接口和参数了

直接看页面代码吧,写的挺乱的,好在未经过混淆,关键部分:

登录接口123456789101112131415161718192021222324252627var loginAction = function(params){ var btn = $("#first_button"); var round = Math.round(Math.random()*1000); var form = $("#frmLogin"); $.ajax({ url: "/cmps/admin.php/api/loginaction?round="+round, data: form.serialize(), type: "post", async: false, dataType: "json", success: function (data) { if (data.status === 1) { if(data.data.reasoncode == "44"){ params = getWechatParams(data); //showSelectMessage(params); wechatAuth(params.okParams); }else{ window.location.href = data.info; } } else { btn.removeAttr('disabled'); doFailedLogin(data,"frmLogin"); return false; } } });}

可以看到接口为/cmps/admin.php/api/loginaction,参数都在登录表单里:

关键部分已手动打码

参数名

说明

access_type

2

作用未知

acsign

***

登录状态接口中的sign字段

btype

pc

猜测为平台类型

client_mac

***

客户端MAC

contact_phone

400-038-5858

服务电话

devicemode

默认空值,作用未知

gw_address

172.17.1.1

网关地址

gw_id

***

AP的SSID

gw_port

8060

网关端口

lastaccessurl

默认空值,作用未知

logout_reason

0

作用未知

mac

***

同client_mac

name

***

账号

online_time

0

猜测为在线时间,作用未知

page_time

1535509645

登录页时间戳

password

***

密码

sign

***

签名,可从登录表单中获取

station_cloud

login.gwifi.com.cn

作用未知

station_sn

***

猜测为基站ID

suggest_phone

400-038-5858

同contact_phone

url

http://www.baidu.com

登录成功后跳转的网站

user_agent

默认空值,作用未知

观察后发现登录所需的大部分参数在认证页的URL参数里已经有了,剩下的有一部分已经在登录表单里填好了,另一部分需要从下文的登录状态接口中取到,将其组合起来后发送POST请求

登录成功后返回JSON数据:

1234567891011121314151617181920212223{ "status":1, "info":"http://172.17.1.1:8060/wifidog/auth?token=***&info=***", "data":{ "auth_verify":1, "reasoncode":0, "remain_time":1053640, "limit_time":null, "cost_type":4, "serviceplan_id":"1357", "is_share":"2", "wechat_enable":"1", "bw_up":"2048", "bw_down":"10240", "ontrial":0, "need_complete_data":null, "complete_data_url":null, "permit_intranet":2, "permit_internet":2, "carrier_operator":"3", "network_type":"2" }}

其中的info字段的URL用作登录验证,使用GET请求就可以完成整个认证登录的流程了

登录失败的话info字段则会返回百度首页的URL,再次吐槽一下🌝

12345{ "status":1, "info":"http:\/\/www.baidu.com", "data":1}

登录状态接口1234567891011121314151617181920function initData(){ //获取终端信息 $.ajax({ url: "http://172.17.1.1:8060/wifidog/get_auth_state?ip=***&mac=***&sign=***&callback=***", dataType:'jsonp', success: function(data) { c = eval('(' + data.data + ')'); if(data.resultCode == 0){ fixData(c); }else { window.top.location.href = "http://www.baidu.com"; } return false; }, error:function(data) { return false; }, cache: false });}

同样可以看到接口为/wifidog/get_auth_state,参数为IP、MAC、签名和回调函数名,其中的签名可以直接在页面表单里取到

返回结果为JSONP数据,提取为:

1234{ "resultCode":0, "data":"{"auth_state":2,"gw_id":"***","access_type":"2","authStaType":"0","station_sn":"***","client_mac":"***","online_time":11,"logout_reason":7,"contact_phone":"400-038-5858","suggest_phone":"400-038-5858","station_cloud":"login.gwifi.com.cn","orgId":"899","sign":"***"}"}

其中的auth_state字段值为2时为正常登录状态

观察登录成功后执行的操作,是替换了部分表单数据:

123456789101112function fixData(data) { $(".gw_id").val(data.gw_id); $(".access_type").val(data.access_type); $(".station_sn").val(data.station_sn); $(".client_mac").val(data.client_mac); $(".online_time").val(data.online_time); $(".logout_reason").val(data.logout_reason); $(".contact_phone").val(data.contact_phone); $(".suggest_phone").val(data.suggest_phone); $(".station_cloud").val(data.station_cloud); $(".acsign").val(data.sign);}

进行模拟登录时也应一一替换

需要注意的是参数callback是必需的,不然将不会返回sign字段值

登出接口登出功能在客户端引导下载页上,接口及参数为http://172.17.1.1/getApp.htm?action=logout

返回数据为:

1234{ "resultCode":0, "data":[]}

其中resultCode字段值为0时登出成功

模拟登录脚本现在登录相关接口和所需参数已经了解,可以开始写模拟登录了

这里使用Python来实现,理论上对所有GiWiFi系统通用

这里需要事先安装好Python以及requests、netifaces库

你可能会用到Visualcppbuildtools(安装C++插件)

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231# -*- coding: utf-8 -*-import osimport reimport timeimport jsonimport argparseimport requestsimport netifacesfrom getpass import getpassfrom urllib.parse import urlparse, parse_qsSCRIPT_VERSION = "1.0.3.2"HEADERS = { 'User-Agent': 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25', 'accept-encoding': 'gzip, deflate, br', 'accept-language': 'zh-CN,zh-TW;q=0.8,zh;q=0.6,en;q=0.4,ja;q=0.2', 'cache-control': 'max-age=0'}PARSER = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description='GiWiFi认证登录脚本', epilog='(c) 2018 journey.ad')PARSER.add_argument('-g', '--gateway', type=str, help='网关IP')PARSER.add_argument('-u', '--username', type=str, help='用户名')PARSER.add_argument('-p', '--password', type=str, help='密码')PARSER.add_argument('-q', '--quit', action='store_true', help='登出')PARSER.add_argument('-d', '--daemon', action='store_true', help='在后台守护运行')PARSER.add_argument('-v', '--verbose', action='store_true', help='额外输出一些技术性信息')PARSER.add_argument('-V', '--version', action='version', version='giwifi-auth-helper {}'.format(SCRIPT_VERSION))CONFIG = PARSER.parse_args()if not CONFIG.quit: if not CONFIG.gateway: CONFIG.gateway = netifaces.gateways()['default'][netifaces.AF_INET][0] if not CONFIG.password: CONFIG.gateway = input('请输入网关地址(%s):' % (CONFIG.gateway)) or CONFIG.gateway if not CONFIG.username: CONFIG.username = input('请输入上网账号:') if not CONFIG.password: CONFIG.password = getpass('请输入账号密码:')else: if not CONFIG.gateway: CONFIG.gateway = netifaces.gateways()['default'][netifaces.AF_INET][0] CONFIG.gateway = input('请输入网关地址(%s):' % (CONFIG.gateway)) or CONFIG.gatewaydef main(): logcat('正在获取网关信息…') try: authUrl = requests.get('http://%s:8062/redirect' % (CONFIG.gateway), timeout=5).url authParmas = {k: v[0] for k, v in parse_qs(urlparse(authUrl).query).items()} loginPage = requests.get('http://login.gwifi.com.cn/cmps/admin.php/api/login/?' + urlparse(authUrl).query, headers=HEADERS, timeout=5).text pagetime = re.search(r'name="page_time" value="(.*?)"', loginPage).group(1) sign = re.search(r'name="sign" value="(.*?)"', loginPage).group(1) except requests.exceptions.ConnectionError: logcat('连接失败,请检查网关地址是否正确') return except requests.exceptions.Timeout: logcat('连接超时,可能已超出上网区间') return except AttributeError: logcat('解析失败,可能网关设备重启或系统已更新') return authState = getAuthState(authParmas, sign) if CONFIG.quit: logout(authParmas) if not authState: return else: if authState['auth_state'] == 2: printStatus(authParmas, authState) logcat('你已登录,无需再次登录') else: data = { 'access_type': authState['access_type'], 'acsign': authState['sign'], 'btype': 'pc', 'client_mac': authState['client_mac'], 'contact_phone': '400-038-5858', 'devicemode': '', 'gw_address': authParmas['gw_address'], 'gw_id': authParmas['gw_id'], 'gw_port': authParmas['gw_port'], 'lastaccessurl': '', 'logout_reason': authState['logout_reason'], 'mac': authParmas['mac'], 'name': CONFIG.username, 'online_time': authState['online_time'], 'page_time': pagetime, 'password': CONFIG.password, 'sign': sign, 'station_cloud': 'login.gwifi.com.cn', 'station_sn': authState['station_sn'], 'suggest_phone': '400-038-5858', 'url': 'http://www.baidu.com', 'user_agent': '', } if CONFIG.verbose: logcat(data) result = login(data) if result['status']: authState = getAuthState(authParmas, sign) printStatus(authParmas, authState) if authState['auth_state'] == 2: logcat('认证成功') else: logcat('认证失败') else: logcat('认证失败,提示信息:%s' % (result['info']))def login(data): logcat('正在尝试认证…') try: resp = json.loads(requests.post('http://login.gwifi.com.cn/cmps/admin.php/api/loginaction', data=data, timeout=5).text) result = { 'status': False, 'info': None } if CONFIG.verbose: logcat(resp) if 'wifidog/auth' in resp['info']: requests.get(resp['info']) result['status'] = True else: result['info'] = resp['info'] except requests.exceptions.Timeout: logcat('连接超时,可能已超出上网区间') finally: return resultdef logout(authParmas): try: resp = json.loads(requests.get('http://%s/getApp.htm?action=logout' % (authParmas['gw_address'])).text) except requests.exceptions.Timeout: logcat('连接超时,可能已超出上网区间') return if resp['resultCode'] == 0: logcat('下线成功') else: logcat('下线失败')def getAuthState(authParmas, sign): try: params = { 'ip': authParmas['ip'], 'mac': authParmas['mac'], 'sign': sign, 'callback': '' } resp = json.loads(requests.get('http://%s:%s/wifidog/get_auth_state' % (authParmas['gw_address'], authParmas['gw_port']), params=params, timeout=5).text[1:-1]) except KeyError: logcat('所需参数不存在') return False except requests.exceptions.Timeout: logcat('连接超时,可能已超出上网区间') return False if CONFIG.verbose: logcat(resp) if resp['resultCode'] == 0: return json.loads(resp['data']) else: return Falsedef printStatus(authParmas, authState): if not CONFIG.verbose: clear() print('''--------------------------------------------SSID: %sAP MAC: %sGateWay: %sIP: %sMAC: %sStation SN: %sLogged: %s--------------------------------------------''' % ( authParmas['gw_id'], authParmas['apmac'], authParmas['gw_address'], authParmas['ip'], authParmas['mac'], authState['station_sn'], 'yes' if(authState['auth_state'] == 2) else 'no' ) )def clear(): os.system('cls' if os.name == 'nt' else 'clear')def logcat(msg, level='I'): print('%s %s: %s' % (time.ctime().split(' ')[-2], level, msg))if __name__ == '__main__': if CONFIG.daemon: while True: main() time.sleep(30) else: main() input()

效果

本文参考:

1.https://nocilol.me/archives/lab/giwifi-auth-process-analysis-and-simulation-login/

2.https://github.com/icepie/giwifi-gear/wiki/GiWiFi-%E5%88%86%E6%9E%90

相关推荐

世界杯开幕式前墨西哥城爆发示威:足球狂欢背后的“社会裂痕”
365最专业的数据服务平台

世界杯开幕式前墨西哥城爆发示威:足球狂欢背后的“社会裂痕”

📅 06-16 👁️ 9048
科学运动、全能手环:荣耀手环5的10天详细体验报告
365最专业的数据服务平台

科学运动、全能手环:荣耀手环5的10天详细体验报告

📅 06-30 👁️ 9090
国内旅行结婚最佳路线
beat365体育亚洲

国内旅行结婚最佳路线

📅 07-29 👁️ 9057
直播间怎么能快速涨人气?提升人气技巧有哪些?2025-03-01 20:13:073463浏览
值得去的IT公司(武汉篇)
365最专业的数据服务平台

值得去的IT公司(武汉篇)

📅 08-05 👁️ 6512
喊麦软件有哪些?好用的喊麦软件盘点
365最专业的数据服务平台

喊麦软件有哪些?好用的喊麦软件盘点

📅 08-30 👁️ 4188