登入華科校園網,我用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