淘宝登录RSA加密逆向:Python复现password2参数生成全流程
2026/7/21 6:46:20 网站建设 项目流程

1. 项目概述:从登录框到加密黑盒

做爬虫或者自动化测试的朋友,对淘宝登录这个“拦路虎”应该都不陌生。你兴冲冲地写好了脚本,模拟了点击,填好了用户名和密码,结果一提交,服务器告诉你密码错误。你明明输对了,为什么?问题就出在那个看似简单的密码输入框背后,藏着一套复杂的客户端加密逻辑。我们今天要啃的硬骨头,就是淘宝登录流程中那个关键的password2参数,它是由 RSA 算法加密生成的。

这不仅仅是一个“找到加密函数然后调用”的简单逆向。淘宝的登录页面经过多年迭代,其 JavaScript 代码经过了混淆、压缩和动态加载,password2的生成逻辑被巧妙地隐藏在一堆难以阅读的代码中。我们的目标,就是像侦探一样,从浏览器纷繁的网络请求和源码中,抽丝剥茧,定位到 RSA 公钥和加密的核心代码,并用 Python 完美复现这一过程。最终,你将得到一个可以脱离浏览器环境,直接生成有效登录凭证的 Python 脚本。这对于需要处理大量账号登录、进行数据采集或自动化测试的场景来说,价值不言而喻。

2. 核心思路与技术选型解析

2.1 逆向工程的核心路径

面对一个经过混淆的 Web 应用,盲目地通读所有 JS 代码是效率最低下的方法。我们的核心思路是“由外及内,顺藤摸瓜”。

2.1.1 网络请求分析是起点

一切始于浏览器的开发者工具(F12)。在淘宝登录页面输入密码点击登录,观察网络(Network)选项卡中发出的请求。你会发现一个关键的 POST 请求,其请求参数中就包含了我们的目标password2,它是一长串看似随机的字符。同时,你很可能还会发现一个publicKey或类似名称的字段,或者在一个更早的 GET 请求响应中,服务器返回了 RSA 公钥信息。这个公钥是加密的必需品,我们必须先拿到它。

2.1.2 搜索与断点追踪

有了目标参数名password2,我们可以在开发者工具的“源代码”(Sources)选项卡中,对所有 JS 文件进行全局搜索。混淆后的代码变量名可能千奇百怪,但字符串常量相对容易被保留。搜索”password2“’password2‘,往往能直接定位到设置这个参数的地方。在这里打下断点,重新触发登录操作,代码执行就会在此暂停。

此时,调用栈(Call Stack)窗口是你的最佳导航。它展示了当前断点函数是被谁调用的,一层层回溯,你就能找到最核心的加密函数入口。这个函数很可能接收明文密码和公钥作为参数,输出加密后的密文。

2.1.3 算法识别与提取

在核心函数内部,你会看到诸如new JSEncrypt()setPublicKeyencrypt等字样,这是常见的 JavaScript RSA 库(如 jsencrypt)的用法。我们的任务就是理清这里面的逻辑:公钥以何种格式传入(可能是模数n和指数e分开,也可能是一个 PEM 格式的字符串),明文在加密前是否经过了其他处理(比如 URL 编码、Base64 转换等)。然后,用 Python 的密码学库(如rsacryptography)实现完全相同的逻辑。

2.2 为什么选择 Python 进行复现?

你可能会有疑问:既然浏览器里 JS 都能跑,为什么还要用 Python 复现?

  1. 环境独立性与自动化:Python 脚本可以运行在服务器、无头浏览器或任何没有图形界面的环境中,便于集成到自动化流水线或爬虫框架中。
  2. 性能与资源:启动一个完整的浏览器实例(即使是无头的)消耗的资源远大于一个纯 Python 脚本。对于批量任务,性能差异巨大。
  3. 稳定与可控:浏览器环境可能因版本、插件或页面动态加载等因素产生波动。Python 复现的加密逻辑是确定性的,只要算法不变,输出就恒定,更稳定可靠。
  4. 学习价值:这个过程是对非对称加密算法、Web 逆向工程、跨语言编程理解的绝佳实践,远比单纯调用一个 Selenium 点击登录按钮要深入得多。

注意:本教程旨在技术交流与学习,帮助你理解现代 Web 应用的安全机制和逆向分析方法。请务必遵守目标网站的服务条款,不得将技术用于恶意爬取、攻击或侵犯他人隐私等非法用途。

3. 实战逆向:定位 password2 的生成逻辑

理论说再多,不如动手走一遍。我们假设一个典型的淘宝登录页面(实际域名和参数可能随时间变化,但方法论通用)。

3.1 第一步:捕获关键网络请求

  1. 打开浏览器开发者工具(F12),切换到Network选项卡,并勾选Preserve log(保留日志)。
  2. 访问淘宝登录页,输入账号和(错误的)密码,点击登录。
  3. 在网络请求列表中,筛选XHRFetch请求,寻找登录提交的请求。通常其 URL 会包含loginsubmit等关键字。
  4. 点击这个请求,查看其HeadersPayload(或Request Payload/Form Data)。

关键发现

  • 请求URL:可能是https://login.taobao.com/member/login.jhtml?或类似的子域名下的地址。
  • 请求参数:你会看到loginId(用户名)、password2(加密密码)、publicKey等一系列字段。password2的值类似aBcDeFgHiJkL...123456==这样的长字符串。

同时,留意在登录页面加载时或点击登录前,是否有一个获取密钥的请求。它可能返回一个 JSON,里面包含了publicKeykeyexponent等信息。

3.2 第二步:在源码中搜索与断点调试

  1. 切换到Sources选项卡。
  2. Ctrl+Shift+F(Windows/Linux)或Cmd+Opt+F(Mac)打开全局搜索框。
  3. 搜索关键词”password2“(带双引号)。在混淆代码中,字符串常量比变量名更容易存活下来。
  4. 在搜索结果中,点击进入包含这个字符串的 JS 文件。代码很可能被压缩成一行,点击左下角的{}(美化代码)按钮让其可读。

定位加密点: 美化后,搜索password2,找到类似这样的赋值语句:

data['password2'] = encryptedPassword; // 或者 obj.password2 = encryptFunc(plainPwd, pubKey);

encryptedPasswordencryptFunc这一行左侧的行号上点击,设置一个断点。

3.3 第三步:追踪调用栈与提取逻辑

  1. 回到登录页,再次触发登录操作。代码执行会在你的断点处暂停。
  2. 观察右侧的ScopeCall Stack面板。
    • Local Scope:可以看到当前函数内的变量,如plainPwd(明文密码)、pubKey(公钥对象或字符串)、encryptedPassword的值。记下它们!
    • Call Stack:显示函数调用链。点击调用栈中上一层的函数,可以跳转到调用当前函数的地方,逐步向上回溯,直到找到加密的入口函数。

核心逻辑提取示例: 假设我们最终定位到一个函数:

function encryptPassword(pwd, key) { var encryptor = new JSEncrypt(); encryptor.setPublicKey(key); var encrypted = encryptor.encrypt(pwd); return encrypted; }

这就非常清晰了:使用了 JSEncrypt 库,公钥key是字符串形式(通常是 PEM 格式,以-----BEGIN PUBLIC KEY-----开头),直接加密明文密码pwd

更复杂的情况: 有时,公钥不是直接以 PEM 字符串给出,而是服务器返回了模数n(一个很长的16进制或Base64字符串)和指数e(通常是”10001“的16进制0x010001)。那么 JS 代码中可能会用这些参数构造一个 RSAKey 对象。类似这样:

var rsaKey = new RSAKey(); rsaKey.setPublic(n, e); // n 是模数,e 是指数 var encrypted = rsaKey.encrypt(pwd);

我们需要记录下ne的具体值以及它们的格式(是16进制字符串还是Base64?)。

实操心得:在断点状态下,你可以将鼠标悬停在变量上查看其值,或在 Console 面板中直接输入变量名回车查看。对于复杂的对象,使用JSON.stringify(varName, null, 2)可以漂亮地打印出来,方便复制。

4. Python 复现 RSA 加密全过程

拿到了加密逻辑和密钥信息,我们就可以在 Python 中动手了。这里根据上面分析的两种常见情况,给出复现方案。

4.1 环境准备与依赖安装

首先,确保你的 Python 环境(建议 3.6 以上)已经就绪。我们需要安装处理 RSA 加密的库。rsa库和cryptography库都是不错的选择,cryptography更强大也更现代。这里以cryptography为例。

pip install cryptography

4.2 情况一:使用 PEM 格式公钥字符串

如果逆向发现公钥是完整的 PEM 格式字符串(-----BEGIN PUBLIC KEY-----...),那么复现非常简单。

from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives import hashes import base64 # 从网络请求或源码中获取的 PEM 公钥字符串 public_key_pem = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzXpLZgJYLpFhW... ... (你的实际公钥内容) ... -----END PUBLIC KEY-----""" # 1. 加载公钥 public_key = serialization.load_pem_public_key( public_key_pem.encode('utf-8') ) # 2. 待加密的明文密码 plain_password = "YourPassword123" # 3. 执行加密 # PKCS1 v1.5 填充是 JSEncrypt 默认使用的,对应 OAEP 填充在 cryptography 中是 PKCS1v15 cipher_text = public_key.encrypt( plain_password.encode('utf-8'), padding.PKCS1v15() # 注意!这是关键,淘宝登录通常使用 PKCS1v1.5 填充 ) # 4. 将加密后的字节进行 Base64 编码,得到最终的 password2 password2 = base64.b64encode(cipher_text).decode('utf-8') print(f"加密后的 password2: {password2}")

关键点解析

  • 填充方案padding.PKCS1v15()是关键。RSA 加密明文需要填充,JSEncrypt 默认使用 PKCS#1 v1.5 填充。如果这里选错(比如用了 OAEP),加密结果会完全不同,导致登录失败。这是逆向复现中最常见的坑之一。
  • 编码:确保明文密码在加密前转换为字节(.encode('utf-8')),加密输出也是字节,最后按需进行 Base64 编码。

4.3 情况二:使用模数(n)和指数(e)构造公钥

如果服务器返回的是单独的模数n和指数e,我们需要用它们来构造公钥。

from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding import base64 import binascii # 假设从服务器获取的 n 是 Base64 编码字符串,e 是 "010001" (16进制字符串) n_base64 = "AMJ9Td...很长一串...QAB" # 你的模数 n e_hex = "010001" # 1. 解码并转换参数 # 将 Base64 的 n 解码为字节,然后转换为整数 n_bytes = base64.b64decode(n_base64) n_int = int.from_bytes(n_bytes, byteorder='big') # 大端序 # 将 16 进制的 e 转换为整数 e_int = int(e_hex, 16) # 2. 构造 RSA 公钥数字 public_numbers = rsa.RSAPublicNumbers(e_int, n_int) # 从数字构造公钥对象,需要指定后端 from cryptography.hazmat.backends import default_backend public_key = public_numbers.public_key(default_backend()) # 3. 加密明文密码 plain_password = "YourPassword123" cipher_text = public_key.encrypt( plain_password.encode('utf-8'), padding.PKCS1v15() # 同样使用 PKCS1v1.5 填充 ) # 4. Base64 编码输出 password2 = base64.b64encode(cipher_text).decode('utf-8') print(f"加密后的 password2: {password2}")

注意事项

  • 字节序int.from_bytes(n_bytes, byteorder='big')中的byteorder='big'(大端序)是网络传输和许多加密库中的标准。务必与 JS 代码中的处理方式保持一致。
  • 参数格式:务必确认ne的原始格式。n可能是 Base64,也可能是 16 进制字符串。e通常是”010001“(16进制),对应十进制 65537,这是最常用的 RSA 公钥指数。解码步骤需要根据实际情况调整。

4.4 验证复现结果

生成password2后,如何验证它是否正确?

  1. 本地对比法(推荐):在浏览器断点处,获取到 JS 代码生成的password2值。用你的 Python 脚本,使用相同的明文密码相同的公钥进行计算,对比两个password2字符串是否完全一致。这是最直接的验证方式。
  2. 网络请求替换法:使用抓包工具(如 Fiddler、Charles)或浏览器的开发者工具,拦截登录请求。将请求体中的password2参数值替换为你 Python 脚本生成的值,然后放行请求。观察服务器返回的响应。如果返回成功或密码错误(而非参数错误),通常说明加密格式是正确的;如果返回明显的加密参数错误,则说明复现有误。

5. 常见问题、调试技巧与深度避坑指南

逆向复现的路上不会一帆风顺,下面是我踩过坑后总结的一些核心问题和解决方案。

5.1 加密结果不一致的排查清单

如果你的 Python 输出和 JS 输出不一致,请按以下顺序检查:

  1. 公钥是否一致?这是最根本的。确保 Python 代码加载的公钥(无论是 PEM 字符串还是 n/e)与 JS 代码运行时使用的完全一致。一个字符都不能差,包括 PEM 的头部尾部标记和换行符。
  2. 明文是否一致?确保加密前的密码字符串完全一致。注意是否有不可见字符、空格,或者 JS 端是否对密码进行了预处理(如 Trim 操作)。
  3. 填充方案是否正确?99% 的问题出在这里。JSEncrypt 默认使用PKCS#1 v1.5填充。在 Pythoncryptography库中,对应的就是padding.PKCS1v15()。如果你错误地使用了padding.OAEP,结果必然不同。
  4. 编码解码是否正确?牢记流程:明文(字符串)-> UTF-8 字节 -> RSA 加密 -> 字节 -> Base64 编码 -> 字符串(password2)。检查每个环节的输入输出类型。
  5. 模数(n)和指数(e)的处理:如果使用 n/e,检查它们的编码转换。n从 Base64 解码后得到的字节,转换成整数时字节序(byteorder)是否正确?e的进制转换是否正确?

