mitmproxy
的一些基础知识和常规用法可以参考:
Mitmproxy联动burp自动化签名/加解密
前言
某众测项目,发现一小程序(很敏感,打上厚厚厚码),抓包后,看到请求包和响应包都做了加密处理:
前几天简单研究了一下mitmproxy
在加解密环境中的应用,这不刚好有个实战环境,说干就干,走起~
前端逆向
对比数据包,可以看到有两个可以参数在请求包和响应包中都有出现:
由于是小程序不能像web一样直接打断点调试了,所以这块用了下小程序hook工具,调试出来源码来逆向。
(最后尝试访问了下,发现居然有对应的H5
,这一下就省事了,不过小程序有许多接口要和微信做交互,H5
仅仅用来做参数逆向还可以,测试就不行了)
此处以H5
页面的调试为例(目的不是为了逆向参数,主要是使用工具)
dora参数:
1、比较简单,下xhr
断点,一步步跟栈,最终定位到具体加密函数处:
2、进入到函数Object(f.b)
中,可以明确这块请求体位AES
加密:
AES-CBC-pkcs7
填充方式
3、使用在线加解密工具验证一下,成功~
4、不过,这块多次打断点,发现AES
的密钥是随机变化的
5、继续翻代码,最终定位到了AES
密钥生成的位置:
进入到函数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()) }
|
根据经验,基本可以判断处理出来该站点为数字信封
的加密方式
- 前端生成
AES
的随机密钥,加密请求体
- 使用
RSA
或其他非对称加密算法,对随机生成的AES
密钥进行加密
所以,针对另一个参数pan
,猜测大差不差就是RSA
加密了
Pan参数:
1、不用特意去找,pan
参数的生成逻辑就在AES
key的下面
2、进入到函数里面去看,有打印出RSA
的公钥,不出所料就是RSA
加密AES
的密钥了:
RSA
的公钥是给定了的,这个一般情况下也不会变化,主要是看是不是硬编码存储的。这个站点就是本地保存了hex的字节,然后前端生成的(不细究)
3、到这块请求包里面的加密逻辑就清楚了。
响应包:
同理,响应包中也有两个参数dora
和pan
。前端定位到具体位置,下断点,发现其逻辑和请求包类似:
响应数据包逻辑为:
- 使用公钥解密响应包中的
pan
参数,得到服务端加密数据的AES
密钥
- 使用得到的密钥再对响应体数据进行解密
所以,整个系统的加解密流程如下:
请求包
- 生成随机
AES
密钥,加密明文数据
- 使用
RSA
公钥对AES
密钥加密
响应包
- 使用
RSA
公钥对pan
参数解密,得到AES
密钥
- 使用得到的
AES
密钥解密数据包返回前端
mitmproxy
针对于这种请求响应的都加密的情况,数据包流程图如下:
AES-key密钥固定
首先,由于前端的AES-key
是随机生成的,这块要实现整个burp抓包过程中纯明文,就需要对AES-key
进行固定
(也就是要能保证在mitmproxy
一级代理处,能对浏览器发送来的密文数据解密,这样展示在burp中的才会是明文)
密钥固定逻辑就是本地替换JS
文件,一般有以下几种方法:
浏览器Override
yakit
热加载
Charles
的Map local
burp的本地替换
主要逻辑都是修改JS文件
,然后本地加载即可。这里使用Charles进行本地替换。
1、下载原本的JS
文件并修改,将其密钥返回值固定为 1111111111111111
2、打开Charles,将原本的main.js
文件替换我们修改后的文件:
3、访问目标小程序,抓包查看,发现已经成功加载了我们本地修改后的js
文件
4、重新使用burp抓取数据包,请求体内容就可以使用密钥1111111111111111
进行解密了:
4、但查看返回包时却发现产生了报错:
检测到重放攻击,什么鬼????
猜测:大概率为该系统后端对生成的AES-key
做了记录,用来判断一段时间内密钥是否已存在,比如保存在Redis
中。只要设置好AES-key
的过期时间,对一段时间内的密钥进行校验,这样既可以保证系统正常运行,也能避免数据存储过大的问题。
解决办法:在mitmproxy
作为burp
的上游代理时——也就是对请求包数据做再加密时,需要使用随机生成的密钥
- 使用随机生成
AES-key
加密请求头
RSA公钥
加密随机AES-key
mitmproxy代码
该系统的加解密逻辑图如下:
实现mitmproxy一级代理
mitmproxy.exe -p 8888 -m upstream:http://127.0.0.1:8080 -s chrome_burp.py --ssl-insecure -k -q
|
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()
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:]
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) 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的时候,已经成功明文显示了:
实现mitmproxy上游代理
mitmproxy.exe -p 8081 -s burp_server.py
|
- 设置
mitmproxy
为burp
的上游代理,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
def randomA(): def e(): n = int(65536 * (1 + random.random())) return hex(n)[2:][1:]
return e() + e() + e() + e()
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
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:]
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) 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: 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全局明文了: