mitmproxy的一些基础知识和常规用法可以参考:

Mitmproxy联动burp自动化签名/加解密

前言

某众测项目,发现一小程序(很敏感,打上厚厚厚码),抓包后,看到请求包和响应包都做了加密处理:

image-20250611165419332

image-20250611165913119

前几天简单研究了一下mitmproxy在加解密环境中的应用,这不刚好有个实战环境,说干就干,走起~

前端逆向

对比数据包,可以看到有两个可以参数在请求包和响应包中都有出现:

  • 请求/响应体 dora
  • 请求/响应头Pan

由于是小程序不能像web一样直接打断点调试了,所以这块用了下小程序hook工具,调试出来源码来逆向。

(最后尝试访问了下,发现居然有对应的H5,这一下就省事了,不过小程序有许多接口要和微信做交互,H5仅仅用来做参数逆向还可以,测试就不行了)

此处以H5页面的调试为例(目的不是为了逆向参数,主要是使用工具)

dora参数:

1、比较简单,下xhr断点,一步步跟栈,最终定位到具体加密函数处:

image-20250611170929102

2、进入到函数Object(f.b)中,可以明确这块请求体位AES加密:

image-20250611171302229

AES-CBC-pkcs7填充方式

3、使用在线加解密工具验证一下,成功~

image-20250611171513918

4、不过,这块多次打断点,发现AES的密钥是随机变化的

image-20250611171702805

5、继续翻代码,最终定位到了AES密钥生成的位置:

image-20250611171805341

image-20250611171836125

进入到函数Object(f.h)中,确认为本地随机生成:

function A() {
var e = function() {
return (65536 * (1 + Math.random()) | 0).toString(16).substring(1)
};
return "".concat(e()).concat(e()).concat(e()).concat(e())
}

image-20250611171956892

根据经验,基本可以判断处理出来该站点为数字信封的加密方式

  • 前端生成AES的随机密钥,加密请求体
  • 使用RSA或其他非对称加密算法,对随机生成的AES密钥进行加密

所以,针对另一个参数pan,猜测大差不差就是RSA加密了

Pan参数:

1、不用特意去找,pan参数的生成逻辑就在AESkey的下面

image-20250611172351320

2、进入到函数里面去看,有打印出RSA的公钥,不出所料就是RSA加密AES的密钥了:

image-20250611172504011

image-20250611172524166

RSA的公钥是给定了的,这个一般情况下也不会变化,主要是看是不是硬编码存储的。这个站点就是本地保存了hex的字节,然后前端生成的(不细究)

image-20250611172651764

3、到这块请求包里面的加密逻辑就清楚了。

响应包:

同理,响应包中也有两个参数dorapan。前端定位到具体位置,下断点,发现其逻辑和请求包类似:

image-20250611172957934

image-20250611173049192

响应数据包逻辑为:

  • 使用公钥解密响应包中的pan参数,得到服务端加密数据的AES密钥
  • 使用得到的密钥再对响应体数据进行解密

所以,整个系统的加解密流程如下:

请求包

  1. 生成随机AES密钥,加密明文数据
  2. 使用RSA公钥对AES密钥加密

响应包

  1. 使用RSA公钥对pan参数解密,得到AES密钥
  2. 使用得到的AES密钥解密数据包返回前端

image-20250611174810511

mitmproxy

针对于这种请求响应的都加密的情况,数据包流程图如下:

image-20250611175143190

AES-key密钥固定

首先,由于前端的AES-key是随机生成的,这块要实现整个burp抓包过程中纯明文,就需要对AES-key进行固定

(也就是要能保证在mitmproxy一级代理处,能对浏览器发送来的密文数据解密,这样展示在burp中的才会是明文)

密钥固定逻辑就是本地替换JS文件,一般有以下几种方法:

  • 浏览器Override

  • yakit热加载

  • Charles的Map local

  • burp的本地替换

主要逻辑都是修改JS文件,然后本地加载即可。这里使用Charles进行本地替换。

1、下载原本的JS文件并修改,将其密钥返回值固定为 1111111111111111

image-20250612095019288

2、打开Charles,将原本的main.js文件替换我们修改后的文件:

image-20250612095134497

3、访问目标小程序,抓包查看,发现已经成功加载了我们本地修改后的js文件

