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

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

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

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

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

逆向目标

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

在登录过程中遇到的加密参数只有一个,即密码加密,加密后的密码在获取 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
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • OpenClaw 开源 AI 助手安装与配置指南
  • Python 与 PyCharm 环境搭建完整指南
  • 国产 AI 编码工具深度测评:核心能力与局限分析
  • 基于大模型与知识库的 Code Review 自动化实践
  • Python 数据分析实战:基于 Pandas 的数据处理全流程指南
  • DeepSeek 与通义万相结合实现 AI 视频生成流程指南
  • 从三年前端到韩国 CS 硕士:留学复盘与回归前端的思考
  • OkHttp 源码分析:缓存策略实现
  • Spatial Joy 2025 AR&AI 开发大赛指南:奖金、赛道与报名要点
  • 斯坦福 2025 AI Index Report 深度解读:技术、产业与治理趋势
  • Stable Diffusion WebUI 目录结构与 Nova Anime XL 模型实战
  • 毕业两年转行软件测试:从物业维修到月薪 9K 的职场逆袭之路
  • SpringBoot 整合 Neo4j 图数据库实战
  • 基于STM32的智能家居安防系统设计与实现
  • 融合语言模型的多模态触觉传感器 SuperTac 助力机器人触觉感知
  • 算法:快慢指针判断快乐数
  • LlamaIndex 本地大模型起步教程
  • GitHub 项目上传实战指南
  • 基于 Docker 构建 Java 编译环境实战
  • python-can 模块使用记录

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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