跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Python大前端算法

微博登录流程逆向分析与加密参数实现

微博登录涉及九步流程,包括预登陆、获取 token、验证码校验等。核心难点在于密码的 RSA 加密逆向。通过分析浏览器 JS 源码定位加密函数,结合公钥与时间戳生成加密串。最终使用 Python 配合 rsa 库复现加密逻辑,完成自动化登录脚本编写。

怪力乱神发布于 2025/1/19更新于 2026/9/863 浏览
微博登录流程逆向分析与加密参数实现

微博登录流程逆向分析与加密参数实现

逆向目标

本次的逆向目标是微博的登录,虽然登录的加密参数没有太多,但是登录的流程稍微复杂一点,经历了很多次中转,细分下来大约要经过九次处理才能成功登录。

在登录过程中遇到的加密参数只有一个,即密码加密,加密后的密码在获取 token 的时候会用到,获取 token 是一个 POST 请求,其 Form Data 里的 sp 值就是加密后的密码,类似于:e23c5d62dbf9f8364005f331e487873c70d7ab0e8dd2057c3e66d1ae5d2837ef1dcf86......

登录流程

首先来理清一下登录流程,每一步特殊的参数进都行了说明,没有提及的参数表示是定值,直接复制即可。

大致流程如下:

  1. 预登陆
  2. 获取加密密码
  3. 获取 token
  4. 获取加密后的账号
  5. 发送验证码
  6. 校验验证码
  7. 访问 redirect url
  8. 访问 crossdomain2 url
  9. 通过 passport url 登录

1. 预登陆

预登陆为 GET 请求,Query String Parameters 中主要包含两个比较重要的参数:su:用户名经过 base64 编码得到,_:13 位时间戳,返回的数据包含一个 JSON,可用正则提取出来,JSON 里面包含 retcode,servertime,pcid,nonce,pubkey,rsakv,exectime 七个参数值,其中大多数值都是后面的请求当中要用到的,部分值是加密密码要用到的,返回数据数示例:

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

2. 获取加密后的密码

密码的加密使用的是 RSA 加密,可以通过 Python 或者 JS 来获取加密后的密码,JS 加密的逆向在后面拿出来单独分析。

3. 获取 token

这个 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. 获取加密后的账号

前面我们遇到的 su 是用户名经过 base64 加密得到,这里它对用户名进行了进一步的加密处理,加密后的用户名在发送验证码和校验验证码的时候会用到,GET 请求,Query String Parameters 的参数也比较简单,token 就是第 3 步获取的 token 值,callback_url 是网站的主页,返回数据是 HTML 源码,可以使用 xpath 语法://input[@name='encrypt_mobile']/@value 来提取加密后的账号,其值类似于:f2de0b5e333a,这里需要注意的是,即便是同一个账号,每次加密的结果也是不一样的。

5. 发送验证码

发送验证码是一个 POST 请求,其参数也比较简单,Query String Parameters 里的 token 是第 3 步获取的 token,Form Data 里的 encrypt_mobile 是第 4 步获取的加密后的账号,返回的数据是验证码发送的状态,例如:{'retcode': 20000000, 'msg': 'succ', 'data': []}。

6. 校验验证码

校验验证码是一个 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

这一步的请求接口其实就是第 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

这一步的请求接口就是第 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 登录

这是最后一步,也是真正的登录操作,GET 请求,请求接口就是第 8 步提取的 passport url,类似于:https://passport.xxxxx.com/wbsso/login?ssosavestate=1661828618&url=https......

返回的数据包含了登录结果、用户 ID 和用户名,类似于:

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

自此,微博的完整登录流程已完成,可以直接拿登录成功后的 cookies 进行其他操作了。

加密密码逆向

在登录流程中,第 2 步是获取加密后的密码,在登录的第 3 步获取 token 里,请求的 Query String Parameters 包含了一个加密参数 sp,这个就是加密后的密码,接下来我们对密码的加密进行逆向分析。

直接全局搜索 sp 关键字,发现有很多值,这里我们又用到了前面讲过的技巧,尝试搜索 sp=、sp: 或者 var sp 等来缩小范围,在本案例中,我们尝试搜索 sp=,可以看到在 index.js 里面只有一个值,埋下断点进行调试,可以看到 sp 其实就是 b 的值:

PS:搜索时要注意,不能在登录成功后的页面进行搜索,此时资源已刷新,重新加载了,加密的 JS 文件已经没有了,需要在登录界面输入错误的账号密码来抓包、搜索、断点。

继续往上追踪这个 b 的值,关键代码有个 if-else 语句,分别埋下断点,经过调试可以看到 b 的值在 if 下面生成:

分析一下两行关键代码:

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 函数:

分别跟进这两个函数,可以看到都在一个匿名函数下面:

直接将整个匿名函数复制下来,去掉最外面的匿名函数,进行本地调试,调试过程中会提示 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 = '%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)

完整代码

以下只演示部分关键代码,不能直接运行!完整代码仓库地址请参考官方文档或社区资源。

关键 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'

# 脱敏处理,完整代码关注 GitHub
pre_login_url = '# 预登陆 URL'
get_token_url = '# 获取 Token URL'
protection_url = '# 保护 URL'
send_code_url = '# 发送验证码 URL'
confirm_url = '# 校验验证码 URL'

headers = {
    'Host': '# Host',
    'Referer': '# Referer',
    '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': '# Entry',
        'callback': '# Callback',
        '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': '# Entry',
        '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': '# URL',
        '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': '# Callback URL'
    }
    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()

目录

  1. 微博登录流程逆向分析与加密参数实现
  2. 逆向目标
  3. 登录流程
  4. 1. 预登陆
  5. 2. 获取加密后的密码
  6. 3. 获取 token
  7. 4. 获取加密后的账号
  8. 5. 发送验证码
  9. 6. 校验验证码
  10. 7. 访问 redirect url
  11. 8. 访问 crossdomain2 url
  12. 9. 通过 passport url 登录
  13. 加密密码逆向
  14. 完整代码
  15. 关键 JS 加密代码架构
  16. Python 登录关键代码
  17. -- coding: utf-8 --
  18. 脱敏处理,完整代码关注 GitHub

更多推荐文章

查看全部
  • 66 个 Java 常见代码示例:从基础语法到并发编程
  • LLaMA-Factory 本地部署与安装指南
  • NestJS 接口响应 Message 编写规范与 API 提示标准化
  • 3DMAX VR 渲染器局部渲染设置教程
  • baoyu-skills:Claude Code 高效内容生成技能集
  • 基于 Next.js 构建支持 TokenP 钱包登录的 DApp 前端实战
  • 人形机器人躯干系统设计:结构方案、驱动布局与刚度要求
  • 若依 (RuoYi) 低代码框架深度解析与选型指南
  • OpenClaw 多飞书机器人配置与绑定指南
  • 程序员 LLM 学习指南:从入门到进阶的技术路线
  • 秋叶绘世 Stable Diffusion 整合包与 ComfyUI 环境说明
  • 红队入门指南:核心技能与角色准备
  • Java 集合框架核心:Map 与 Set 深度解析
  • OpenMV人脸追踪:在单片机里跑Haar级联的取舍与积分图实战
  • 算法题解:740.删除并获得点数
  • GitHub Copilot 在 Web 开发中的 20 个高效用法
  • Envoy 架构与配置详解
  • 动态规划专题:01 背包问题与分割等和子集
  • Python Web 开发框架选型指南
  • MCP 协议详解:与 Function Call 的区别及使用方法

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online