0x00 前言
本期文章主要是对上期提到的关于数据解密后前端适配的解决方案进行的一些补充。天欣安全实验室的小天师傅在上期留言区提到了双层mitmproxy代理,这种办法其实就很好的解决了前端适配问题,并且拥有了更多的灵活性,在此也感谢下小天师傅的提醒。
注:本篇依然是通过实战案例讲解如何通过双层mitmproxy代理实现自动化加解密。
0x01 分析
今天开头先不进行逆向,先来分析一下双层mitmproxy代理和一层mitmproxy代理之间的区别,以及双层mitmproxy代理到底该如何实现自动化加解密。
在上期文章中我已经分析过如何通过一个mitmproxy代理实现自动化加密/解密,并且当时我是画了一张图的。这种方法虽然能实现,但是还是有一点不足之处:数据适配前端会很麻烦。就拿上期自动化解密案例来说,我最后是通过分析js逻辑,然后修改了脚本才解决的前端适配问题。如果我用双层mitmproxy代理就能很方便的解决这个问题,至于为什么我还是画了一张图:
有读者可能已经发现了双层mitmproxy代理和一层mitmproxy代理之间的区别了。还是拿上期解密案例来说,如果我们要使用双层mitmproxy代理解决前端适配的问题,只需要把它的加密方式分析出来并在本地实现,再通过下游mitmproxy代理加密被解密的数据即可,这样就可以省去上期文章中那段多余的分析js逻辑的过程了,这就是双层mitmproxy代理的方便所在之处。但是这样写的东西就稍微有点多了,从图里就可以看出本期内容会很多。
另外这样做还有一个好处就是:如果请求包中也有数据涉及到加密,我们也可以在下游实现先解密,这样的话在burp中显示的数据全都是解密后的内容,整个过程全都是自动化完成。
注:下文编写脚本的过程都是通过上图图示的过程逐步实现的:分析解密逻辑 --> 下游mitmproxy代理(downstream.py)实现请求体解密 --> 分析加密逻辑 --> 上游mitmproxy代理(upstream.py)实现请求体加密&响应体解密 --> 下游实现响应体加密(downstream.py)
0x02 逆向
直接放图:
post请求,请求头没有加密参数,请求体有两个加密参数:payload、sig,响应体中d为加密参数。通过上文的分析我们已经知道:如果我们要让它们的请求体和响应体数据都为解密后的并展示在burp中,那么就需要先找到它们的解密方法,再谈加密。
可能有读者已经发现了,请求体加密参数payload和响应体d加密参数值有些许相似,所以我们不妨大胆地猜测一下他们的加密和解密方法都是同一个,这样就能省去很多时间。跟下堆栈:
下面这段代码就是对请求体的payload和sig进行了加密操作:
遇到类似这种上面进行了一大串操作的并且下面赋值的属性和请求包/响应包的加密参数一模一样的,基本上就可以判定是这里做的加密/解密操作。打个断点验证一下:
确认为此处进行的加密操作,不过不着急,我们现在的主要目的是先找到解密方法。回过头来看一下这段js:
上面进行完加密操作后,紧接着下面就进行了callback,也就是第一个then,第一个then执行完 return e.json()
后又进行了第二次callback,我们不妨直接看一下第二个then里做了什么:
可能有读者已经发现了,这串代码和上面对payload和sig进行加密操作的那串代码有些许相似,所以基本上可以肯定就是这里进行的解密操作,打个debug验证一下:
确认是这里进行的解密,看一下Object(u.a)(s)解密后是什么:
跟进Object(u.a):
扒到本地:
继续看一下 Object(u.b)(d)
结果:
到这一步才最终解密出明文,跟进Object(u.b):
继续扒到本地:
最后将解密后的字符串转成了对象:
接下来就是对扒到本地的js进行一些修改和补充,当时解密时是先调用的d1,然后又将返回的数据传给了d2,最终才解密完毕。将那段解密代码直接扒下来:
稍微修改一下:
因为我们不需要将它解密后的字符串转成对象,只需要它的字符串即可,所以就直接删去了那一步。将响应体的d扒下来进行解密测试:
运行:
全局查找 _keyStr
:
将这两个变量都扒下来,因为_p在d2中有使用:
运行:
全局查找_u_d
:
扒下来:
运行:
成功解密。
另外提一点,上文我猜测请求体payload和响应体d使用的加密和解密方法都是同一个,现在就拿请求体的payload复制下来解密验证一下:
可见成功解密,说明我们的猜测是正确的。还有一个点就是我们请求体只需要解密payload即可,sig只是一个sign而已,下文会提到。
0x03 下游mitmproxy代理实现解密
从分析中我们可以知道,下游mitmproxy需要编写两个函数,也就是request和response,在request中我们需要把请求数据进行解密并发送到burp中。所以接下来要做的就是先修改一下解密js:
将这串代码封装成一个函数并将解密数据return回去:
downstream.py:
# -*- coding: UTF-8 -*-
# @Project :JS逆向
# @File :downstream.py
# Author :0xsdeo
# Date :2024/10/24 21:27
import json
import execjs
from mitmproxy.http import HTTPFlow
with open("decrypt.js", "r", encoding="utf-8") as f:
js = f.read()
js = execjs.compile(js)
def request(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.request.content.decode())
encrypt_text = data['payload']
# 解密payload
decrypt_text = js.call("decrypt", encrypt_text)
# 将解密后的payload替换掉原先的paylaod
data['payload'] = decrypt_text
flow.request.content = json.dumps(data).encode()
print(flow.request.content.decode())
请求一下看看效果:
成功进行解密。
注:读者可以发现此时payload解密后的字符串和前端加密的那个字符串并不一致:
所以这里留个引子,后文会在上游mitmproxy代理实现加密中提到如何解决。
0x04 加密逆向
下游请求体解密我们已经解决了,但是解密后我们还需要对请求体重新加密,也就是上游mitmproxy该做的事。回头看一下进行了加密操作的那段js:
f赋给了payload,p赋给了sig,其中可以很明显的看到p是将f传给了Object(u.e)进行加密得到的,所以sig只是一个sign而已,不用去解密这个参数,我们的重点是解密payload。打个断点跟一下Object(u.d):
扒到本地:
继续跟一下Object(u.c):
扒到本地:
加密完payload后代码又对payload进行了加密赋给了p,也就是sig,跟一下Object(u.e):
扒到本地:
将那段加密操作也扒到本地:
当时是先调用的e2,然后调用的e1,最后加密f时调用的是sig,简单修改一下并封装成函数:
因为payload加密前的原始状态是一个object:
所以将这个object拿过来进行加密测试:
运行:
全局查找_u_e
:
扒下来:
运行:
将刚刚放进解密js的_p和_keyStr都复制过来:
运行:
sig是通过加密后的payload拼接_p,然后对这个字符串进行了md5加密,再将字符串中所有字母变为大写得到的,我们这里缺的是md5加密方法,不过正好cryptojs有现成的方法,我百度找了一段直接拿过来用了:
const crypto = require('crypto');
function md5(str) {
const hash = crypto.createHash('md5');
hash.update(str);
return hash.digest('hex');
}
运行:
成功加密。
0x05 上游mitmproxy代理实现加密&解密
上游mitmproxy接受到burp发送的请求包后,需要将解密后的payload重新加密,不过在此之前还需要修改一下js,将加密后的payload、sig一起return回去:
这样写是因为js只能return回去一个值:
所以要将payload和sig写到数组里并return回去。
upstream.py:
# -*- coding: UTF-8 -*-
# @Project :JS逆向
# @File :upstream.py
# Author :0xsdeo
# Date :2024/10/25 21:36
import json
import execjs
from mitmproxy.http import HTTPFlow
with open("encrypt.js", "r", encoding="utf-8") as f:
js = f.read()
js = execjs.compile(js)
def request(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.request.content.decode())
# 转换成dict
encrypt_text = json.loads(data['payload'])
decrypt_text = js.call("encrypt", encrypt_text)
# payload
data['payload'] = decrypt_text[0]
# sig
data['sig'] = decrypt_text[1]
flow.request.content = json.dumps(data).encode()
print(flow.request.content.decode())
万事俱备只欠东风,我们先测试一下是否能正常加密解密。
启动下游mitmdump代理命令:
mitmdump -q -p 8888 -s downstream.py --mode upstream:http://127.0.0.1:8080/ --ssl-insecure
下面我来讲一下这两个之前没提到过的mitmdump指令。
--mode upstream:http://127.0.0.1:8080/
:表示将流量转发到指定的上游代理服务器,这里我指定的代理服务器就是burp监听的地址。
--ssl-insecure
:这个指令主要是告诉mitmdump处理https请求时不去验证ssl证书的有效性,我还是强烈建议读者指定下这个指令。
启动上游mitmproxy代理命令:
mitmdump -q -p 8989 -s upstream.py
burp上游服务器代理设置:
请求一下网站看看是否能正常访问:
成功解密。
成功实现下游解密,上游加密请求体。
对比解密后不加密进行请求:
现在我需要特别指出关于在0x03中最后留的引子,解密后的请求体转为json时payload会自动加上转义字符是因为不加就不是一个有效json了,但是我们要让本地加密的数据和前端js加密的数据保持一致。要解决这个问题其实很简单。回过头看一下我上面写的上游脚本,只需要像我这样接受时使用 json.loads()
转换成dict即可:
可见使用 json.loads
后就会将多余的转义字符去除了,此时这个payload就是str了,因为js加密的是object,所以还需要再将这段str转换成dict传给加密js即可。python中的dict实际上就是js中的object,我简单写了一个demo演示一下:
js:
function a(b){
return typeof b
}
py:
import execjs
test = {
"a" : 1
}
with open("ttest.js","r",encoding="utf-8") as f:
js = f.read()
js = execjs.compile(js)
result = js.call("a",test)
print(result)
结果:
接下来要做的就是将响应体的数据进行解密,修改一下上游mitmdump脚本即可。
upstream.py:
# -*- coding: UTF-8 -*-
# @Project :JS逆向
# @File :upstream.py
# Author :0xsdeo
# Date :2024/10/25 21:36
import json
import execjs
from mitmproxy.http import HTTPFlow
with open("encrypt.js", "r", encoding="utf-8") as f:
encrypt_js = f.read()
encrypt_js = execjs.compile(encrypt_js)
with open("decrypt.js", "r", encoding="utf-8") as f:
decrypt_js = f.read()
decrypt_js = execjs.compile(decrypt_js)
def request(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.request.content.decode())
encrypt_text = json.loads(data['payload'])
decrypt_text = encrypt_js.call("encrypt", encrypt_text)
data['payload'] = decrypt_text[0]
data['sig'] = decrypt_text[1]
flow.request.content = json.dumps(data).encode()
print(flow.request.content.decode())
def response(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.response.content.decode())
# get d
encrypt_text = data['d']
# 解密d
decrypt_text = decrypt_js.call('decrypt', encrypt_text)
print(decrypt_text)
data['d'] = decrypt_text
flow.response.content = json.dumps(data, ensure_ascii=False).encode()
效果:
成功实现响应包自动解密并显示在burp中。
0x06 下游mitmproxy代理实现加密
这也是最后一步,将解密后的响应体参数d重新加密,修改下游mitmdump脚本即可。
downstream.py:
# -*- coding: UTF-8 -*-
# @Project :JS逆向
# @File :downstream.py
# Author :0xsdeo
# Date :2024/10/24 21:27
import json
import execjs
from mitmproxy.http import HTTPFlow
with open("encrypt.js", "r", encoding="utf-8") as f:
encrypt_js = f.read()
encrypt_js = execjs.compile(encrypt_js)
with open("decrypt.js", "r", encoding="utf-8") as f:
decrypt_js = f.read()
decrypt_js = execjs.compile(decrypt_js)
def request(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.request.content.decode())
encrypt_text = data['payload']
decrypt_text = decrypt_js.call("decrypt", encrypt_text)
data['payload'] = decrypt_text
flow.request.content = json.dumps(data).encode()
print(flow.request.content.decode())
def response(flow: HTTPFlow):
if "/api2/service/x_service/person_home/list_hot_industries" in flow.request.url:
data = json.loads(flow.response.content.decode())
# encrypt d
encrypt_text = encrypt_js.call('encrypt', json.loads(data['d']))
# get encrypt_d
data['d'] = encrypt_text[0]
flow.response.content = json.dumps(data).encode()
需要注意的是拿到js返回的第一个加密参数payload即可,第二个参数sig就不用拿了。
最终效果:
browser请求:
浏览器正常解析。
对比正常请求解密后转换成对象的m:
一致。
原文始发于微信公众号(听风安全):双层mitmproxy代理实现自动化加解密
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论