【JS 逆向百例】復雜的登錄過程,最新WB逆向


聲明

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

逆向目標

本次的逆向目標是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.預登陸

01.png

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

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

2.獲取加密后的密碼

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

3.獲取 token

02.png

這個 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.獲取加密后的賬號

03.png

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

5.發送驗證碼

04.png

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

6.校驗驗證碼

05.png

校驗驗證碼是一個 POST 請求,其參數也非常簡單,Query String Parameters 里的 token 是第3步獲取的 token,Form Data 里的 encrypt_mobile 是第4步獲取的加密后的賬號,code 是第5步收到的驗證碼,返回數據是一個 JSON,retcodemsg 代表校驗的狀態,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

06.png

這一步的請求接口其實就是第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

07.png

這一步的請求接口就是第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 登錄

08.png

這是最后一步,也是真正的登錄操作,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 文件已經沒有了,需要在登錄界面輸入錯誤的賬號密碼來抓包、搜索、斷點。

09.png

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

10.png

分析一下兩行關鍵代碼:

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

me.rsaPubkeyme.servertimeme.nonce 都是第1步預登陸返回的數據。

把鼠標移到 f.setPublicf.encrypt,可以看到分別是 brbt 函數:

11.png

12.png

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

13.png

直接將整個匿名函數復制下來,去掉最外面的匿名函數,進行本地調試,調試過程中會提示 navigator 未定義,查看復制的源碼,里面用到了 navigator.appNamenavigator.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 = '%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 關注 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 = '%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': '脫敏處理,完整代碼關注 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()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM