頁面重新載入JS方法,重新載入js文件

聲明

本文章中所有內容僅供學習交流,抓包內容、敏感網址、數據介面均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關,若有侵權,請聯繫我立即刪除!

逆向目標

本次的逆向目標是WB的登錄,雖然登錄的加密參數沒有太多,但是登錄的流程稍微複雜一點,經歷了很多次中轉,細分下來大約要經過九次處理才能成功登錄。

在登錄過程中遇到的加密參數只有一個,即密碼加密,加密後的密碼在獲取 token 的時候會用到,獲取 token 是一個 POST 請求,其 Form Data 里的 sp 值就是加密後的密碼,類似於:
e23c5d62dbf9f8364005f331e487873c70d7ab0e8dd2057c3e66d1ae5d2837ef1dcf86……

登錄流程

首先來理清一下登錄流程,每一步特殊的參數進都行了說明,沒有提及的參數表示是定值,直接複製即可。

大致流程如下:

  1. 預登陸
  2. 獲取加密密碼
  3. 獲取 token
  4. 獲取加密後的賬號
  5. 發送驗證碼
  6. 校驗驗證碼
  7. 訪問 redirect url
  8. 訪問 crossdomain2 url
  9. 通過 passport url 登錄

1.預登陸

「JS 逆向百例」複雜的登錄過程,最新WB逆向

預登陸為 GET 請求,Query String Parameters 中主要包含兩個比較重要的參數:su:用戶名經過 base64 編碼得到,_: 13 位時間戳,返回的數據包含一個 JSON,可用正則提取出來,JSON 裡面包含 retcode,servertime,pcid,nonce,pubkey,rsakv, exectime 七個參數值,其中大多數值都是後面的請求當中要用到的,部分值是加密密碼要用到的,返回數據數示例:

xxxxSSOController.preloginCallBack({
    "retcode": 0,
    "servertime": 1627461942,
    "pcid": "gz-1cd535198c0efe850b96944c7945e8fd514b",
    "nonce": "GWBOCL",
    "pubkey": "EB2A38568661887FA180BDDB5CABD5F21C7BFD59C090CB2D245......",
    "rsakv": 1330428213,
    "exectime": 16
})

2.獲取加密後的密碼

密碼的加密使用的是 RSA 加密,可以通過 Python 或者 JS 來獲取加密後的密碼,JS 加密的逆向在後面拿出來單獨分析。

3.獲取 token

「JS 逆向百例」複雜的登錄過程,最新WB逆向

這個 token 值在後面的獲取加密手機號、發送驗證碼、校驗驗證碼等步驟中都會用到,獲取 token 值為 POST 請求,Query String Parameters 的值是固定的:client: ssologin.js(v1.4.19),Form Data 的值相對來說比較多,但是除了加密的密碼以外,其他參數其實都是可以在第1步預登陸返回的數據里找到,主要的參數如下:

  • su:用戶名經過 base64 加密得到
  • servertime:通過第1步預登陸返回的 JSON 裡面獲取
  • nonce:通過第1步預登陸返回的 JSON 裡面獲取
  • rsakv:通過第1步預登陸返回的 JSON 裡面獲取
  • sp:加密後的密碼
  • prelt:隨機值

返回數據為 HTML 源碼,可以從裡面提取 token 值,類似於:
2NGFhARzFAFAIp_QwX70Npj8gw4lgj7RbCnByb3RlY3Rpb24.,如果返回的 token 不是這種,則說明賬號或者密碼錯誤。

4.獲取加密後的賬號

「JS 逆向百例」複雜的登錄過程,最新WB逆向

前面我們遇到的 su 是用戶名經過 base64 加密得到,這裡它對用戶名進行了進一步的加密處理,加密後的用戶名在發送驗證碼和校驗驗證碼的時候會用到,GET 請求,Query String Parameters 的參數也比較簡單,token 就是第3步獲取的 token 值,callback_url 是網站的主頁,返回數據是 HTML 源碼,可以使用 xpath 語法://input[@name=’encrypt_mobile’]/@value 來提取加密後的賬號,其值類似於:f2de0b5e333a,這裡需要注意的是,即便是同一個賬號,每次加密的結果也是不一樣的。

