聲明
本文章中所有內(nèi)容僅供學(xué)習(xí)交流,抓包內(nèi)容琐凭、敏感網(wǎng)址统屈、數(shù)據(jù)接口均已做脫敏處理,嚴(yán)禁用于商業(yè)用途和非法用途吨掌,否則由此產(chǎn)生的一切后果均與作者無(wú)關(guān)窿侈,若有侵權(quán),請(qǐng)聯(lián)系我立即刪除肛著!
逆向目標(biāo)
本次的逆向目標(biāo)是WB的登錄殉农,雖然登錄的加密參數(shù)沒(méi)有太多超凳,但是登錄的流程稍微復(fù)雜一點(diǎn),經(jīng)歷了很多次中轉(zhuǎn)危队,細(xì)分下來(lái)大約要經(jīng)過(guò)九次處理才能成功登錄聪建。
在登錄過(guò)程中遇到的加密參數(shù)只有一個(gè)钙畔,即密碼加密茫陆,加密后的密碼在獲取 token 的時(shí)候會(huì)用到,獲取 token 是一個(gè) POST 請(qǐng)求擎析,其 Form Data 里的 sp
值就是加密后的密碼簿盅,類(lèi)似于:e23c5d62dbf9f8364005f331e487873c70d7ab0e8dd2057c3e66d1ae5d2837ef1dcf86......
登錄流程
首先來(lái)理清一下登錄流程,每一步特殊的參數(shù)進(jìn)都行了說(shuō)明,沒(méi)有提及的參數(shù)表示是定值,直接復(fù)制即可。
大致流程如下:
預(yù)登陸
獲取加密密碼
獲取 token
獲取加密后的賬號(hào)
發(fā)送驗(yàn)證碼
校驗(yàn)驗(yàn)證碼
訪(fǎng)問(wèn) redirect url
訪(fǎng)問(wèn) crossdomain2 url
通過(guò) passport url 登錄
1.預(yù)登陸
預(yù)登陸為 GET 請(qǐng)求粉铐,Query String Parameters 中主要包含兩個(gè)比較重要的參數(shù):su
:用戶(hù)名經(jīng)過(guò) base64 編碼得到倡缠,_
: 13 位時(shí)間戳盾饮,返回的數(shù)據(jù)包含一個(gè) JSON徘钥,可用正則提取出來(lái)而钞,JSON 里面包含 retcode
,servertime
,pcid
果元,nonce
倡怎,pubkey
,rsakv
, exectime
七個(gè)參數(shù)值吼句,其中大多數(shù)值都是后面的請(qǐng)求當(dāng)中要用到的尔艇,部分值是加密密碼要用到的,返回?cái)?shù)據(jù)數(shù)示例:
xxxxSSOController.preloginCallBack({
"retcode": 0,
"servertime": 1627461942,
"pcid": "gz-1cd535198c0efe850b96944c7945e8fd514b",
"nonce": "GWBOCL",
"pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245......",
"rsakv": 1330428213,
"exectime": 16
})
2.獲取加密后的密碼
密碼的加密使用的是 RSA 加密恨憎,可以通過(guò) Python 或者 JS 來(lái)獲取加密后的密碼今瀑,JS 加密的逆向在后面拿出來(lái)單獨(dú)分析。
3.獲取 token
這個(gè) token 值在后面的獲取加密手機(jī)號(hào)影暴、發(fā)送驗(yàn)證碼魂拦、校驗(yàn)驗(yàn)證碼等步驟中都會(huì)用到安疗,獲取 token 值為 POST 請(qǐng)求,Query String Parameters 的值是固定的:client: ssologin.js(v1.4.19)
璧亚,F(xiàn)orm Data 的值相對(duì)來(lái)說(shuō)比較多,但是除了加密的密碼以外觉义,其他參數(shù)其實(shí)都是可以在第1步預(yù)登陸返回的數(shù)據(jù)里找到,主要的參數(shù)如下:
-
su
:用戶(hù)名經(jīng)過(guò) base64 加密得到 -
servertime
:通過(guò)第1步預(yù)登陸返回的 JSON 里面獲取 -
nonce
:通過(guò)第1步預(yù)登陸返回的 JSON 里面獲取 -
rsakv
:通過(guò)第1步預(yù)登陸返回的 JSON 里面獲取 -
sp
:加密后的密碼 -
prelt
:隨機(jī)值
返回?cái)?shù)據(jù)為 HTML 源碼海洼,可以從里面提取 token 值龙优,類(lèi)似于:2NGFhARzFAFAIp_QwX70Npj8gw4lgj7RbCnByb3RlY3Rpb24.
,如果返回的 token 不是這種陆淀,則說(shuō)明賬號(hào)或者密碼錯(cuò)誤。
4.獲取加密后的賬號(hào)
前面我們遇到的 su
是用戶(hù)名經(jīng)過(guò) base64 加密得到募闲,這里它對(duì)用戶(hù)名進(jìn)行了進(jìn)一步的加密處理,加密后的用戶(hù)名在發(fā)送驗(yàn)證碼和校驗(yàn)驗(yàn)證碼的時(shí)候會(huì)用到看铆,GET 請(qǐng)求,Query String Parameters 的參數(shù)也比較簡(jiǎn)單诫睬,token
就是第3步獲取的 token 值,callback_url
是網(wǎng)站的主頁(yè),返回?cái)?shù)據(jù)是 HTML 源碼,可以使用 xpath 語(yǔ)法://input[@name='encrypt_mobile']/@value
來(lái)提取加密后的賬號(hào),其值類(lèi)似于:f2de0b5e333a
,這里需要注意的是,即便是同一個(gè)賬號(hào),每次加密的結(jié)果也是不一樣的。
5.發(fā)送驗(yàn)證碼
發(fā)送驗(yàn)證碼是一個(gè) POST 請(qǐng)求张抄,其參數(shù)也比較簡(jiǎn)單,Query String Parameters 里的 token
是第3步獲取的 token,F(xiàn)orm Data 里的 encrypt_mobile
是第4步獲取的加密后的賬號(hào)样眠,返回的數(shù)據(jù)是驗(yàn)證碼發(fā)送的狀態(tài)甥桂,例如:{'retcode': 20000000, 'msg': 'succ', 'data': []}
。
6.校驗(yàn)驗(yàn)證碼
校驗(yàn)驗(yàn)證碼是一個(gè) POST 請(qǐng)求完沪,其參數(shù)也非常簡(jiǎn)單,Query String Parameters 里的 token
是第3步獲取的 token嵌戈,F(xiàn)orm Data 里的 encrypt_mobile
是第4步獲取的加密后的賬號(hào)覆积,code
是第5步收到的驗(yàn)證碼,返回?cái)?shù)據(jù)是一個(gè) JSON熟呛,retcode
和 msg
代表校驗(yàn)的狀態(tài)宽档,redirect url
是校驗(yàn)步驟完成后接著要訪(fǎng)問(wèn)的頁(yè)面,在下一步中要用到庵朝,返回的數(shù)據(jù)示例:
{
"retcode": 20000000,
"msg": "succ",
"data": {
"redirect_url": "https://login.xxxx.com.cn/sso/login.php?entry=xxxxx&returntype=META&crossdomain=1&cdult=3&alt=ALT-NTcxNjMyMTA2OA==-1630292617-yf-78B1DDE6833847576B0DC4B77A6C77C4-1&savestate=30&url=https://xxxxx.com"
}
}
7.訪(fǎng)問(wèn) redirect url
這一步的請(qǐng)求接口其實(shí)就是第6步返回的 redirect url吗冤,GET 請(qǐng)求,類(lèi)似于:https://login.xxxx.com.cn/sso/login.php?entry=xxxxx&returntype=META......
返回的數(shù)據(jù)是 HTML 源碼九府,我們要從中提取 crossdomain2 的 URL椎瘟,提取的結(jié)果類(lèi)似于:https://login.xxxx.com.cn/crossdomain2.php?action=login&entry=xxxxx......
,同樣的侄旬,這個(gè) URL 也是接下來(lái)需要訪(fǎng)問(wèn)的頁(yè)面肺蔚。
8.訪(fǎng)問(wèn) crossdomain2 url
這一步的請(qǐng)求接口就是第7步提取的 crossdomain2 url,GET 請(qǐng)求儡羔,類(lèi)似于:https://login.xxxx.com.cn/crossdomain2.php?action=login&entry=xxxxx......
返回的數(shù)據(jù)同樣是 HTML 源碼宣羊,我們要從中提取真正的登錄的 URL,提取的結(jié)果類(lèi)似于:https://passport.xxxxx.com/wbsso/login?ssosavestate=1661828618&url=https......
笔链,最后一步只需要訪(fǎng)問(wèn)這個(gè)真正的登錄 URL 就能實(shí)現(xiàn)登錄操作了段只。
9.通過(guò) passport url 登錄
這是最后一步,也是真正的登錄操作鉴扫,GET 請(qǐng)求赞枕,請(qǐng)求接口就是第8步提取的 passport url,類(lèi)似于:https://passport.xxxxx.com/wbsso/login?ssosavestate=1661828618&url=https......
返回的數(shù)據(jù)包含了登錄結(jié)果、用戶(hù) ID 和用戶(hù)名炕婶,類(lèi)似于:
({"result":true,"userinfo":{"uniqueid":"5712321368","displayname":"tomb"}});
自此姐赡,WB的完整登錄流程已完成,可以直接拿登錄成功后的 cookies 進(jìn)行其他操作了柠掂。
加密密碼逆向
在登錄流程中项滑,第2步是獲取加密后的密碼,在登錄的第3步獲取 token 里涯贞,請(qǐng)求的 Query String Parameters 包含了一個(gè)加密參數(shù) sp
枪狂,這個(gè)就是加密后的密碼,接下來(lái)我們對(duì)密碼的加密進(jìn)行逆向分析宋渔。
直接全局搜索 sp
關(guān)鍵字州疾,發(fā)現(xiàn)有很多值,這里我們又用到了前面講過(guò)的技巧皇拣,嘗試搜索 sp=
严蓖、sp:
或者 var sp
等來(lái)縮小范圍,在本案例中氧急,我們嘗試搜索 sp=
颗胡,可以看到在 index.js 里面只有一個(gè)值,埋下斷點(diǎn)進(jìn)行調(diào)試吩坝,可以看到 sp
其實(shí)就是 b
的值:
PS:搜索時(shí)要注意毒姨,不能在登錄成功后的頁(yè)面進(jìn)行搜索,此時(shí)資源已刷新钾恢,重新加載了手素,加密的 JS 文件已經(jīng)沒(méi)有了,需要在登錄界面輸入錯(cuò)誤的賬號(hào)密碼來(lái)抓包瘩蚪、搜索泉懦、斷點(diǎn)。
繼續(xù)往上追蹤這個(gè) b
的值疹瘦,關(guān)鍵代碼有個(gè) if-else 語(yǔ)句崩哩,分別埋下斷點(diǎn),經(jīng)過(guò)調(diào)試可以看到 b
的值在 if 下面生成:
分析一下兩行關(guān)鍵代碼:
f.setPublic(me.rsaPubkey, "10001");
b = f.encrypt([me.servertime, me.nonce].join("\t") + "\n" + b)
me.rsaPubkey
言沐、me.servertime
邓嘹、me.nonce
都是第1步預(yù)登陸返回的數(shù)據(jù)。
把鼠標(biāo)移到 f.setPublic
和 f.encrypt
险胰,可以看到分別是 br
和 bt
函數(shù):
分別跟進(jìn)這兩個(gè)函數(shù)汹押,可以看到都在一個(gè)匿名函數(shù)下面:
直接將整個(gè)匿名函數(shù)復(fù)制下來(lái),去掉最外面的匿名函數(shù)起便,進(jìn)行本地調(diào)試棚贾,調(diào)試過(guò)程中會(huì)提示 navigator
未定義窖维,查看復(fù)制的源碼,里面用到了 navigator.appName
和 navigator.appVersion
妙痹,直接定義即可铸史,或者置空都行。
navigator = {
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
繼續(xù)調(diào)試會(huì)發(fā)現(xiàn)在 var c = this.doPublic(b);
提示對(duì)象不支持此屬性或方法怯伊,搜索 doPublic
發(fā)現(xiàn)有一句 bq.prototype.doPublic = bs;
琳轿,這里直接將其改為 doPublic = bs;
即可。
分析整個(gè) RSA 加密邏輯耿芹,其實(shí)也可以通過(guò) Python 來(lái)實(shí)現(xiàn)崭篡,代碼示例(pubkey 需要補(bǔ)全):
import rsa
import binascii
pre_parameter = {
"retcode": 0,
"servertime": 1627461942,
"pcid": "gz-1cd535198c0efe850b96944c7945e8fd514b",
"nonce": "GWBOCL",
"pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245......",
"rsakv": 1330428213,
"exectime": 16
}
password = '12345678'
public_key = rsa.PublicKey(int(pre_parameter['pubkey'], 16), int('10001', 16))
text = '%s\t%s\n%s' % (pre_parameter['servertime'], pre_parameter['nonce'], password)
encrypted_str = rsa.encrypt(text.encode(), public_key)
encrypted_password = binascii.b2a_hex(encrypted_str).decode()
print(encrypted_password)
完整代碼
GitHub 關(guān)注 K 哥爬蟲(chóng),持續(xù)分享爬蟲(chóng)相關(guān)代碼猩系!歡迎 star 媚送!https://github.com/kgepachong/
以下只演示部分關(guān)鍵代碼,不能直接運(yùn)行寇甸!完整代碼倉(cāng)庫(kù)地址:https://github.com/kgepachong/crawler/
關(guān)鍵 JS 加密代碼架構(gòu)
navigator = {
appName: "Netscape",
appVersion: "5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
}
function bt(a) {}
function bs(a) {}
function br(a, b) {}
// 此處省略 N 個(gè)函數(shù)
bl.prototype.nextBytes = bk;
doPublic = bs;
bq.prototype.setPublic = br;
bq.prototype.encrypt = bt;
this.RSAKey = bq
function getEncryptedPassword(me, b) {
br(me.pubkey, "10001");
b = bt([me.servertime, me.nonce].join("\t") + "\n" + b);
return b
}
// 測(cè)試樣例
// var me = {
// "retcode": 0,
// "servertime": 1627283238,
// "pcid": "gz-a9243276722ed6d4671f21310e2665c92ba4",
// "nonce": "N0Y3SZ",
// "pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245A87AC253062882729293E5506350508E7F9AA3BB77F4333231490F915F6D63C55FE2F08A49B353F444AD3993CACC02DB784ABBB8E42A9B1BBFFFB38BE18D78E87A0E41B9B8F73A928EE0CCEE1F6739884B9777E4FE9E88A1BBE495927AC4A799B3181D6442443",
// "rsakv": "1330428213",
// "exectime": 13
// }
// var b = '12312312312' // 密碼
// console.log(getEncryptedPassword(me, b))
Python 登錄關(guān)鍵代碼
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import re
import json
import time
import base64
import binascii
import rsa
import execjs
import requests
from lxml import etree
# 判斷某些請(qǐng)求是否成功的標(biāo)志
response_success_str = 'succ'
pre_login_url = '脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
get_token_url = '脫敏處理疗涉,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
protection_url = '脫敏處理拿霉,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
send_code_url = '脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
confirm_url = '脫敏處理咱扣,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
headers = {
'Host': '脫敏處理绽淘,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'Referer': '脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'sec-ch-ua': '" Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"',
'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_pre_parameter(username: str) -> dict:
su = base64.b64encode(username.encode())
time_now = str(int(time.time() * 1000))
params = {
'entry': '脫敏處理闹伪,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'callback': '脫敏處理沪铭,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'su': su,
'rsakt': 'mod',
'checkpin': 1,
'client': 'ssologin.js(v1.4.19)',
'_': time_now,
}
response = session.get(url=pre_login_url, params=params, headers=headers).text
parameter_dict = json.loads(re.findall(r'\((.*)\)', response)[0])
# print('1.【pre parameter】: %s' % parameter_dict)
return parameter_dict
def get_encrypted_password(pre_parameter: dict, password: str) -> str:
# 通過(guò) JS 獲取加密后的密碼
# with open('encrypt.js', 'r', encoding='utf-8') as f:
# js = f.read()
# encrypted_password = execjs.compile(js).call('getEncryptedPassword', pre_parameter, password)
# # print('2.【encrypted password】: %s' % encrypted_password)
# return encrypted_password
# 通過(guò) Python 的 rsa 模塊和 binascii 模塊獲取加密后的密碼
public_key = rsa.PublicKey(int(pre_parameter['pubkey'], 16), int('10001', 16))
text = '%s\t%s\n%s' % (pre_parameter['servertime'], pre_parameter['nonce'], password)
encrypted_str = rsa.encrypt(text.encode(), public_key)
encrypted_password = binascii.b2a_hex(encrypted_str).decode()
# print('2.【encrypted password】: %s' % encrypted_password)
return encrypted_password
def get_token(encrypted_password: str, pre_parameter: dict, username: str) -> str:
su = base64.b64encode(username.encode())
data = {
'entry': '脫敏處理,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'gateway': 1,
'from': '',
'savestate': 7,
'qrcode_flag': False,
'useticket': 1,
'pagerefer': '',
'vsnf': 1,
'su': su,
'service': 'miniblog',
'servertime': pre_parameter['servertime'],
'nonce': pre_parameter['nonce'],
'pwencode': 'rsa2',
'rsakv': pre_parameter['rsakv'],
'sp': encrypted_password,
'sr': '1920*1080',
'encoding': 'UTF-8',
'prelt': 38,
'url': '脫敏處理偏瓤,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler',
'returntype': 'META'
}
response = session.post(url=get_token_url, headers=headers, data=data)
# response.encoding = 'gbk'
ajax_login_url = re.findall(r'replace\("(.*)"\)', response.text)[0]
token = ajax_login_url.split('token%3D')[-1]
if 'weibo' not in token:
# print('3.【token】: %s' % token)
return token
else:
raise Exception('登錄失敗! 用戶(hù)名或者密碼錯(cuò)誤!')
def get_encrypted_mobile(token: str) -> str:
params = {
'token': token,
'callback_url': '脫敏處理杀怠,完整代碼關(guān)注 GitHub:https://github.com/kgepachong/crawler'
}
response = session.get(url=protection_url, params=params, headers=headers)
tree = etree.HTML(response.text)
encrypted_mobile = tree.xpath("http://input[@name='encrypt_mobile']/@value")[0]
# print('4.【encrypted mobile】: %s' % encrypted_mobile)
return encrypted_mobile
def send_code(token: str, encrypt_mobile: str) -> str:
params = {'token': token}
data = {'encrypt_mobile': encrypt_mobile}
response = session.post(url=send_code_url, params=params, data=data, headers=headers).json()
if response['msg'] == response_success_str:
code = input('請(qǐng)輸入驗(yàn)證碼: ')
# print('5.【code】: %s' % code)
return code
else:
# print('5.【failed to send verification code】: %s' % response)
raise Exception('驗(yàn)證碼發(fā)送失敗: %s' % response)
def confirm_code(encrypted_mobile: str, code: str, token: str) -> str:
params = {'token': token}
data = {
'encrypt_mobile': encrypted_mobile,
'code': code
}
response = session.post(url=confirm_url, params=params, data=data, headers=headers).json()
if response['msg'] == response_success_str:
redirect_url = response['data']['redirect_url']
# print('6.【redirect url】: %s' % redirect_url)
return redirect_url
else:
# print('6.【驗(yàn)證碼校驗(yàn)失敗】: %s' % response)
raise Exception('驗(yàn)證碼校驗(yàn)失敗: %s' % response)
def get_cross_domain2_url(redirect_url: str) -> str:
response = session.get(url=redirect_url, headers=headers).text
cross_domain2_url = re.findall(r'replace\("(.*)"\)', response)[0]
# print('7.【cross domain2 url】: %s' % cross_domain2_url)
return cross_domain2_url
def get_passport_url(cross_domain2_url: str) -> str:
response = session.get(url=cross_domain2_url, headers=headers).text
passport_url_str = re.findall(r'setCrossDomainUrlList\((.*)\)', response)[0]
passport_url = json.loads(passport_url_str)['arrURL'][0]
# print('8.【passport url】: %s' % passport_url)
return passport_url
def login(passport_url: str) -> None:
response = session.get(url=passport_url, headers=headers).text
login_result = json.loads(response.replace('(', '').replace(');', ''))
if login_result['result']:
user_unique_id = login_result['userinfo']['uniqueid']
user_display_name = login_result['userinfo']['displayname']
print('登錄成功!用戶(hù) ID:%s厅克,用戶(hù)名:%s' % (user_unique_id, user_display_name))
else:
raise Exception('登錄失斉馔恕:%s' % login_result)
def main():
username = input('請(qǐng)輸入登錄賬號(hào): ')
password = input('請(qǐng)輸入登錄密碼: ')
# 1.預(yù)登陸,獲取一個(gè)字典參數(shù)证舟,包含后面要用的 servertime硕旗、nonce、pubkey女责、rsakv
pre_parameter = get_pre_parameter(username)
# 2.通過(guò) JS 或者 Python 獲取加密后的密碼
encrypted_password = get_encrypted_password(pre_parameter, password)
# 3.獲取 token
token = get_token(encrypted_password, pre_parameter, username)
# 4.通過(guò) protection url 獲取加密后的手機(jī)號(hào)
encrypted_mobile = get_encrypted_mobile(token)
# 5.發(fā)送手機(jī)驗(yàn)證碼
code = send_code(token, encrypted_mobile)
# 6.校驗(yàn)驗(yàn)證碼漆枚,校驗(yàn)成功則返回一個(gè)重定向的 URL
redirect_url = confirm_code(encrypted_mobile, code, token)
# 7.訪(fǎng)問(wèn)重定向的 URL,提取 crossdomain2 URL
cross_domain2_url = get_cross_domain2_url(redirect_url)
# 8.訪(fǎng)問(wèn) crossdomain2 URL抵知,提取 passport URL
passport_url = get_passport_url(cross_domain2_url)
# 9.訪(fǎng)問(wèn) passport URL 進(jìn)行登錄操作
login(passport_url)
if __name__ == '__main__':
main()