5.2 进阶:处理动态密钥与登录上下文

淘宝等大型网站为了安全,登录流程可能更复杂:

  • 动态公钥:每次登录页面加载或每次登录尝试前,服务器都可能下发一个新的公钥。这意味着你不能硬编码一个公钥,必须在 Python 脚本中模拟首次访问,从页面或特定接口中提取最新的公钥。
  • 登录令牌(Token):登录请求可能还需要umidTokensessionIdcookie等其他上下文参数,这些参数来自前序的页面请求。你的 Python 脚本需要模拟一个完整的会话:先请求登录页,解析 HTML 或后续接口响应获取公钥和 Token,再组合密码进行加密提交。
  • 滑块验证码:如果触发验证码,单纯的密码加密复现就无法完成登录了。这属于另一个层面的逆向(验证码识别或绕过),不在本文讨论范围内。但通常,在未触发风控的正常环境下,正确的加密请求是可以直接登录的。

5.3 Python 代码的健壮性优化

  1. 错误处理:添加try...except块来捕获密钥加载失败、加密失败等异常。
  2. 日志记录:使用logging模块记录关键步骤的信息,如获取到的公钥片段、加密前后的值,便于离线调试。
  3. 配置化:将公钥获取的 URL、请求头等信息写入配置文件,提高脚本的适应性。
  4. 模拟完整会话:使用requests.Session()对象来保持 cookies,模拟浏览器行为。
import requests import re from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import padding import base64 import logging logging.basicConfig(level=logging.INFO) session = requests.Session() session.headers.update({ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }) def fetch_public_key(): """模拟访问登录页,从页面或接口中提取公钥""" login_page_url = "https://login.taobao.com/" resp = session.get(login_page_url) # 这里需要根据实际页面结构,用正则或解析库提取公钥 # 例如,公钥可能在某个 <script> 标签的变量里,或者在一个单独的接口返回中 # 假设我们通过正则从 JS 变量中提取到了 PEM 格式公钥 match = re.search(r'var pubKey = "-----BEGIN PUBLIC KEY-----(.*?)-----END PUBLIC KEY-----";', resp.text, re.S) if match: pub_key_str = f"-----BEGIN PUBLIC KEY-----\n{match.group(1)}\n-----END PUBLIC KEY-----" logging.info("成功提取公钥") return pub_key_str else: logging.error("未能提取公钥") return None def encrypt_password(password, public_key_pem): """使用公钥加密密码""" public_key = serialization.load_pem_public_key(public_key_pem.encode()) cipher = public_key.encrypt(password.encode(), padding.PKCS1v15()) return base64.b64encode(cipher).decode() def login(username, encrypted_pwd): """提交登录请求""" login_api = "https://login.taobao.com/member/login.jhtml" payload = { 'loginId': username, 'password2': encrypted_pwd, # ... 其他必要的参数,如 token、umid 等,需要从前期请求中获取 } resp = session.post(login_api, data=payload) # 检查响应,判断登录是否成功 return resp # 主流程 if __name__ == '__main__': pub_key = fetch_public_key() if pub_key: my_password = "YourSecurePassword" pwd2 = encrypt_password(my_password, pub_key) login_response = login("your_username@example.com", pwd2) print(login_response.text)

6. 总结与扩展思考

通过以上步骤,我们完成了一次完整的 Web 登录 RSA 加密逆向与 Python 复现。这个过程的核心在于耐心细致:耐心地分析网络请求,细致地对比每一步的数据。当你成功用 Python 生成出与浏览器完全一致的password2时,那种成就感是无与伦比的。

这个技能的价值不仅限于淘宝。几乎所有采用前端加密的登录系统(如许多银行、电商、社交平台),其逆向思路都是相通的:抓包 -> 定位关键参数 -> 逆向加密函数 -> 用其他语言复现。区别只在于加密算法(可能是 RSA、AES、SM系列国密等)、混淆的复杂度以及反调试手段的强弱。

最后再次强调,技术是把双刃剑。掌握逆向分析能力,能让你更深刻地理解系统工作原理,写出更健壮的自动化程序。但请务必用于合法的学习、测试和授权范围内的自动化任务,尊重网站的数据安全和用户隐私,这是每一位技术从业者的底线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询