校園網斷線重連,用爬蟲來搞定!

2021.11.08

JS解密 ,當然現在隨著加加殼方式多樣多彩,反爬手段也越發高明,很多網站尤其是有商業性質數據網站是真的很難搞。


前言

hello,大家好,我是大賽哥(),好久不見,甚是想念。

 

最近因為有小需求研究了兩登錄的加密,也成功解密加密的參數,在這裡給大家分享一波。

 

前段時間,有個同學他實驗室服務器校園網老是掉,想問問有沒有啥斷線重連的方法。


當時因為比較忙並沒有研究,並且也很久沒有搞了,昨天沒事的時候研究分析了一下,這個過程可能對有基礎的人來說是個小菜一碟,但是對於沒了解過的可以體驗一波說不定後面就用得著。有時候會了一個其他的再會,也就簡單了。

 

這個內容的範疇屬於爬蟲中的進階:JS解密 ,當然現在隨著加加殼方式多樣多彩,反爬手段也越發高明,很多網站尤其是有商業性質數據網站是真的很難搞。

 

大部分網站,都需要進行權限認證和管理,有很多頁面和操作都需要認證後才能訪問,而登錄就是認證最關鍵的步驟。我們想在一個頁面上暢通無阻,那大部分都是要用戶登錄的。登錄是很多爬蟲程序要解決的第一個問題,也有很多時候也是整個爬蟲中最複雜最難的部分,只有搞定登錄,我們才能用程序。


上面列舉登錄的兩個情況,第一種情況一般很少出現但是我們學生階段寫的登錄就是這樣實現登錄的,明文不加密,但是這種情況不太安全所以大部分登錄或者請求會對一些參數進行一些加密,我們如果要用程序模擬這個登錄就需要把各個參數形成過程搞懂模擬生成發送才行。當然,登錄其實最棘手的驗證碼問題由於能力有限沒研究過這裡不做講解。大部分網站在錯誤不高情況是沒有驗證碼的,所以大部分場景還是可以嘗試搞一搞的。

 

校園網需要通過http成功登錄後可以才訪問互聯網,這個登錄參數密碼是加密的,下面就根據我自己所在環境小分析分享給大家。


分析

前面介紹這麼多,咱們直奔主題,開始解析這個問題。

 

對於校園網,我們連接上它的wifi或者網線,我們處在這個校園網的局域網之中,而網絡流量需要成本的,當你訪問外界網絡時候如果沒有獲得認證授權那麼你是無法訪問外界服務的,只有成功登錄校園網平台才能訪問互聯網。

 

然而,現在登錄的方式五花八門,我們第一步要觀察登錄的情況,大致我分成兩種,一個是普通表單登錄,還有的就是Ajax動態登錄,

 

怎麼區分兩者呢?

 

很簡單,登錄的時候看看url有沒有變化(酷酷的??)


你看,某校的校園網登錄頁面登錄之後它的url是不變的,所以說這個是Ajax登錄的情況。

 

兩者有區別嘛?區別不大的,但是Ajax一般情況可以不使用專業抓包工具,而有些form表單的登錄可能涉及到各種重定向、新頁面可能瀏覽器不太好抓對應信息,然後需要藉助一些fiddlerwireshark等工具抓包。

 

首先,我們要打開瀏覽器的F12,打開network這一項,然後點進去XHR這個小目錄,這裡面all的話獲取內容太多,而少部分數據可能藏在JavaScript(正常不會)。而doc一般就是主頁面了,如果普通form表單的話就要看doc請求了。


點擊登錄之後你就可以看到各個請求交互的內容了。你會發現在這個網頁上有個loginlogin上面有個getchallenge,首先點開login查看攜帶的參數。


可以看到這個請求的參數有三個,分別是用戶名,密碼,和一個不知道的challenge,但是上面有個getchallenge請求,然後一看一下果然有一challenge這個參數,當然如果有其他參數,它可能直接存在頁面中,也可能通過加密動態生成,就要自己分析啦,從上面圖中可以發現,其實我們就只需要破譯這個密碼的加密方式就得啦(對數據敏感的人可能都已經猜到它是什麼加密了)

 

