聲明
本文章中所有內容僅供學習交流篡腌,抓包內容术吝、敏感網(wǎng)址、數(shù)據(jù)接口均已做脫敏處理罩引,嚴禁用于商業(yè)用途和非法用途各吨,否則由此產生的一切后果均與作者無關,若有侵權袁铐,請聯(lián)系我立即刪除揭蜒!
逆向目標
- 目標:X米賬號登錄
- 主頁:aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20v
- 接口:aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20vcGFzcy9zZXJ2aWNlTG9naW5BdXRoMg==
-
逆向參數(shù):Form Data:
hash: FCEA920F7412B5DA7BE0CF42B8C93759
逆向過程
抓包分析
來到X米的登錄頁面,隨便輸入一個賬號密碼登陸剔桨,抓包定位到登錄接口為 aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20vcGFzcy9zZXJ2aWNlTG9naW5BdXRoMg==
POST 請求屉更,F(xiàn)orm Data 里的參數(shù)比較多,分析一下主要參數(shù):
-
serviceParam:
{"checkSafePhone":false,"checkSafeAddress":false,"lsrp_score":0.0}
洒缀,從參數(shù)的字面意思來看瑰谜,似乎是在檢查手機和地址是否安全,至于具體是什么含義,暫時不得而知似舵,也不知道是在哪個地方設置的脚猾。 -
callback:
http://order.xxx.com/login/callback?followup=https%3A%2F%2Fwww.xx......
,回調鏈接砚哗,一般來說是固定的龙助,后面帶有 followup 和 sid 參數(shù)。 -
qs:
%3Fcallback%3Dhttp%253A%252F%252Forder.xxx.com%252Flogin%252Fcallback%2......
蛛芥,把 qs 的值格式化一下可以發(fā)現(xiàn)提鸟,其實是 callback、sign仅淑、sid称勋、_qrsize 四個值按照 URL 編碼進行組合得到的。 -
_sign:
w1RBM6cG8q2xj5JzBPPa65QKs9w=
涯竟,這個一串看起來是經過某種加密后得到的赡鲜,也有可能是網(wǎng)頁源碼中的值。 -
user:
15555555555
庐船,明文用戶名银酬。 -
hash:
FCEA920F7412B5DA7BE0CF42B8C93759
,加密后的密碼筐钟。
參數(shù)逆向
基本參數(shù)
先來看一下 serviceParam
等基本參數(shù)揩瞪,一般思路我們是先直接搜索一下看看能不能直接找到這個值,搜索發(fā)現(xiàn) serviceParam
關鍵字在一個 302 重定向請求里:
我們注意到篓冲,當只輸入登錄的主頁 aHR0cHM6Ly9hY2NvdW50LnhpYW9taS5jb20v李破,它會有兩次連續(xù)的 302 重定向,來重點分析一下這兩次重定向壹将。
第一次重定向嗤攻,新的網(wǎng)址里有 followup
、callback
瞭恰、sign
屯曹、sid
參數(shù),這些我們都是在后面的登錄請求中要用到的惊畏。
第二次重定向恶耽,新的網(wǎng)址里同樣有 followup
、callback
颜启、sign
偷俭、sid
參數(shù),此外還有 serviceParam
缰盏、qs
參數(shù)涌萤,同樣也是后面的登錄請求需要用到的淹遵。
找到了參數(shù)的來源,直接從第二次重定向的鏈接里提取各項參數(shù)负溪,這里用到了 response.history[1].headers['Location']
來提取頁面第二次重定向返回頭里的目標地址透揣,urllib.parse.urlparse
來解析重定向鏈接 URL 的結構,urllib.parse.parse_qs
提取參數(shù)川抡,返回字典辐真,代碼樣例:
import requests
import urllib.parse
headers = {
'Host': '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
index_url = '脫敏處理崖堤,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
response = requests.get(url=index_url, headers=headers)
location_url = response.history[1].headers['Location']
urlparse = urllib.parse.urlparse(location_url)
query_dict = urllib.parse.parse_qs(urlparse.query)
print(query_dict)
need_theme = query_dict['needTheme'][0]
show_active_x = query_dict['showActiveX'][0]
service_param = query_dict['serviceParam'][0]
callback = query_dict['callback'][0]
qs = query_dict['qs'][0]
sid = query_dict['sid'][0]
_sign = query_dict['_sign'][0]
print(need_theme, show_active_x, service_param, callback, qs, sid, _sign)
hash
其他參數(shù)都齊全了侍咱,現(xiàn)在還差一個加密后的密碼 hash,一般來講這種都是通過 JS 加密的密幔,老方法楔脯,全局搜索 hash
或者 hash:
,可以在 78.4da22c55.chunk.js 文件里面看到有一句:hash: S()(r.password).toUpperCase()
胯甩,很明顯是將明文的密碼經過加密處理后再全部轉為大寫:
重點是這個 S()昧廷,鼠標移上去會發(fā)現(xiàn)其實是調用了 78.4da22c55.chunk.js 的一個匿名函數(shù),我們在匿名函數(shù)的 return 位置埋下斷點進行調試:
e.exports = function(e, n) {
if (void 0 === e || null === e)
throw new Error("Illegal argument " + e);
var r = t.wordsToBytes(u(e, n));
return n && n.asBytes ? r : n && n.asString ? s.bytesToString(r) : t.bytesToHex(r)
}
可以看到傳進來的 e 是明文的密碼偎箫,最后的 return 語句是一個三目運算符麸粮,由于 n 是 undefined,所以最后 return 的實際上是 t.bytesToHex(r)
镜廉,其值正是加密后的密碼,只不過所有字母都是小寫愚战,按照正常思維娇唯,我們肯定是開始扣 JS 了,這里傳入了參數(shù) r寂玲,var r = t.wordsToBytes(u(e, n));
塔插,先跟進 u 這個函數(shù)看看:
可以看到 u 函數(shù)實際上是用到了 567 這個對象方法,在這個對象方法里面拓哟,還用到了 129想许、211、22 等非常多的方法断序,這要是挨個去扣流纹,那還不得扣到猴年馬月,而且還容易出錯违诗,代碼太多也不好定位錯誤的地方漱凝,所以這里需要轉變一下思路,先來看看 t.bytesToHex(r)
是個什么東東诸迟,跟進到這個函數(shù):
bytesToHex: function(e) {
for (var t = [], n = 0; n < e.length; n++)
t.push((e[n] >>> 4).toString(16)),
t.push((15 & e[n]).toString(16));
return t.join("")
}
解讀一下這段代碼茸炒,傳進來的 e 是一個 16 位的 Array 對象愕乎,定義了一個 t 空數(shù)組,經過一個循環(huán)壁公,依次取 Array 對象里的值感论,第一次經過無符號右移運算(>>>)后,轉為十六進制的字符串紊册,將結果添加到 t 數(shù)組的末尾比肄。第二次進行位運算(&)后,同樣轉為十六進制的字符串湿硝,將結果添加到 t 數(shù)組的末尾薪前。也就是說,原本傳進來的 16 位的 Array 對象关斜,每一個值都經過了兩次操作示括,那么最后結果的 t 數(shù)組中就會有 32 個值,最后再將 t 數(shù)組轉換成字符串返回痢畜。
結合一下調用的函數(shù)名稱垛膝,我們來捋一下整個流程,首先調用 wordsToBytes()
方法將明文密碼字符串轉為 byte 數(shù)組丁稀,無論密碼的長度如何吼拥,最后得到的 byte 數(shù)組都是 16 位的,然后調用 bytesToHex()
方法线衫,循環(huán)遍歷生成的 byte 類型數(shù)組凿可,讓其生成 32 位字符串。
無論密碼長度如何授账,最終得到的密文都是 32 位的枯跑,而且都由字母和數(shù)字組成,這些特點很容易讓人想到 MD5 加密白热,將明文轉換成 byte 數(shù)組后進行隨機哈希敛助,對 byte 數(shù)組進行摘要,得到摘要 byte 數(shù)組屋确,循環(huán)遍歷 byte 數(shù)組纳击,生成固定位數(shù)的字符串,這不就是 MD5 的加密過程么攻臀?
直接把密碼拿來進行 MD5 加密焕数,和網(wǎng)站的加密結果進行對比,可以發(fā)現(xiàn)確實是一樣的刨啸,驗證了我們的猜想是正確的:
既然如此百匆,直接可以使用 Python 的 hashlib 模塊來實現(xiàn)就 OK 了,根本不需要去死扣代碼呜投,代碼樣例:
import hashlib
password = "1234567"
encrypted_password = hashlib.md5(password.encode(encoding='utf-8')).hexdigest().upper()
print(encrypted_password)
# FCEA920F7412B5DA7BE0CF42B8C93759
總結
有的時候需要我們轉變思路加匈,不一定每次都要死扣 JS 代碼存璃,相對較容易的站點的加密方式無非就是那么幾種,有的是稍微進行了改寫雕拼,有的是把密鑰纵东、偏移量等參數(shù)隱藏了,有的是把加密解密過程給你混淆了啥寇,讓你難以理解偎球,如果你對常見的加密方式和原理比較熟悉的話,有時候只需要搞清楚他用的什么加密方式辑甜,或者拿到了密鑰衰絮、偏移量等關鍵參數(shù),就完全可以自己還原整個加密過程磷醋!
完整代碼
GitHub 關注 K 哥爬蟲猫牡,持續(xù)分享爬蟲相關代碼!歡迎 star 邓线!
https://github.com/kgepachong/
以下只演示部分關鍵代碼淌友,完整代碼倉庫地址:
https://github.com/kgepachong/crawler/
Python 登錄關鍵代碼
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import hashlib
import urllib.parse
import requests
index_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
login_url = '脫敏處理骇陈,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
headers = {
'Host': '脫敏處理震庭,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
'Origin': '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
'Referer': '脫敏處理你雌,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
session = requests.session()
def get_encrypted_password(password):
encrypted_password = hashlib.md5(password.encode(encoding='utf-8')).hexdigest().upper()
return encrypted_password
def get_parameter():
response = requests.get(url=index_url, headers=headers)
location_url = response.history[1].headers['Location']
urlparse = urllib.parse.urlparse(location_url)
query_dict = urllib.parse.parse_qs(urlparse.query)
# print(query_dict)
return query_dict
def login(username, encrypted_password, query_dict):
data = {
'bizDeviceType': '',
'needTheme': query_dict['needTheme'][0],
'theme': '',
'showActiveX': query_dict['showActiveX'][0],
'serviceParam': query_dict['serviceParam'][0],
'callback': query_dict['callback'][0],
'qs': query_dict['qs'][0],
'sid': query_dict['sid'][0],
'_sign': query_dict['_sign'][0],
'user': username,
'cc': '+86',
'hash': encrypted_password,
'_json': True
}
response = session.post(url=login_url, data=data, headers=headers)
response_json = json.loads(response.text.replace('&&&START&&&', ''))
print(response_json)
return response_json
def main():
username = input('請輸入登錄賬號: ')
password = input('請輸入登錄密碼: ')
encrypted_password = get_encrypted_password(password)
parameter = get_parameter()
login(username, encrypted_password, parameter)
if __name__ == '__main__':
main()