前言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