记一次复杂的rsa+aes绕过

admin 2026-06-23 05:49:26 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文记录了一次针对前端RSA+AES混合加密的逆向分析实战,通过HookCryptoJS随机函数固定AES密钥与IV,结合Mitmproxy中间人代理实现请求解密/重加密,最终达成BurpSuite明文调试目的。关键发现包括加密数据包内含IV拼接结构、Python解密脚本编写及双向代理链路设计,为类似加密场景的渗透测试提供可复用的技术路径。 综合评分: 80 文章分类: WEB安全,逆向分析,红队,渗透测试,安全工具


cover_image

记一次复杂的rsa+aes绕过

原创

z0Reverse z0Reverse

z0Reverse

2026年6月21日 08:53 广东

在小说阅读器读本章

去阅读

第一次研究web侧的js逆向,这个还比较简单,大佬们勿喷。场景是用随机生成的aes密钥和iv加密body,最终的加密结果是body拼接iv,然后rsa公钥加密aes密钥,进行与服务端的通信

交流

个人账号,用于在学习过程中技术交流分享,进行不定期更新,可关注作者Github:z0Reverse;CSDN:z0Reverse;52pojie:North886。

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【z0Reverse】联系作者立即删除!

逆向目标

  • 目标:某前端rsa+aes加密
  • apk 版本:xxx
  • 逆向参数:encryptedData

抓包分析

登录请求包,结果如下:

逆向分析

分析js代码

这里的思路就是,找到加密点,进行hook。因为无法拿到rsa的私钥,所以考虑hook aes密钥和iv为固定的值,然后采用浏览器 → mitmproxy(解密) → Burp(看到明文/修改) → mitmproxy(加密) → 服务器方式实现bp侧明文

前端js采用aes密钥、iv随机生成(一般都有规则,不会纯随机),aes加密请求体,rsa公钥加密aes密钥,传输给后端

去分析encryptedBody定位到login.js中,随机生成aes密钥以及iv,然后将body进行加密采用cbc模式,body格式”username:password“格式

打断点尝试

Hook JS

思路:注入aes加密前,让aeskey永远为我们固定的值。无论rsa加密aes密钥,加密的密钥是我们固定好的值