image-20250612095444091

4、重新使用burp抓取数据包,请求体内容就可以使用密钥1111111111111111进行解密了:

image-20250612095739620

image-20250612095827687

4、但查看返回包时却发现产生了报错:

image-20250612095903893

检测到重放攻击,什么鬼????

猜测:大概率为该系统后端对生成的AES-key做了记录,用来判断一段时间内密钥是否已存在,比如保存在Redis中。只要设置好AES-key的过期时间,对一段时间内的密钥进行校验,这样既可以保证系统正常运行,也能避免数据存储过大的问题。

解决办法:在mitmproxy作为burp的上游代理时——也就是对请求包数据做再加密时,需要使用随机生成的密钥

  • 使用随机生成AES-key加密请求头
  • RSA公钥加密随机AES-key

mitmproxy代码

该系统的加解密逻辑图如下:

image-20250612102603041

实现mitmproxy一级代理

mitmproxy.exe -p 8888 -m upstream:http://127.0.0.1:8080  -s chrome_burp.py --ssl-insecure -k -q 
  • mitmproxy监听8888端口,小程序将流量转发到mitmproxy

  • 设置mitmproxy的上游代理端口为8080,burp监听该端口,将流量转发到burp

chrome_burp.py脚本如下:

import json
import mitmproxy.http
import mitmproxy.ctx
import base64
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Util.Padding import pad, unpad


def aes_encrypt(key, data):
cipher = AES.new(key, AES.MODE_CBC, key)
padded_data = pad(data.encode(), AES.block_size)
cipher_text = cipher.encrypt(padded_data)
return base64.b64encode(cipher_text).decode()


def aes_decrypt(data):
key = '1111111111111111'.encode('utf8')
cipher = AES.new(key, AES.MODE_CBC, key)
decrypted_data = cipher.decrypt(base64.b64decode(data))
unpadded_data = unpad(decrypted_data, AES.block_size)
return unpadded_data.decode()


# === 解包 PKCS#1 v1.5 填充 ===
def pkcs1_unpad(padded: bytes) -> bytes:
if not padded.startswith(b'\x00\x01'):
raise ValueError("不是有效的 PKCS#1 v1.5 格式")
sep = padded.find(b'\x00', 2)
if sep < 0:
raise ValueError("找不到 padding 分隔符")
return padded[sep + 1:]


