【JS 逆向百例】复杂的登录过程,最新微博逆向


关注微信公众号:K哥爬虫,QQ交流群:808574309,持续分享爬虫进阶、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.预登陆

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()

  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值