// ========== AES Key、IV 自定义固定值(自行替换) ==========// AES-256 32字节 十六进制(64位hex)const FIX_AES_KEY_HEX = ”00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff”;// IV 16字节 十六进制(32位hex)const FIX_IV_HEX = ”112233445566778899aabbccddeeff00”; // 缓存原始随机函数let originalWordArrayRandom; // 转换十六进制字符串为 WordArrayfunction hexToWordArray(hexStr) { return CryptoJS.enc.Hex.parse(hexStr);} // Hook CryptoJS.lib.WordArray.randomfunction hookCryptoRandom() { // 保存原生方法 originalWordArrayRandom = CryptoJS.lib.WordArray.random;
 CryptoJS.lib.WordArray.random = function(size) { // 判断长度区分 AESKey(32) / IV(16) if (size === 32) { // 生成固定32字节AES密钥 return hexToWordArray(FIX_AES_KEY_HEX); } else if (size === 16) { // 生成固定16字节IV return hexToWordArray(FIX_IV_HEX); } else { // 其他长度走原生随机 return originalWordArrayRandom(size); } };} // 等待 CryptoJS 加载完成再 Hookfunction waitCryptoJSAndHook() { if (window.CryptoJS && CryptoJS.lib && CryptoJS.lib.WordArray) { hookCryptoRandom(); console.log(”[Hook成功] CryptoJS.random 已劫持,固定AES Key/IV”); } else { setTimeout(waitCryptoJSAndHook, 10); }}waitCryptoJSAndHook();

结果验证

Hook成功,进行验证 这是hook成功后的数据,现在进行aes解密验证

x-key: okrkPcxIHub0EjiKmkjyCFw6R4G12IDBz1grLJlcZnOoCS0GMAZWXU4CrRYDSrxQ2PxqqHAbGcnbB1ZLKfXOhZzPLZgGWUMan6SUDe50ZhSnMGBAfYUfCJQLcyXmxvdqA7BVvtL+DpqhBSr3o/TnmXYu7Zgb8o8sK7kXn59GACU+4DOXzoSqhnErAOv0boBP0xzXiYFeEL2M+/0DMa+CRQSyv8s5V8dQVuQI+hqncCaw+NgkmRFPfTcrUqNq/p+z8AFWMFSxEc+AO5A9Y4YqacZkktju0l+IimCLdSUuV1dz9C653RJvwpeHEG49PW9zutVLMlQbENG9RSWQWrXjpw==Accept: */*Origin: http://121.37.229.118:8080Referer: http://121.37.229.118:8080/Accept-Encoding: gzip, deflateAccept-Language: zh-CN,zh;q=0.9Cookie: Hm_lvt_49889d9149ea3475919030325b18afef=1764308241,1764685415 {”encryptedBody”:”ESIzRFVmd4iZqrvM3e7/AKkLykgip5J2oyuHHe/VO3U=”}

解密失败,再去看代码发现,加密的数据这里,吧iv拼接进去了(重点!!!如果iv没有特殊规则,随机生成,那么后端就必须拿到iv,否则解密肯定不成功。这里是吧iv放到加密后的数据中进行了拼接)

所以解密脚本

#!/usr/bin/env python3# -*- coding: utf-8 -*-import base64import jsonfrom Crypto.Cipher import AESfrom Crypto.Util.Padding import unpad# ===================== 固定参数(必须与前端 Hook 一致) =====================# AES-256 密钥(32 字节),十六进制字符串(64 个 hex 字符)FIXED_AES_KEY_HEX = ”00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff”# IV(16 字节),十六进制字符串(32 个 hex 字符)FIXED_IV_HEX = ”112233445566778899aabbccddeeff00”# 将 hex 转为字节KEY = bytes.fromhex(FIXED_AES_KEY_HEX)IV = bytes.fromhex(FIXED_IV_HEX)def decrypt_login_payload(encrypted_body_b64: str) -> str: ””” 解密 /api/auth/login 请求中的 encryptedBody 字段 :param encrypted_body_b64: 请求体中的 encryptedBody 值(Base64 字符串) :return: 明文字符串,格式 ”username:password” ”””# 1. Base64 解码得到 IV(16) + 密文 的拼接字节 combined = base64.b64decode(encrypted_body_b64)# 2. 拆分:前 16 字节为 IV,剩余为密文(这里直接用固定 IV,也可从数据中提取)
# 从数据中提取 IV 仅供校验,实际解密用固定 IV iv_from_data = combined[:16] ciphertext = combined[16:]# (可选)校验 iv_from_data 是否等于固定 IV,若不相等则可能被篡改或密钥不匹配 if iv_from_data != IV: print(”[警告] 数据中的 IV 与固定 IV 不一致,仍使用固定 IV 继续解密”)# 3. AES-CBC 解密 cipher = AES.new(KEY, AES.MODE_CBC, IV)# 使用固定 IV decrypted_padded = cipher.decrypt(ciphertext)# 4. 去除 PKCS7 填充 try: decrypted = unpad(decrypted_padded, AES.block_size) except ValueError:# 如果填充不对,可能是密钥/IV错误,或数据损坏 raise ValueError(”解密失败,可能密钥/IV不正确或数据损坏”)# 5. 转为 UTF-8 字符串 return decrypted.decode('utf-8')# ========================== 使用示例 ==========================if __name__ == ”__main__”:# 示例:从浏览器的 Network 面板复制 encryptedBody 值 sample = ”ESIzRFVmd4iZqrvM3e7/AKkLykgip5J2oyuHHe/VO3U=” try: plain = decrypt_login_payload(sample) print(f”解密成功: {plain}”) except Exception as e: print(f”解密失败: {e}”)# 如果是从完整请求体 JSON 中解析,可以这样:
# with open('request.json', 'r') as f:
# data = json.load(f)
# encrypted_body = data['encryptedBody']
# print(decrypt_login_payload(encrypted_body))

验证

MitProxy联动BurpSuit实现明文

接下来做自动脚本,实现前端hook,解密明文到bp,bp转发出去给服务端还是密文:浏览器 → mitmproxy(解密) → Burp(看到明文/修改) → mitmproxy(加密) → 服务器 思路:浏览器挂代理到8081端口,mitA脚本,监听8081端口并将数据进行解密,转给上游代理8080(burpsuit),burpsuit开启上游代理8082端口转给mitB脚本,B脚本再根据原始加密逻辑进行还原,发送给服务端密文

实现:

mit脚本:

 # decrypt_proxy.py # 启动命令:mitmproxy -p 8081 --mode upstream:http://127.0.0.1:8080 -s decrypt_proxy.py import json import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import unpad from mitmproxy import http # ========== 固定 AES Key/IV 和最初保持一致 ==========FIX_AES_KEY_HEX = ”00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff” FIX_IV_HEX = ”112233445566778899aabbccddeeff00” KEY = bytes.fromhex(FIX_AES_KEY_HEX) IV = bytes.fromhex(FIX_IV_HEX)
def decrypt_aes_cbc(encrypted_b64: str) -> str | None:  ”””原始解密逻辑:base64(IV+密文),丢弃数据包内IV,使用固定IV解密”””  try:  combined = base64.b64decode(encrypted_b64)  ciphertext = combined[16:]  cipher = AES.new(KEY, AES.MODE_CBC, IV)  decrypted = unpad(cipher.decrypt(ciphertext), AES.block_size)  return decrypted.decode('utf-8')  except Exception as e:  print(f”[解密失败] {e}”)  return None
def request(flow: http.HTTPFlow) -> None:  print(”\n===== 解密代理8081 收到请求 =====”)  print(f”原始路径: {flow.request.path}, Method: {flow.request.method}”)  print(f”原始请求体: {flow.request.text}”) # 打印当前携带的x-key,确认不会丢失  origin_xkey = flow.request.headers.get(”x-key”, ”无”)  print(f”当前携带x-key: {origin_xkey[:80]}...”) # 仅处理 POST + application/json if flow.request.method != ”POST”:  print(”跳过:非POST请求,原样放行”)  return  content_type = flow.request.headers.get(”Content-Type”, ””)  if ”application/json” not in content_type:  print(”跳过:非JSON请求,原样放行”)  return # 备份全部原始内容,异常时完整恢复(防止丢header/body)  origin_body = flow.request.text
 try:  body = json.loads(flow.request.text)  encrypted_body = body.get(”encryptedBody”)  if not encrypted_body:  print(”跳过:无encryptedBody字段,不修改请求,所有头完整保留”)  return # 执行解密  plain = decrypt_aes_cbc(encrypted_body)  if plain is None:  print(”解密返回空,放弃修改请求体,原始流量透传”)  return
 print(f”解密明文:{plain}”) # 拆分账号密码,构造明文JSON  if ”:” in plain:  username, password = plain.split(”:”, 1)  new_body = json.dumps({”username”: username, ”password”: password}, separators=(',', ':'))  else:  new_body = plain # 只替换请求体,【完全不碰任何请求头,不删除x-key!】  flow.request.text = new_body  print(f”已替换为明文请求体: {new_body}”) # 移除了之前 flow.request.headers.pop(”x-key”, None) 这行删除代码
 except Exception as e:  print(f”解密处理异常,恢复原始完整请求: {repr(e)}”) # 异常恢复原始body,所有header(包含x-key)维持不变,不会丢失  flow.request.text = origin_body

mitB脚本

# encrypt_proxy.py # 启动命令:mitmproxy -p 8082 -s encrypt_proxy.py import json import base64 from Crypto.Cipher import AES from Crypto.Util.Padding import pad from mitmproxy import http# ========== 固定 AES Key/IV 沿用原来配置 ==========FIX_AES_KEY_HEX = ”00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff” FIX_IV_HEX = ”112233445566778899aabbccddeeff00” KEY = bytes.fromhex(FIX_AES_KEY_HEX) IV = bytes.fromhex(FIX_IV_HEX) def encrypt_aes_cbc(plaintext: str) -> str:  ”””沿用原始逻辑:固定IV,返回 Base64(IV + Ciphertext)””” cipher = AES.new(KEY, AES.MODE_CBC, IV)  ciphertext = cipher.encrypt(pad(plaintext.encode('utf-8'), AES.block_size))  combined = IV + ciphertext  return base64.b64encode(combined).decode('utf-8') def request(flow: http.HTTPFlow) -> None:  print(”\n===== 加密代理8082 收到明文请求 =====”)  print(f”原始Body: {flow.request.text}”)  x_key_val = flow.request.headers.get(”x-key”, ””)  print(f”透传原始x-key: {x_key_val[:60]}...”)# 只处理 POST + application/json if flow.request.method != ”POST”:  print(”跳过:非POST”)  return  content_type = flow.request.headers.get(”Content-Type”, ””)  if ”application/json” not in content_type:  print(”跳过:非JSON请求”)  return# 备份原始内容,异常时恢复,防止400  orig_body = flow.request.text  try:  body = json.loads(flow.request.text)  username = body.get(”username”)  password = body.get(”password”)  if username is None or password is None:  print(”跳过:无username/password,不加密,原样转发”)  return# 拼接明文 username:password plain = f”{username}:{password}”  encrypted_body = encrypt_aes_cbc(plain)  print(f”生成encryptedBody: {encrypted_body}”)# 构造加密请求体  new_body = json.dumps({”encryptedBody”: encrypted_body}, separators=(',', ':'))  flow.request.text = new_body# x-key 不做任何修改,直接透传,不新增、不删除、不RSA加密 # 删掉手动设置Content-Length(关键修复400)  print(f”替换后加密请求体: {new_body}”)  except Exception as e:  print(f”加密处理异常,恢复原始明文包: {repr(e)}”)# 出错还原原始body,不发送非法加密数据包  flow.request.text = orig_body

总结反思

踩坑总结:

1、挂这个mit代理,chorm有问题,应该是要配置mit证书,否则链接失败。不认这个代理。这边是用的edge

2、mit二级代理命令:

mitmproxy -p 8081 --mode upstream:http://127.0.0.1:8080 -s decrypt_proxy.py

3、针对webpack打包的js,无法被外部引用从而进行hook–待研究


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:z0Reverse z0Reverse z0Reverse《记一次复杂的rsa+aes绕过》

夏至|盛夏初临安全常存 网络安全文章

夏至|盛夏初临安全常存

文章总结: 火绒安全成立于2011年,专注终端安全领域,提供专业轻巧的安全产品。企业版针对内网脆弱环节拓展终端管理范围,提升兼容性和易用性,实现威胁可视化和管理
评论:0   参与:  0