5.發送驗證碼

「JS 逆向百例」複雜的登錄過程,最新WB逆向

發送驗證碼是一個 POST 請求,其參數也比較簡單,Query String Parameters 里的 token 是第3步獲取的 token,Form Data 里的 encrypt_mobile 是第4步獲取的加密後的賬號,返回的數據是驗證碼發送的狀態,例如:{‘retcode’: 20000000, ‘msg’: ‘succ’, ‘data’: []}。

6.校驗驗證碼

「JS 逆向百例」複雜的登錄過程,最新WB逆向

校驗驗證碼是一個 POST 請求,其參數也非常簡單,Query String Parameters 里的 token 是第3步獲取的 token,Form Data 里的 encrypt_mobile 是第4步獲取的加密後的賬號,code 是第5步收到的驗證碼,返回數據是一個 JSON,retcode 和 msg 代表校驗的狀態,redirect url 是校驗步驟完成後接著要訪問的頁面,在下一步中要用到,返回的數據示例:

{
  "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.訪問 redirect url

「JS 逆向百例」複雜的登錄過程,最新WB逆向

這一步的請求介面其實就是第6步返回的 redirect url,GET 請求,類似於:
https://login.xxxx.com.cn/sso/login.php?entry=xxxxx&returntype=META……

返回的數據是 HTML 源碼,我們要從中提取 crossdomain2 的 URL,提取的結果類似於:
https://login.xxxx.com.cn/crossdomain2.php?action=login&entry=xxxxx……,同樣的,這個 URL 也是接下來需要訪問的頁面。

8.訪問 crossdomain2 url

「JS 逆向百例」複雜的登錄過程,最新WB逆向

這一步的請求介面就是第7步提取的 crossdomain2 url,GET 請求,類似於:
https://login.xxxx.com.cn/crossdomain2.php?action=login&entry=xxxxx……

返回的數據同樣是 HTML 源碼,我們要從中提取真正的登錄的 URL,提取的結果類似於:
https://passport.xxxxx.com/wbsso/login?ssosavestate=1661828618&url=https……,最後一步只需要訪問這個真正的登錄 URL 就能實現登錄操作了。

9.通過 passport url 登錄

「JS 逆向百例」複雜的登錄過程,最新WB逆向

這是最後一步,也是真正的登錄操作,GET 請求,請求介面就是第8步提取的 passport url,類似於:
https://passport.xxxxx.com/wbsso/login?ssosavestate=1661828618&url=https……

返回的數據包含了登錄結果、用戶 ID 和用戶名,類似於:

({"result":true,"userinfo":{"uniqueid":"5712321368","displayname":"tomb"}});

自此,WB的完整登錄流程已完成,可以直接拿登錄成功後的 cookies 進行其他操作了。

加密密碼逆向

在登錄流程中,第2步是獲取加密後的密碼,在登錄的第3步獲取 token 里,請求的 Query String Parameters 包含了一個加密參數 sp,這個就是加密後的密碼,接下來我們對密碼的加密進行逆向分析。

直接全局搜索 sp 關鍵字,發現有很多值,這裡我們又用到了前面講過的技巧,嘗試搜索 sp=、sp: 或者 var sp 等來縮小範圍,在本案例中,我們嘗試搜索 sp=,可以看到在 index.js 裡面只有一個值,埋下斷點進行調試,可以看到 sp 其實就是 b 的值:

PS:搜索時要注意,不能在登錄成功後的頁面進行搜索,此時資源已刷新,重新載入了,加密的 JS 文件已經沒有了,需要在登錄界面輸入錯誤的賬號密碼來抓包、搜索、斷點。

「JS 逆向百例」複雜的登錄過程,最新WB逆向

繼續往上追蹤這個 b 的值,關鍵代碼有個 if-else 語句,分別埋下斷點,經過調試可以看到 b 的值在 if 下面生成:

「JS 逆向百例」複雜的登錄過程,最新WB逆向

分析一下兩行關鍵代碼:

f.setPublic(me.rsaPubkey, "10001");
b = f.encrypt([me.servertime, me.nonce].join("t") + "n" + b)

me.rsaPubkey、me.servertime、me.nonce 都是第1步預登陸返回的數據。

把滑鼠移到 f.setPublic 和 f.encrypt,可以看到分別是 br 和 bt 函數:

「JS 逆向百例」複雜的登錄過程,最新WB逆向
「JS 逆向百例」複雜的登錄過程,最新WB逆向

分別跟進這兩個函數,可以看到都在一個匿名函數下面:

「JS 逆向百例」複雜的登錄過程,最新WB逆向

直接將整個匿名函數複製下來,去掉最外面的匿名函數,進行本地調試,調試過程中會提示 navigator 未定義,查看複製的源碼,裡面用到了 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"
}

繼續調試會發現在 var c = this.doPublic(b); 提示對象不支持此屬性或方法,搜索 doPublic 發現有一句 bq.prototype.doPublic = bs;,這裡直接將其改為 doPublic = bs; 即可。

分析整個 RSA 加密邏輯,其實也可以通過 Python 來實現,代碼示例(pubkey 需要補全):

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 = '%st%sn%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 關注 K 哥爬蟲,持續分享爬蟲相關代碼!歡迎 star !
https://github.com/kgepachong/

**以下只演示部分關鍵代碼,不能直接運行!**完整代碼倉庫地址:
https://github.com/kgepachong/crawler/

關鍵 JS 加密代碼架構

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 個函數

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
}

// 測試樣例
// 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 登錄關鍵代碼

#!/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


# 判斷某些請求是否成功的標誌
response_success_str = 'succ'

pre_login_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
get_token_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
protection_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
send_code_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'
confirm_url = '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler'

headers = {
    'Host': '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
    'Referer': '脫敏處理,完整代碼關注 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': '脫敏處理,完整代碼關注 GitHub:https://github.com/kgepachong/crawler',
        'callback': '脫敏處理,完整代碼關注 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:
    # 通過 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

    # 通過 Python 的 rsa 模塊和 binascii 模塊獲取加密後的密碼
    public_key = rsa.PublicKey(int(pre_parameter['pubkey'], 16), int('10001', 16))
    text = '%st%sn%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': '脫敏處理,完整代碼關注 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': '脫敏處理,完整代碼關注 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('登錄失敗! 用戶名或者密碼錯誤!')


def get_encrypted_mobile(token: str) -> str:
    params = {
        'token': token,
        'callback_url': '脫敏處理,完整代碼關注 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("//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('請輸入驗證碼: ')
        # print('5.【code】: %s' % code)
        return code
    else:
        # print('5.【failed to send verification code】: %s' % response)
        raise Exception('驗證碼發送失敗: %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.【驗證碼校驗失敗】: %s' % response)
        raise Exception('驗證碼校驗失敗: %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('登錄成功!用戶 ID:%s,用戶名:%s' % (user_unique_id, user_display_name))
    else:
        raise Exception('登錄失敗:%s' % login_result)


def main():
    username = input('請輸入登錄賬號: ')
    password = input('請輸入登錄密碼: ')

    # 1.預登陸,獲取一個字典參數,包含後面要用的 servertime、nonce、pubkey、rsakv
    pre_parameter = get_pre_parameter(username)

    # 2.通過 JS 或者 Python 獲取加密後的密碼
    encrypted_password = get_encrypted_password(pre_parameter, password)

    # 3.獲取 token
    token = get_token(encrypted_password, pre_parameter, username)

    # 4.通過 protection url 獲取加密後的手機號
    encrypted_mobile = get_encrypted_mobile(token)

    # 5.發送手機驗證碼
    code = send_code(token, encrypted_mobile)

    # 6.校驗驗證碼,校驗成功則返回一個重定向的 URL
    redirect_url = confirm_code(encrypted_mobile, code, token)

    # 7.訪問重定向的 URL,提取 crossdomain2 URL
    cross_domain2_url = get_cross_domain2_url(redirect_url)

    # 8.訪問 crossdomain2 URL,提取 passport URL
    passport_url = get_passport_url(cross_domain2_url)

    # 9.訪問 passport URL 進行登錄操作
    login(passport_url)


if __name__ == '__main__':
    main()

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/229527.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-10 12:30
下一篇 2024-12-10 12:30

相關推薦

發表回復

登錄後才能評論