登录华科校园网,我用Socket
登录华科校园网,我用Socket
导语:
找一个华科学生问一问,学校的网络怎么样?得到的大多数是负面回答。其实不论是从覆盖区域、网络稳定性、还是速度来说,华科做的都还是可以的(24:00断网除外)。可是有一点我从进校以来就一直不爽,那就是校园网的认证方式是有线锐捷+无线web页面组合,并且无线网不能输入MAC来指定无感认证设备。真的是非常的安(má)全(fàn)啊!
这就意味着像esp32这类MCU没法使用无线网,特别是大一学生不能开通有线也没法装路由器,当时想用esp32做点东西的我十分郁闷。我从来到华科的第一天就想搞它了。使用Socket直接模拟网页认证,让esp32也能直接联网。
补充 :
做完之后也看了网上类似的博客,其他学校的同学也用Socket进行过类似的认证,可大部分没有提及跳转重定向和加密等重要部分,而且也都比较简短,没有分析整个认证过程,所以这一篇就尽量详细的还原整个过程,并且使用ESP32+micropython进行测试通过。所以,多图预警。
工具 :
FireFox浏览器、WireShark、Python3
0x00 观察
登录过程
这就是认证页面,在手机端上的模样与电脑端大同小异。一般我们输入正确的用户名(学号)、密码再点击按钮就能跳转到认证成功的页面上去了:
一般的,我会给电脑和手机开启无感认证,每次连接到校园网就不必手动认证,缺点在于不支持输入MAC进行无感认证。这就意味着设备必须支持浏览器才能进行认证,我们的目标也在于破除这一限制。
页面后台
单看网页前台能获得的信息十分有限,接下来就要去页面的实现代码上看一看了。按下F12,进入火狐的开发者工具:
因为页面非常的简洁,所以html内容较少,在调试器下我们能找到几个独立的JavaScript文件:
不难发现,登录认证的核心在于红框内的三个文件。他们的名字非常的坦白明晰啊,authinterface应该是负责认证的接口,security可能是负责加密,login_bch肯定也和登录脱不了干系,统统拿下来研究。
交待一下,我之前从没接触过JavaScript,HTML也只是了解几个标签的运作方式,为了能看懂这几个js,就连夜预习最终达到了能看懂的水平😢。
其中security.js
开头注释就说明了用途:
/*
* RSA, a suite of routines for performing RSA public-key computations in JavaScript.
* Copyright 1998-2005 David Shapiro.
* Dave Shapiro
* dave@ohdave.com
* changed by Fuchun, 2010-05-06
* fcrpg2005@gmail.com
*/
后面我们能看到,这是对密码传输进行加密的RSA算法。
然而三个文件加起来超过2300行,并且注释量不多,我还是决定通过调试找出整个登录的函数调用路线,去除无关内容的干扰。
刚开始做东西的时候不爱用调试工具,这一两年却是越来越喜欢了。不论软硬件都是开发一小时,调试一整天😂。
0x01 尝试
网络监控
仅仅是打开这些网页就有如下网络请求:
我随机写了账号和密码,点击了登录按钮:
出现了一个POST一个GET,通过类型我们能知道,GET是用来获取那张验证码图片的。那么重点就在于GET,其中一定包含了账户密码的上传认证。
果不其然,在POST的请求里我们看到了我输入的账号“1234567”,以及加密后的密码。这几个字段里queryString
包含了大量对于我本机的描述,IP、mac、网络名称等信息。最后一个字段passwordEncrypt
为Ture
按照字面意思来讲是开启密码加密。
以为成功了?
据此,我做了一个尝试,利用浏览器的编辑并重发功能,将这条POST中的passworEncrypt
改为false,并用我的账户替换了userId字段,用我的密码明文替换了password
字段内容并重新发送。我发现我已经获得了互联网连接,只是页面不会自动跳转。
进一步为了验证登录过程对其他的步骤有没有依赖,确保只发送这一个POST就可以完成认证,我用WireShark把这个POST的TCP包拿了出来,并掏出了高中以后再没用过的Python试了一下:
import socket,time
Host='172.18.18.60'
Port=8080
context='内容'
byte=context.encode()
def connect(byte):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((Host,Port))
print('[*]\r\n'+context)
s.sendall(byte)
time.sleep(0.5)
re=s.recv(1024).decode()
if("success" in re):
s.close()
return 1
else:
print('\r\n'+re)
s.close()
return 0
print(connect(byte))
结果还真的成了!?当时是晚上,我就挺高兴:没想到,这么个事这么快就解决了。
远远没有!
第二天晚上我再一次运行了脚本,却得到了如下信息:
{"userIndex":null,"result":"fail","message":"您当前使用的源IP与设备重定向地址中用户IP不一致,请重新认证!","forwordurl":null,"keepaliveInterval":0,"validCodeUrl":""}
果然人欢无好事,仅仅靠投机取巧获得的结论总是靠不住的。
感觉得出这与queryString里一长串的设备信息有关,要想发出正确的POST就必须有正确的queryString,可是queryString如何获取呢?要知道里面不仅仅有非常多的自身设备信息,更有AP设备的mac等信息,组织起来极其困难。除此以外,这些信息是加密过的,用站长工具尝试无果,除了一小部分字段是两次urlEncode,其他的加密方法即使到写这篇文章时都不得而知。
情况陷入了僵局,这就倒逼我回到我一度不愿意阅读的源码上去,毕竟源码之下无秘密。
0x02 调试
其实我从小就对浏览器按下F12的开发者工具很感兴趣,一直到这次摸索校园网登录才算是真正的用起来了。不由感叹网页的调试器真的很强大。
充分的运用搜索和倒推调用技巧之后,三个JavaScript文件的基本函数功能算是了解了,接着花了点时间学习浏览器的调试功能就直接上手了。
由于拙劣的技术,在打了无数个断点和中断事件之后,终于摸索出了整个流程。流程如下图所示:
- 从上带下表示调用的先后顺序
- 注意:图中对一些过程进行了简化,保留了核心功能,并不能完全代表整个过程。
流程分析
基本流程已经通过图片展示出来了,现在对图中标有数字序号的地方进行展开分析。
- document.location.search
- 隐藏的文本框
- passwordmac和encryptedpassword()
- 回调处理
1. document.location.search
这个值是对于当前的html页面来说的,也就是下图所示:
把这些值给到了queryString
2. 隐藏的文本框
看似简洁的登陆页面,在后台可以发现不少隐藏的文本框:
图中展现三个带有初始值的文本框,将他们标签中的hidden
去掉以后就可以看到了。
第一个框中的true
是passwordEncrypt的值,也就是默认加密。
而后两个框中的值:10001
和94dd2a8675fb779e6b9f7103698634cd400f27a154afa67af6166a43fc26417222a79506d34cacc7641946abda1785b7acf9910ad6a0978c91ec84d40b71d2891379af19ffb333e7517e390bd26ac312fe940c340466b4a5d4af1d65c3b5944078f96a1a51a5a53e4bc302818b7c9f63c4a1b07bd7d874cef1c3d4b2f5eb7871
组成了RSA加密的公钥,这就是华科校园网加密的公钥。
3. passwordmac和encryptedpassword()
这是一个变量的名称,它的定义是: var passwordMac = password+">"+macString;
也就是,接下来被处理的不是咱们的密码,而是:'密码>mac'
这还没完,我们继续追踪下去追踪到在AuthInterFace.js里的Encryptedpassword()函数:
function encryptedPassword(password){//有删减
var passwordEncode = password.split("").reverse().join("");//反转字符
var key = new RSAUtils.getKeyPair(publicKeyExponent, "", publicKeyModulus); //rsa加密公钥
var passwordEncry = RSAUtils.encryptedString(key,passwordEncode);//这里要对字符串进行反转,否则解密的密码是反的
return passwordEncry;
}
直观体会一下这个操作:
没搞明白这么做的目的是什么。。。
4. 回调处理
在发出包含一切信息的POST之后,我们会收到验证服务器JSON格式的回应,成功也好,不成功也罢,需要对响应进行处理。比如:
{"userIndex":null,"result":"fail","message":"您当前使用的源IP与设备重定向地址中用户IP不一致,请重新认证!","forwordurl":null,"keepaliveInterval":0,"validCodeUrl":""}
通过result=fail能知道认证失败,message可以告诉我们原因。
分析结果
至此,我们弄明白了网页认证的主干流程,也明白我们要做什么:
发送一个内容正确的POST给认证服务器,所以就要组织出正确的queryString。
可是分析一圈下来我们知道queryString来自于网页的的URL,可是这页面也是自己弹出来的啊!
这让网络技术薄弱的我陷入思考。。。
0x03 重定向
在这个过程中,不断地用WireShark抓包,遇到了不少的困难,好在最后找到了重定向的地址。
既然这个登陆页面可以自己弹出来,那么我们的电脑是从哪里获得这个页面的网址?百度之后,结论如下:
连接WiFi之后,系统会自动访问一些地址,比如获取时间或者专门验证是否联网的页面,在Windows下这个网址是:
http://www.msftconnecttest.com/redirect如果有网络的话最终会被转到MSN中国的页面上去。
那如果AP设置了登录页面,就会在系统自动访问上述页面的时候,通过一些手段给客户端强制返回登陆页面(重定向),然后就是我们看见的登陆页面。
为了能亲眼看看这个过程,漫长的抓包开始了。
一开始并没有什么收获,因为HTTP包数量非常多,而且我们不知道重定向页面的IP地址,也就没法进一步筛选。于是我又转向了浏览器。
我发现,如果在地址框里直接输入172.18.18.60:8080,也能跳转到带有一长串queryString的页面,毫不犹豫我勾选了调试器中的“在任何网址处暂停”
然后输入并访问172.18.18.60:8080。
页面暂停在了一个陌生的地址:123.123.123.123
这个网页没有其他内容,只有一句js:
<script>top.self.location.href='http://172.18.18.60:8080/eportal/index.jsp?wlanuserip=xxxxxxxxxxx&wlanacname=xxxxxxxxxxxxx&ssid=&nasip=xxxxxxxxxxx&snmpagentip=&mac=xxxxxxxxxxxx&t=wireless-v2&url=xxxxxxxxxxxxx&apmac=&nasid=xxxxxxxxxxxx&vid=xxxxxxxx&port=xxxxxxxxxx&nasportid=xxxxxxxxxxxxxxxx'</script>
这不就是queryString的来源吗!?
于是又发挥传统艺能验证了一下:
import socket
Host='123.123.123.123'
Port=80
con="GET / HTTP/1.1\r\nHost: 123.123.123.123\r\nUser-Agent: Python Socket\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n"
byte=con.encode()
def connect(byte):
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((Host,Port))
s.sendall(byte)
re=s.recv(1024).decode()
s.close()
if re != '' :
print(re)
href=re[(re.find('http://172')):(re.find('\'</script>'))]
print('\r\n'+href)
querystr=re[(re.find('wlanuserip')):(re.find('\'</script>'))]
print('\r\n'+querystr)
return('\r\n 1')
print(connect(byte))
结果喜人,验证通过。
除此以外,还发现123.123.123.123只能在未认证的情况下访问,在后面WireShark的抓包下,又发现了几个功能类似的地址,但是他们的地址显然没有123.123.123.123这么 讨人喜欢:
0x04 整合
将整个过程了解之后,实现自然是非常简单,将前文中的几个段落拼接修改之后不难得出最终版本:
直接使用Python3的socket与重定向和认证服务器建立TCP连接。
import socket,time
redirect_host='123.123.123.123'
redirect_port=80
login_host='172.18.18.60'
login_port=8080
redirect_request_str='GET / HTTP/1.1\r\nHost: 123.123.123.123\r\nUser-Agent: Python Socket\r\nAccept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nConnection: keep-alive\r\nUpgrade-Insecure-Requests: 1\r\n\r\n'
login_str_line='POST /eportal/InterFace.do?method=login HTTP/1.1\r\n'
login_str_headers='Host: 172.18.18.60:8080\r\nUser-Agent: Python Socket\r\nAccept: */*\r\nAccept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2\r\nAccept-Encoding: gzip, deflate\r\nContent-Type: application/x-www-form-urlencoded; charset=UTF-8\r\nContent-Length: duetocontent\r\nOrigin: http://172.18.18.60:8080\r\nConnection: keep-alive\r\n\r\n'
login_str_content_head='userId=theuserid&password=thepassword&service=&queryString='
login_str_content_tail='&operatorPwd=&operatorUserId=&validcode=&passwordEncrypt=false'
def info_request(redirect_host,redirect_port,redirect_request_str):
#在重定向处获取queryString
print('[*]requesting redirection : \r\n')
flag=0
while(1):
print('[*]trying \r\n')
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((redirect_host,redirect_port))
s.sendall(redirect_request_str.encode())
re=s.recv(1024).decode()
s.close()
if(re == ''):
flag=flag+1
if(flag == 3):
return 0
continue
else:
querystr=re[(re.find('wlanuserip')):(re.find('\'</script>'))]
print('[*]requesting success \r\n')
print(querystr+'\r\n')
return querystr
def login(login_host,login_port,querystr,id=None,pwd=None):
if(id == None or pwd == None):
print('[*]Please check the account.\r\n')
return 0
print('[*]trying to login \r\n')
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect((login_host,login_port))
global login_str_headers
global login_str_content_head
login_str_content_head=login_str_content_head.replace('theuserid',id).replace('thepassword',pwd)
querystr=querystr.replace('=','%253D')
querystr=querystr.replace('&','%2526')
content=login_str_content_head+querystr+login_str_content_tail
login_str_headers=login_str_headers.replace('duetocontent',str(len(content)))
login_str=login_str_line+login_str_headers+content
print(login_str+'\r\n')
s.sendall(login_str.encode())
#time.sleep(0.5)
re=s.recv(1024).decode()
s.close()
if("success" in re):
print('[*]login Successfully \r\n')
return 1
else:
print('[*]login failed \r\n')
print(re)
return 0
query=querystr=info_request(redirect_host,redirect_port,redirect_request_str)
print(login(login_host,login_port,query,id='',pwd=''))
Python水平也蛮差的,也就只能应付这样的小场面了。。。。。
0x05 测试
为了不影响正常使用,我整个过程研究的都是2.4G的信号,这也正好符合咱们的目标是ESP32这一类设备。
实际测试,两个信号都可以用脚本登录。
ESP32测试
不能忘记咱们是为什么开始的呀,ESP32才是事情的起源,也是咱们的目标。
micropython真的是非常方便,之前的代码几乎直接复制,再加一个WiFi连接就可以了,一遍过。
这个RT-Thread发布的micropython插件真的挺好用的,编辑器竟然支持代码补全。
0x06 总结
总体来讲,整个持续时间只有四五天,正值期末考试周。这个小项目成了我放(huá)松(shuǐ)的好机会。
第一次读JavaScript,第一次运用浏览器调试,第一次使用WireShark。有些过程在文章里展示的不多,但的确耗费了大量的时间。
这一次,对网络的认识又加深了几分。
在查找资料的时候,我了解到mentohust这个由10多年前的华科大神编写的校园网认证软件,了解到现在很多学校的学生都在用mentohust来进行锐捷认证,算是受到一些感召。期间,联系到一位今年刚毕业的华科计科学长,也非常感谢前辈们的帮助。
代码虽短,但也放到了GitHub上去了。如果有同好想移植到不同的平台或者用其他语言实现可以汇总起来方便查找。
仓库地址:https://github.com/HuXioAn/HUST_Wireless_login_by_socket
接下来可能会去折腾一下梅林固件、dd-wrt上的mentohust认证。
技术新人,水平一般,能力有限,如果文章中或者代码有任何疑问或者错误请不吝赐教,一定指出!
看更多相关文章或者联系我请看公众号,来找我聊聊天吧:
欢迎转载,请注明
作者:胡小安 https://blog.csdn.net/qq_28039135?spm=1000.2115.3001.5343