既然知道需要解決哪一個參數,那麼一般來說可以從兩個方面入手,第一個就是利用瀏覽器元素定位到登錄那個按鈕,在全局搜索查看js中哪裡用到,可以debug其中的邏輯,但是很多這時這種方案看似從前到後實際上你很難發現一些有用內容,因為你不知道它的參數可能在你填寫完就加密好了,所以不推薦這種方式。


直接對著參數進行搜索,裡面有usernamepasswordchallenge這三個參數你可以直接搜,這裡面我就搜索password,看看到底哪裡用到了password,包括login等詞都可以搜搜。最終我在某個地方看到login的邏輯,這個password應該就是經過createChapPassword 方法實現加密。

 

我們在這裡打一個斷點,然後點一下登錄,程序成功到達斷點,並且此時我們的賬號密碼都還是明文 ,說明數據都還是未被加密的,從這裡就要開始捋一捋邏輯了。


進入查看一看函數,就發現核心內容就在這裡。

1.     var createChapPassword = function(password){ 

2.         var id = ''

3.         var challenge = ''

4.         var str = ''

5.      

6.         id = Math.round(Math.random()*10000)%256; 

7.      

8.         $.ajax({ 

9.             type : 'POST'

10.           url : globalVar.io_url + 'getchallenge'

11.           dataType : 'json'

12.           timeout : 5000, 

13.           cache : false

14.           async : false

15.           success : function(resp){ 

16.               if(resp && (resp.reply_code != null) && (resp.reply_code == 0))  challenge = resp.challenge; 

17.           } 

18.       }); 

19.    

20.       str += String.fromCharCode(id); 

21.       str += password

22.    

23.       for(i=0;i<challenge.length;i+=2){ 

24.           var hex = challenge.substring(i,i+2); 

25.           var dec = parseInt(hex,16); 

26.           str += String.fromCharCode(dec); 

27.       } 

28.    

29.       var hash = $.md5(str); 

30.    

31.       chappassword = ((id<16) ? "0" : "") + id.toString(16) + hash; 

32.    

33.       return {password : chappassword , challenge : challenge}; 

34.   }; 

 

這裡面邏輯給大家解讀一下其中邏輯,不會不懂的利用搜索引擎搜索一下就好啦。

 

首先就是隨機數產生的一個id,在其他語言復現時候可以選擇一個固定的。

 

然後Ajax發請求獲取一個challenge參數,str先加上id對應Unicode的字符,然後依次加上challenge兩兩組成16進制數字對應Unicode的字符。

 

str進行一次MD5加密,然後拼湊一下返回結果就行啦。所以說,參數的加密邏輯就在這裡,我們只需要復現就行啦。

 

邏輯復現

然後事實是複現的邏輯沒那麼簡單。我在復現的時候老老實實前面都沒問題,和瀏覽器的內容進行比對,然而就是MD5Python中實現的時候結果和前端的MD5加密內容不一致。

 

這個問題真的是排查了很久,浪費了很多時間,過程也給大家分享一下。

 


 

怎麼個情況呢,正常的編程語言要對字符串先進行編碼,然後再進行MD5編碼,而常規編碼方式最熟知的就是utf-8,並且使用在線加密的網站結果都和Pyhton調庫加密結果相同。

 

然後我再嘗試控制台打印字符utf-8編碼的結果,用瀏覽器的console對我編碼後字符串進行加密,發現了震驚的一幕!這個結果竟然和控製到的結果一致(33c9那一串)

 

這就說明,JQuery這個MD5加密庫並沒有對字符進行utf-8編碼而是採取了其他方式,我們需要找到這個方式在編程語言中實現,經過好幾番嘗試、查找最終終找到一個編碼格式:

 

ISO-8859-1

 這個編碼還是很久前學習JavaWeb服務器文件下載出現中文名文件名稱異常,對文件重新編碼遇到過後面就很少接觸,用這個編碼替代之後,終於打印出我們想要的結果

 

1.     ª124412ðRkhìy’LŒÁZosõ 

