记一次复杂的rsa+aes绕过"

admin 2026-10-20 16:28:57 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

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(”\
===== 解密代理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(”\
===== 加密代理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--待研究


安全工程师如何打造自己的第二大脑:Karpathy的LLMWiki模式实战指南" 网络安全文章

安全工程师如何打造自己的第二大脑:Karpathy的LLMWiki模式实战指南"

本文介绍安全工程师如何应用Karpathy的LLMWiki模式构建个人知识库,通过直接加载上下文替代传统RAG方案。核心价值在于简化知识管理流程,支持渗透测试报告、CVE公告、事件日志等安全资料的快速摄入与查询,提供完整的配置指南和四大核心技能(万能摄入、URL摄入、全面查询、每日维护)的具体操作演示。
记一次复杂的rsa+aes绕过" 网络安全文章

记一次复杂的rsa+aes绕过"

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

四层交换机真的存在吗?"

本文探讨了四层交换机的存在性,指出其通常以四层负载均衡器形态存在。文章基于OSI模型解析了四层交换通过TCP/UDP端口号实现流量分发的原理,对比了二、三、四、七层交换机的核心功能差异,并阐述了该技术在实现高并发处理、服务器健康检查及安全防御方面的关键价值。
IP摄像头入侵:利用FFmpeg工具处理视频流" 网络安全文章

IP摄像头入侵:利用FFmpeg工具处理视频流"

该文档以乌克兰网络战为背景,介绍利用FFmpeg工具处理IP摄像头RTSP视频流的技术方法,包括安装FFmpeg、连接RTSP流、截取截图和录制视频等操作指令。文档强调摄像头访问权限在情报收集中的战略价值,但涉及未经授权入侵监控设备的内容需注意法律合规性。
评论:0   参与:  0