JavaScript逆向实例:RSA公钥加密参数的提取与模拟
深入解析前端JavaScript中RSA公钥加密参数的逆向提取与模拟实战,涵盖定位加密逻辑、解析PEM与模数指数公钥格式,以及Python与Node.js模拟实现技巧。
在现代 Web 逆向工程中,登录入口和核心交易接口对密码、Token 等敏感字段采用非对称加密是极其常见的防御手段。遇到 RSA 加密时,逆向的核心目标并非破解高强度的数学算法,而是精准理清“公钥获取 - 算法加载 - 参数组装 - 密文输出”的浏览器执行链路,进而通过代码独立模拟这一过程。本文围绕这一目标,直接讲解 RSA 加密在前端的运行机制、公钥参数的提取技巧,以及使用 Python 和 Node.js 进行脱机模拟调用的具体方案。
一、RSA 在前端交互中的机制与密文特征
RSA 属于非对称加密算法,具备“公钥加密、私钥解密”的特性。在典型的 Web 认证体系中,服务端生成一对密钥后,将私钥严密保存在服务器内部,而将公钥下发给客户端浏览器或直接硬编码在前端脚本中。当用户在表单输入密码后,前端 JavaScript 脚本调用 RSA 算法库,使用公钥对明文进行加密,并将生成的密文字符串通过网络请求提交给后端。服务端接收到密文后,使用对应的私钥进行解密校验。
在抓包分析时,RSA 加密后的参数通常具备非常明显的形态特征:
- 密文长度固定:RSA 加密输出的二进制字节长度等于密钥模长(Key Size)。例如常见的 1024 位 RSA 对应 128 字节密文,Base64 编码后通常为 172 字符;2048 位 RSA 对应 256 字节密文,Base64 编码后约为 344 字符。如果密文采用十六进制(Hex)表示,1024 位则固定为 256 个十六进制字符。
- 密文动态变化:由于标准 RSA 加密广泛引入了填充机制(如 PKCS#1 v1.5 或 OAEP 填充中包含随机生成的填充字节),即使每次加密相同的明文密码,生成的密文结果也会完全不同。这能有效防御重放攻击与彩虹表碰撞。
二、步骤一:网络分析与加密入口定位
逆向的第一步是在浏览器开发者工具中找到调用加密函数的确切位置。具体可以按照以下步骤高效排查:
1. 抓包分析定位加密字段
打开浏览器的“网络(Network)”面板,在登录页面输入测试账号和密码(例如 admin / 123456),点击登录按钮。查看提交的请求 URL,在“载荷(Payload)”中寻找对应的参数。通常密码字段可能命名为 password、pwd、rsaPass 或一长串无规则哈希名。确认该值呈现上述典型的 Base64 或 Hex 长字符串格式。
2. 常用定位手段
- 全局搜索法:在“源代码(Sources)”面板按下 Ctrl+Shift+F(Mac 为 Cmd+Option+F),搜索特定关键字。首选搜索
setPublicKey、setKey、JSEncrypt、RSAKey、encrypt(。绝大部分网站采用开源成熟库(如jsencrypt.js),直接搜索这些固定 API 通常能快速命中设置公钥或执行加密的代码行。 - XHR/fetch 断点法:若全局搜索匹配项过多或代码被混淆,可以在“源代码”右侧的“XHR/fetch 断点”中添加登录接口的部分路径(例如
/api/login)。重新点击登录后,页面会在发起网络请求的瞬间暂停,此时在“调用堆栈(Call Stack)”中由下往上逐帧回溯,即可找到构建请求体前调用加密函数的位置。 - Console Hook 监听法:如果页面使用的是标准
JSEncrypt,可在控制台预先注入 Hook 逻辑捕获公钥和明文:
// 在网页加载前或控制台提前注入 Hook
(function() {
var originalSetPublicKey = window.JSEncrypt ? window.JSEncrypt.prototype.setPublicKey : null;
if (originalSetPublicKey) {
window.JSEncrypt.prototype.setPublicKey = function(pubKey) {
console.log("【RSA Hook】捕获到公钥:\n", pubKey);
return originalSetPublicKey.apply(this, arguments);
};
}
})();
三、步骤二:RSA 公钥提取与两种形态解析
在断点命中后,核心任务是获取公钥内容。在前端实现中,RSA 公钥主要以以下两种截然不同的形态存在:
1. 形态 A:标准 PEM / PKCS#8 格式
这是最标准的格式,普遍出现在使用 jsencrypt.js 库的网站中。公钥通常以 Base64 编码字符串呈现,包含头部声明 -----BEGIN PUBLIC KEY----- 和尾部声明 -----END PUBLIC KEY-----。有时前端代码为了缩减体积,只保留了中间的 Base64 密文体,在传递给 setPublicKey() 前动态拼接头尾标签。
ValueError: Could not deserialize key。
2. 形态 B:模数(Modulus / n)与指数(Exponent / e)分离格式
部分网站使用的是底层大数运算库(例如早期流行的 BigInteger.js、RSA.js 或自定义打包库)。这类实现不使用 PEM 证书标准,而是直接分发大素数乘积模数 n 和加密指数 e:
- 模数(n):一个超大整数,在 JS 中通常以十六进制字符串表示(如 1024 位为 256 个十六进制字符)。
- 指数(e):公钥指数,通常固定为十六进制的
"10001"(即十进制的 65537),极少数情况下为"3"或"11"。
前端调用方式通常形如:rsaKey.setKey(modulus, exponent) 或 setMaxDigits(130); key = new RSAKeyPair(e, "", n);。
四、步骤三:脱机环境下的加密模拟实战
在成功提取出公钥及算法细节后,我们可以选择使用 Node.js 直接复用前端逻辑,或使用 Python 原生加密库实现脱机运行。
1. 方案一:Node.js 环境模拟(直接复用库逻辑)
如果在逆向中确认前端就是使用的 jsencrypt,在 Node.js 中只需安装 npm install jsencrypt,即可 100% 复刻浏览器的加密行为:
// 引入 jsencrypt 模块(Node.js 环境)
const JSEncrypt = require('jsencrypt');
function encryptPassword(plainPassword, rawPublicKey) {
// 补齐 PEM 规范标头(如果抓包提取的公钥缺少头尾)
let formattedKey = rawPublicKey.trim();
if (!formattedKey.includes('-----BEGIN PUBLIC KEY-----')) {
formattedKey = `-----BEGIN PUBLIC KEY-----\n${formattedKey}\n-----END PUBLIC KEY-----`;
}
const encryptor = new JSEncrypt();
encryptor.setPublicKey(formattedKey);
const encrypted = encryptor.encrypt(plainPassword);
if (!encrypted) {
throw new Error("RSA 加密失败,请检查公钥格式是否正确");
}
return encrypted; // 默认输出 Base64 编码字符串
}
// 示例调用
const testPubKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDH3..."; // 提取的公钥内容
const cipherText = encryptPassword("MySecurePassword123!", testPubKey);
console.log("加密输出密文:\n", cipherText);
2. 方案二:Python 原生模拟(使用 PyCryptodome)
在生产级爬虫或自动化工程中,通常更倾向于纯 Python 实现,避免 Node 子进程调用的性能损耗。推荐安装行业标准的 pycryptodome 库:
pip install pycryptodome
针对前文提到的两种公钥形态,Python 分别提供了优雅的加载和构造方式:
import base64
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_v1_5
def encrypt_with_pem(plain_text: str, pem_public_key: str) -> str:
"""针对形态 A:标准 PEM 公钥的加密模拟"""
pem_key = pem_public_key.strip()
if not pem_key.startswith("-----BEGIN PUBLIC KEY-----"):
pem_key = f"-----BEGIN PUBLIC KEY-----\n{pem_key}\n-----END PUBLIC KEY-----"
# 导入公钥对象
rsa_key = RSA.import_key(pem_key)
# 前端 JSEncrypt 默认采用 PKCS#1 v1.5 填充
cipher = PKCS1_v1_5.new(rsa_key)
# 加密要求输入字节串,输出为二进制密文
cipher_bytes = cipher.encrypt(plain_text.encode("utf-8"))
# 转为与前端一致的 Base64 格式
return base64.b64encode(cipher_bytes).decode("utf-8")
def encrypt_with_modulus(plain_text: str, modulus_hex: str, exponent_hex: str = "10001") -> str:
"""针对形态 B:基于模数 n 与指数 e 构造公钥的加密模拟"""
n = int(modulus_hex, 16)
e = int(exponent_hex, 16)
# 根据模数与指数重构 RSA 密钥对象
rsa_key = RSA.construct((n, e))
cipher = PKCS1_v1_5.new(rsa_key)
cipher_bytes = cipher.encrypt(plain_text.encode("utf-8"))
return base64.b64encode(cipher_bytes).decode("utf-8")
# 示例测试
if __name__ == "__main__":
mock_pem = """MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC3T9..."""
# 模拟加密
# result = encrypt_with_pem("UserPassword123", mock_pem)
# print("Python 生成的 RSA 密文:", result)
五、常见踩坑点与排查清单
在实际逆向 RSA 参数的过程中,容易因细节疏漏导致服务端返回“解密失败”或“密码错误”。以下是关键排查清单:
| 问题现象 | 根本原因 | 解决与规避方案 |
|---|---|---|
| 首次可用,后续报错 | 公钥是服务端动态生成的临时密钥,且绑定了 Session、Timestamp 或 Key-ID | 排查抓包历史,寻找前置的 /getPublicKey 接口,在请求前动态获取公钥并携带对应 Token 提交。 |
| Python 报错 key format error | 公钥缺失标准的换行符或头尾起止行 | 严格保证公钥包含 -----BEGIN PUBLIC KEY-----,且 Base64 内容每 64 字符换行或直接由 RSA 库解析。 |
| 解密出乱码或空值 | 明文编码不一致(如前端进行了 encodeURIComponent) | 在断点处检查 encrypt() 函数接收的原始参数,确认是否预先拼入了盐值(Salt)或时间戳。 |
| 密文长度超标或报错 | 明文长度超过了 RSA 单块加密容量上限 | 1024 位 RSA 采用 PKCS1 填充时单次最多加密 117 字节。若加密长文本,需检查前端是否采用了分段加密循环。 |
掌握了公钥提取、形态识别与脱机模拟调用后,面对绝大多数网站的 RSA 参数加密均能迅速理清调用链,实现稳定、自动化的参数构造与接口联调。
JS 鼠标滚轮事件频繁触发导致卡顿怎么优化?
2026-11-05 17:08:07
优化JS回到顶部性能:防抖节流与requestAnimationFrame应用
2026-11-05 16:33:51
JS中Array、Set与Map的区别及应用场景选择
2026-11-05 16:03:02
PixiJS 常见报错排查:纹理跨域、黑屏与内存泄漏解决
2026-11-05 10:05:21
JS数组find与filter取值区别及性能对比
2026-11-05 08:39:21