2.     b'\xc2\xaa124412\xc3\xb0Rkh\xc3\xacy\xc2\x92L*\x08\xc2\x8c\xc3\x81Zos\xc3\xb5' 

3.     hash 297ad4844ee638891233c9ca65df4d9c 

4.     chappasword aa297ad4844ee638891233c9ca65df4d9c 

 

這就完全通了,將代碼封裝寫好嘗試一下,這裡我用Python實現,Java也可以都一樣的,用了requests模塊的session(這個模塊自動保持cookie),不過代碼用不了,只能和前面前端JavaScript邏輯對比一下。

1.     import requests 

2.     import hashlib 

3.     import urllib 

4.      

5.     from requests import sessions 

6.      

7.     # header 请求头,通过浏览器请求抓包查看请求所需要的头信息,其中包括返回数据类型、浏览器等信息 

8.     header={ 

9.          'user-agent':'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36'

10.        'x-requested-with':'XMLHttpRequest'

11.        'accept':'application/json, text/javascript, */*; q=0.01'

12.        'accept-encoding':'gzip, deflate, br'

13.        'accept-language':'zh-CN,zh;q=0.9'

14.        'connection''keep-alive'

15.        'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8' 

16.        ,'Host''m.njust.edu.cn' 

17.    } 

18.   #数据(携带这部分数据传到后台 账号密码等我们访问接口需要携带的参数,其中需要我们变换的就是namepassword,讲输入的账号和密码赋值进去 

19.   data={ 

20.       'username':''

21.       'password':''

22.       'challenge':'' 

23.  

24.   def get_challenge(): 

25.       url = 'http://m.njust.edu.cn/portal/index.html' 

26.       req = session.get(url) 

27.       #print(req.text) 

28.       req2 = session.post("http://m.njust.edu.cn/portal_io/getchallenge"

29.       challenge = req2.json()['challenge'

30.       return challenge 

31.   def get_str2(): 

32.       str2 = chr(id) 

33.       str2 = str2 + password 

34.    

35.       for i in range(len(challenge)): 

36.           if i % 2 == 1: 

37.               continue 

38.           hex1 = challenge[i: i + 2] 

39.           dec = int(hex1, 16) 

40.           str2 = str2 + (chr(dec)) 

41.       return str2 

42.    

43.   def login(): 

44.       loginurl='http://m.njust.edu.cn/portal_io/login' 

45.       req3=session.post(loginurl,data=data,headers=header) 

46.       print(req3.text) 

47.    

48.   if __name__ == '__main__'

49.       # 第一次登录获取cookie 

50.       id = 162 

51.       session = requests.session() 

52.       challenge = get_challenge() 

53.       username = '12010xxxxxx49' 

54.       password = "12xxxx2" 

55.       str2 = get_str2() 

56.    

57.       hash = hashlib.md5(str2.encode('ISO-8859-1')).hexdigest() 

58.       # 打印加密后的密码  #测试结果,是md5 32位加密 

59.       print('hash',hash) 

60.    

61.       chappassword = hex(int(id))[2:] + hash  ##前面的0X去掉 

62.       print('chappasword', chappassword) 

63.    

64.       data['username'] = username 

65.       data['password'] = chappassword 

66.       data['challenge'] = challenge 

67.       login() 

 

準備發射,本來是沒網絡的登錄一下,網絡就來了,看來我們的結果是成功的。


                                                                                發射成功

 總結

這個問題對於老手來說並不復雜,但是對於不少人來說可能是個新奇有意思的事情,當然近年來爬蟲這種東西自己小玩玩還好,設計商業或者隱私數據大規模抓取可能會有危險哦,後面有機會在分享一些傳統登錄方式的頁面。

 

這個小加密分析簡單複現卻因為編碼問題卡了很久,說到底還是基礎比較薄弱,對這些加密算法和偏底層的基礎東西掌握不牢浪費了很多時間,像很多大佬可能看到一個串他可能就猜到這可能是什麼加密,這種數據格式是那種類型的編碼……不過還好也通過這個demo要補足一下這個盲點。