# 实现RSA公钥解密(针对RSA公钥加密的情况)
def rsa_decrypt(data):
pan_base64 = data.replace(" ", "")
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCD6rhwXRwzyhnTVvPvMmMptWqJ
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
D/mnzsSC1rEB7zDtV3qJfqpNKyNq31rqtrhNEaFGtouQChcfNTCa0TJ4KKb04fn/
2YaFvXsHDUjDRfhvawIDAQAB
-----END PUBLIC KEY-----"""
ciphertext_bytes = base64.b64decode(pan_base64)
# === 加载公钥,做 RSA 运算 ===
key = RSA.import_key(public_key_pem)
n = key.n
e = key.e
cipher_int = int.from_bytes(ciphertext_bytes, byteorder='big')
plaintext_int = pow(cipher_int, e, n)
key_length = (key.size_in_bits() + 7) // 8
padded_plaintext = plaintext_int.to_bytes(key_length, byteorder='big')
aes_key = pkcs1_unpad(padded_plaintext)
return aes_key


# 解密后的请求体处理回加密流程
def convert_to_dict(b64_str):
return '{"dora": "' + b64_str + '"}'


class dataDecrypt:
# 请求解密
def request(self, flow: mitmproxy.http.HTTPFlow):
if flow.request.method == 'POST':
body = str(flow.request.text)
if "dora" in body:
data = json.loads(flow.request.get_text())
request_data = data["dora"]
flow.request.text = aes_decrypt(request_data)

# 响应加密
def response(self, flow: mitmproxy.http.HTTPFlow):
headers = flow.request.headers
if "Pan" in headers:
databody = str(flow.response.text)
resp_pan = flow.response.headers["Pan"]
aes_key = rsa_decrypt(resp_pan)
enc_data = aes_encrypt(aes_key, databody)
body_data = convert_to_dict(enc_data)
flow.response.text = body_data


addons = [
dataDecrypt()
]

此时抓包,可以看到数据包从客户端到burp的时候,已经成功明文显示了:

image-20250612134509744

实现mitmproxy上游代理

mitmproxy.exe -p 8081 -s burp_server.py
  • 设置mitmproxyburp的上游代理,mitmproxy监听8081端口,burp将流量转发到mitmproxy

burp_server.py代码如下:

import random
import mitmproxy.http
import mitmproxy.ctx
from Crypto.Util.Padding import pad, unpad
import execjs
import json
from Crypto.PublicKey import RSA
from Crypto.Cipher import AES
import base64


# 还原JS的随机密钥函数
def randomA():
def e():
n = int(65536 * (1 + random.random()))
return hex(n)[2:][1:]

return e() + e() + e() + e()


# 实现RSA加密
def rsa_encrypt(data):
with open('rsa.js', 'r', encoding='utf-8') as f:
js_obj = execjs.compile(f.read())
result = js_obj.call("rsa_encrypt", data)
return result


# === 解包 PKCS#1 v1.5 填充 ===
def pkcs1_unpad(padded: bytes) -> bytes:
if not padded.startswith(b'\x00\x01'):
raise ValueError("不是有效的 PKCS#1 v1.5 格式")
sep = padded.find(b'\x00', 2)
if sep < 0:
raise ValueError("找不到 padding 分隔符")
return padded[sep + 1:]


# 实现RSA公钥解密(针对RSA公钥加密的情况)
def rsa_decrypt(data):
pan_base64 = data.replace(" ", "")
public_key_pem = """-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCD6rhwXRwzyhnTVvPvMmMptWqJ
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
2YaFvXsHDUjDRfhvawIDAQAB
-----END PUBLIC KEY-----"""
ciphertext_bytes = base64.b64decode(pan_base64)
# === 加载公钥,做 RSA 运算 ===
key = RSA.import_key(public_key_pem)
n = key.n
e = key.e
cipher_int = int.from_bytes(ciphertext_bytes, byteorder='big')
plaintext_int = pow(cipher_int, e, n)
key_length = (key.size_in_bits() + 7) // 8
padded_plaintext = plaintext_int.to_bytes(key_length, byteorder='big')
aes_key = pkcs1_unpad(padded_plaintext)
return aes_key


# 解密后的请求体处理回加密流程
def convert_to_dict(b64_str):
return '{"dora": "' + b64_str + '"}'


def aes_encrypt(key, data):
key = key.encode('utf8')
cipher = AES.new(key, AES.MODE_CBC, key)
padded_data = pad(data.encode(), AES.block_size)
cipher_text = cipher.encrypt(padded_data)
return base64.b64encode(cipher_text).decode()


def aes_decrypt(data, key):
cipher = AES.new(key, AES.MODE_CBC, key)
decrypted_data = cipher.decrypt(base64.b64decode(data))
unpadded_data = unpad(decrypted_data, AES.block_size)
return unpadded_data.decode()


class dataEncrypt:
# 请求加密
def request(self, flow: mitmproxy.http.HTTPFlow):
if flow.request.method == 'POST':
headers = flow.request.headers
if "Pan" in headers:
# RSA加密生成请求头pan并添加到数据包中
random_key = randomA()
header_pan = rsa_encrypt(random_key)
flow.request.headers['Pan'] = header_pan
# 重新构造请求体并加密
body = str(flow.request.text)
enc_data = aes_encrypt(random_key, body)
body_data = convert_to_dict(enc_data)
flow.request.text = body_data

# 响应解密
def response(self, flow: mitmproxy.http.HTTPFlow):
databody = str(flow.response.text)
resp_pan = flow.response.headers["Pan"]
if "dora" in databody:
data = json.loads(flow.response.get_text().replace("\u003d", "="))
response_data = data["dora"]
aes_key = rsa_decrypt(resp_pan)
plan_data = aes_decrypt(response_data, aes_key)

flow.response.text = plan_data


addons = [
dataEncrypt()
]

此时将一级代理和上游代理都开启,即可实现burp全局明文了:

  • 未启动mitmproxy时的数据包

image-20250612135757956

image-20250612135901550

  • 启动mitmproxy后的数据包

image-20250612135246954

image-20250612135531061