1.使用场景
-
想自动修改某一路径或者一定规则下的url的请求包/响应包 (因为burp默认匹配修改所有请求和响应)
-
我burp发的包是明文我想自动对其进行JS加密使其发送到服务器是合法的(或是返回包是加密的我想自动解密使我burp看到的是明文)
-
每次请求需要携带时间戳等动态变化的参数
-
实时保存记录测试的流量
-
提取返回包中的一些信息并保存
-
。。。
2.mitmproxy的简述
2.1 mitmproxy的安装
1.通过官网直接下载
https://mitmproxy.org/downloads/
2.通过pip的国内源来安装mitmproxy(个人推荐,注意python版本至少为3.10)pip install mitmproxy -i https://pypi.tuna.tsinghua.edu.cn/simple
或者使用 pipx来创建隔离环境来安装:
pipx install mitmproxy -i https://pypi.tuna.tsinghua.edu.cn/simple
pip安装好之后会在python目录的Scripts文件夹下出现三个exe文件,我们也可把这三个exe文件复制到其他地方直接运行。
3.Docker、GitHub等其他安装方法请查看官网:https://docs.mitmproxy.org/stable/overview-installation/
2.2 mitmproxy的介绍
mitmproxy是一款支持多协议的抓包工具,我们常说的mitmproxy其实是指的是下面三个工具之一
mitmproxy有三种不同的前端展示:
-
mitmproxy 是一个控制台工具,允许交互式检查和修改 HTTP 流量。它与 mitmdump 的不同之处在于所有流都保存在内存中,这意味着它旨在获取和处理小样本。使用
?
问号键查看上下文相关使用文档。 -
mitmweb 是 mitmproxy 的基于 Web 的用户界面,允许交互式检查和修改 HTTP 流量。与 mitmproxy 一样,所有流都保存在内存中,同样适合小型样本,默认监听
127.0.0.1:8080
端口同时提供一个web
交互界面在127.0.0.1:8081
。 -
mitmdump
mitmdump
是mitmproxy
的一个命令行版本。与mitmproxy
的交互式用户界面不同,mitmdump
提供了一个更像tcpdump
的工具,用于在命令行环境下工作,输入--help查看信息。
以上三个的功能是一致的,只不过是交互界面不同,或者说mitmproxy
是交互式命令行工具,mitmdump
是无交互的命令行工具(只能显示),而 mitmweb
提供了网页前端界面。
2.3 mitmproxy的使用介绍
mitmproxy三个工具都具有如下几项主要的功能。
-
拦截HTTP和HTTPS请求和响应。-
-
保存HTTP会话并进行分析。
-
模拟客户端发起请求,模拟服务端返回响应。
-
支持Mac和Linux上的透明代理。
-
利用Python对HTTP请求和响应进行实时处理。(这一点我们是主要使用的)
-
等等。。
因为这三个工具的命令参数和核心功能几乎都是一样的,所以我们这里主要拿mitmproxy来运行。大多数情况下其他两个工具用起来就直接将命令中的mitmproxy替换为mitmdump或者mitmweb运行即可。
我们在挖掘src或者是渗透测试当中,可以利用mitmproxy+burp+python脚本来对指定路径的响应包进行替换以及对一些sign这些加密参数进行加密或者解密,可以结合JSRPC进行快速的修改请求包
2.4 mitmproxy的常用参数例举
mitmproxy提供了交互式的命令行界面, 早期的 mitmproxy (命令行交互版本)不支持 Windows 系统。从mitmproxy 4.0 版本开始,官方已经增加了对 Windows 的支持,而现在最新的mitmproxy版本则是v11,所以大家放心使用。
我们通过pip安装好之后,直接cmd输入mitmproxy
回车即可运行,不指定端口的话mitmproxy会默认在PC的8080端口运行,然后其会开启一个代理服务,这个服务实际上是一个HTTP/HTTPS的代理,默认打开的运行结果如下是空的,当我们设置了代理之后这个窗口就会有流量数据了。
我们通过命令 mitmproxy -p 9999
来指定监听的端口为9999:
以下是 mitmproxy 的一些参数例举:
-
-p / --port
-
设置代理监听端口(默认
8080
),示例:mitmproxy --port 8888
-
-s / --script
-
加载自定义脚本(Python 脚本),示例:
mitmproxy -s myscript.py
(利用这个功能我们可以自定义脚本实现对请求和响应的任意修改) -
--mode
-
设置代理模式(正向代理、透明代理、上游代理、SOCKS5),示例:
mitmproxy --mode transparent、mitmdump --modeupstream:127.0.0.1:8080
(我们可以开两个mitmproxy,一个作为burp的下游代理一个作为上游代理,这样确保在有加密的情况下burp看到的请求和响应都是明文的) -
--set
-
配置项设置,例如
block_global、upstream_cert
,示例:mitmproxy --set block_global=false
-
-b / --bind-address
-
设置代理监听的地址(默认
127.0.0.1
),示例:mitmproxy --bind-address 0.0.0.0
-
--ssl-insecure
-
禁用 SSL 证书验证,示例:
mitmproxy --ssl-insecure
-
-w / --save-stream
-
保存流量到
.mitm
文件,示例:mitmproxy -w traffic_log.mitm
-
-r / --read-file
-
从
.mitm
文件读取流量记录,示例:mitmproxy -r traffic_log.mitm
-
--anticache
-
禁用客户端请求中的缓存,示例:
mitmproxy --anticache
-
--anticomp
-
禁用请求和响应中的压缩,示例:
mitmproxy --anticomp
-
--ignore
-
忽略特定的主机或路径,示例:
mitmproxy --ignore "example.com"
-
--replace
-
替换请求/响应中的内容,示例:
mitmproxy --replace "/foo/bar/"
-
-q /--quiet
是 mitmdump 的一个命令行选项,意味着它将运行在“安静模式”。在这种模式下,mitmdump 会减少输出,通常只显示我们自定义的输出或者一些错误和关键信息,而不会显示常规的流量信息。
2.5 证书的快速安装
当我们pip安装之后会在本地的目录C:Users本机电脑用户名.mitmproxy
下面生成一些证书:
Windows安装后缀为.p12的证书即可。
当浏览器挂上mitmproxy代理之后,我们可以通过访问 http://mitm.it
来检查网络流量是否经过 mitmproxy。如果配置正确,该页面正常会显示一个简单的界面(如下图),用于安装 mitmproxy 的证书颁发机构(CA)来实现对https请求的抓包(本质上还是使用了中间人攻击的原理),安装类似于Burpsuite,我们把它放到受信任的根证书颁发机构即可。
更多关于证书的说明我们可以阅读官网:https://docs.mitmproxy.org/stable/concepts-certificates/
安装好证书之后我们就可以成功的抓包https的流量数据了,我们使用百度搜索的情况如下所示:
我们可以看到使用mitmproxy工具后,流量包下方有一些交互的命令,我们可以通过鼠标滚轮选择某个请求,然后对应下方的一些交互操作,操作都比较简单,大家上手试试就明白了,况且我们使用其交互功能的情况也不多,这里就不再赘述了。
2.6 mitmproxy的插件机制
mitmproxy的插件(addons)其实就是我们使用python实现对请求和响应操作的功能,需要我们有一定的python编程基础,当然大家gpt写也可以。
通过-s来指定我们的插件脚本,使用命令为:mitmproxy -s xxx.py
插件机制包含了一些钩子事件(回调函数,意思是特定场景下会自动触发的函数):生命周期事件、 连接事件 、HTTP 事件(用的较多)、 WebSocket 事件等,这些事件都是回调函数(或称为“钩子函数”其函数名称是固定的),大多事件(例如http事件)接收 Flow
对象作为参数,插件则可以通过修改Flow
对象来实现实时更改流量。 当你在 HTTP 事件的上下文中使用 flow
时,mitmproxy 会自动将 flow
的类型确定为 HTTPFlow
,其他的事件类似。
官方文档中给我们详细的介绍了这些事件:https://docs.mitmproxy.org/stable/api/events.html
(有个小技巧就是插件的代码可以实时修改,修改完插件代码之后不用重新连接mitmproxy会重新读取。)
后文我们会重点说一下http回调函数的插件编写:
mitmproxy的插件机制允许我们一次性加载多个插件(每个插件都是一个类),加载方法为:
addons = ['插件1','插件2'...]
插件加载放到脚本最后即可。
首先作者不推荐直接写钩子方法,虽然快但是可复用性和拓展性都不好,一个简单的处理每次请求和响应的脚本如下:
from mitmproxy import http
# 处理所有发出的请求数据包
def request(flow: http.HTTPFlow) -> None:
# 获取请求对象
request = flow.request
# 打印请求的 URL 和请求头
print(f"Request URL: {request.url}")
# 处理所有服务器响应的数据包
def response(flow: http.HTTPFlow) -> None:
# 获取响应对象
response = flow.response
# 打印响应状态码和响应头
print(f"Response Status Code: {response.status_code}")
我们最好通过类的形式来编写插件来实现我们想要的功能,以上的功能用插件的形式如下:
from mitmproxy import http
class MyPlugin1:
def request(self, flow: http.HTTPFlow) -> None:
# 获取请求对象
request = flow.request
# 打印请求的 URL 和请求头
print(f"Request URL: {request.url}")
def response(self, flow: http.HTTPFlow) -> None:
# 获取响应对象
response = flow.response
# 打印响应状态码和响应头
print(f"Response Status Code: {response.status_code}")
# 创建插件实例,这里可以添加多个
addons = [
MyPlugin1()
]
我们最好用python中的类型注释来书写,有的师傅可能有疑问为什么这里是flow.request,其实是因为mitmproxy的钩子函数调用的时候会自动传递对应事件类型的flow对象,所以我们直接访问其属性即可。我们将上方的插件代码命名为 mitm.py 后保存,运行命令:mitmdump -q -p 9091 -s mitm.py
(因为mitmproxy无法完全禁止流量输出,为了效果更明显我们用mitmdump,参数命令都是一样的)
mitmdump
的常用运行方式(mitmproxy同理):
mitmdump -p 9091 -s ./xxx.py --ssl-insecure -w ./xxx.log -q
解释:
-
-p 9091
: 指定监听的端口为9091
。所有通过这个端口的流量都会被拦截和记录。 -
-s ./xxx.py
: 运行当前目录下的xxx.py
脚本。这个脚本可以用来处理和修改流量,例如拦截请求、修改响应等。 -
--ssl-insecure
: 忽略 SSL 证书验证。如果目标服务器使用的证书是自签名或无效的,这个选项允许 mitmdump 拦截 HTTPS 流量而不报错。 -
-w ./traffic.log
: 将所有拦截到的流量数据保存到当前目录下的xxx.log
文件中。 -
-q
: 不输出流量数据,只输出自定义的输出和一些报错等关键信息
2.7 HTTP事件的详解
如果我们想要更多的对请求和响应进行自定义就需要了解其HTTP事件的一些类的属性。
下文会列出一些常用属性,更多信息推荐大家多阅读官方文档:https://docs.mitmproxy.org/stable/api/mitmproxy/http.html
2.7.1 HTTPFlow类
HTTPFlow 是一个表示单个 HTTP 事务的对象集合,其中包含的属性如下:
-
request: (Request) 客户端的 HTTP 请求对象。这个对象包含了请求的方法、URL、头部、内容等信息。
-
response: (Response 或 None) 服务器的 HTTP 响应对象。如果服务器还没有发送响应,或者请求还没有被发送到服务器,这个属性可能是 None。
-
error、websocket、timestamp_start、timestamp_end
我们用的最多的就是request和response了,其他的如果大家有需要请查看官网。
2.7.2 request类
以下是 http.HTTPFlow.request
类的属性和描述
属性 |
类型 |
描述 |
|
|
请求的完整 URL,包括协议、主机、路径和查询参数。 |
|
|
HTTP 请求的方法,例如 |
|
|
请求头,包含所有 HTTP 头部信息。 |
|
|
请求的 Cookie 字典。 |
|
|
请求的内容,通常是请求体的字节表示。 |
|
|
请求的主机名,不包括协议或路径部分。 |
|
|
请求的端口号,通常是 |
|
|
请求的路径部分,不包括查询字符串。 |
|
|
请求的查询字符串部分。 |
2.7.3 response类
http.HTTPFlow.response
类的属性表格如下:
属性名称 |
类型 |
描述 |
|
|
HTTP 响应状态码(如 200, 404 等) |
|
|
状态码对应的描述(如 "OK", "Not Found" 等) |
|
|
响应头信息 |
|
|
HTTP 版本(如 "HTTP/1.1") |
|
|
响应体内容(以字节形式表示) |
|
|
响应中的 cookies |
|
|
解码后的文本响应体 |
|
|
可读的响应头信息 |
|
|
原始响应体内容 |
我们从官网可找到更多的插件例子供我们学习:https://docs.mitmproxy.org/stable/addons-examples/
3.实战案例
我们拿某妈妈查询接口返回做例子,同时会利用上篇文章讲的JSRPC技术进行返回值解密。
urlaHR0cHM6Ly93d3cuY2hhbm1hbWEuY29tL3Byb21vdGlvblJhbmsvdGlrR29vZHNTYWxlLz9jYXRlZ29yeV9pZD0tMQ==
我们登录后,查看接口的返回信息如下:
可以看到返回的data被加密了,data实在太长了,这次小天就懒得再猜了,那我们就老老实实看看是如何解码的吧~
3.1 data参数分析
首先data这个参数实在没什么代表性,就不用搜索data关键字了,我们可以搜索一下decrypt这个解密的关键字(关于返回包加密我们可以搜索的关键字有:interceptors.response、response、interceptors、decrypt、json.parse、url路径、返回包的特征字段等等),当然实在搜索不到,理论上下xhr断点后跟堆栈也是可以的(可能会比较麻烦)
我们全局搜索 decrpyt(
可以看到再app.xxxxxxxx.js
里面有一个AES解密的函数调用像这种app后面接哈希值的js文件一般就是经过打包工具生成的(例如:Webpack、Vite 等)其中都是核心代码居多,我们优先去看这类的js,我们跟进去看看。
为了确认这一块是不是返回包data数据的解密操作,我们下断点进行调试一下:
运行到r的值生成之后,我们浏览器控制台输入r,这时候可以看到解密后的结果::
输入e则是解密前的data,所以可以确定了返回包参数data的解密逻辑是先用AES解密然后使用ungzip方法进行解压的,至于ungzip则是一个pako库的方法,我们可以遇到这种先百度一下,因为引用库的方法要比自己扣代码方便的多(虽然本文不用扣代码实现。)
那我们话不多说,利用JSRPC来实现一下这个解密,省的扣代码了嘿嘿,如果大家对JSRPC有不了解的可以去看小天的之前的文章:
3.2 JSRPC实现data解密
我们使用工具JsRpc来实现这个功能,当然大家如果用Sekiro也可以。
我们先执行浏览器客户端的初始化,将客户端代码插入控制台后回车:
然后我们将断点打在r的生成位置后,点击选项触发断点,随后在控制台输入下方代码将方法提升到全局(当然大家可以使用本地替换或者是浏览器插件:reres或者Netify也可实现)
window.getdata=function getdata(param){
return c.ungzip(u(s.AES.decrypt(param, n, {
mode: s.mode.ECB,
padding: s.pad.Pkcs7
})),{
to: "string"
})
}
这时候注意要 放开断点! 放开断点!放开断点!(重要的话说三遍),继续运行之后我们控制台再次打入window.getdata
来确认一下是否成功提升到全局变量,可以看到再放开断点继续运行后,我们成功将完整的解密函数赋给了全局的getdata方法。
这时候我们在注册jsrpc接口之前需要打开一下JsRpc的服务端:
这时候我们再向控制台插入以下代码注册接口以供后续调用,可以看到返回了true:
var demo = new Hlclient("ws://127.0.0.1:12080/ws?group=cmm");
demo.regAction("getdata", function (resolve,param) {
resolve(window.getdata(param));
})
同时JsRpc的服务端提示了已上线,这时候我们可以调用接口了。
我们先写一个python脚本发送一个携带加密数据的post请求验证一下:
import requests
url = 'http://127.0.0.1:12080/go'
data = {'param': '加密的数据','group':'cmm','action':'getdata'} # 替换为你要传递的data
response = requests.post(url, json=data)
if response.status_code == 200:
print('成功:', response.json()) # 打印返回的 JSON 数据
else:
print('请求失败:', response.status_code, response.text)
运行结果如下,可以清楚的看到解密了请求数据。
4.Mitmproxy与Burpsuite联动
下一步我们想要让burpsuite中也能看到解密后的明文,所以我们在这时候可以将Mitmproxy设置为Burpsuite的上游代理(上游代理可以简单理解为离服务器近的那个,如图)然后对每次查询接口的响应做JsRpc解密后的替换,这样BurpSuite上看到的就是明文了(但是这样会有一个问题,后文会说)。
文末会给出小天写的修改请求和响应的插件模板,大家可以直接拿来用即可,使用案例如下:
from mitmproxy import http
import requests
import json
"mitmdump -p 9091 -s mitm.py --ssl-insecure -q"
class ModifyResponse:
def __init__(self):
# 定义要修改的 URL 及对应的请求修改规则(url为部分匹配,自定义处理函数要返回request)
self.request_rules = {
}
# 定义要修改的 URL 及对应的响应修改规则(url为部分匹配,自定义处理函数要返回response)
self.response_rules = {
'https://api-service.chanmama.com/v6/home/rank/yesterdaySaleRank':self.decrypt
}
# 自定义的修改对应路径的返回包的函数
def decrypt(self, response):
if not response.content.strip(): # 如果为空或仅包含空格,跳过处理
return response # 返回原始响应或其他逻辑处理
first_json=json.loads(response.content)
data=first_json['data']['data']
print(data)
url = 'http://127.0.0.1:12080/go'
data = {'param': data, 'group': 'cmm', 'action': 'getdata'} # 替换为你要传递的data
decrypt_data = requests.post(url, json=data).json()['data'] # 将响应转化为字典
# 这里的decrypt_data需要用json.loads将其转化为字典
first_json['data']['data'] = json.loads(decrypt_data)
# 将修改后的数据转换为 JSON 字符串并更新响应内容,ensure_ascii=False 为了使得json中出现中文
response.content =response.content = json.dumps(first_json, ensure_ascii=False).encode('utf-8', errors='ignore')
print(response.content)
return response
def all_response(self, response):
"""可自定义修改全部的response"""
return response
def all_request(self, request):
"""可自定义修改全部的resquest"""
return request
# 下面的不用修改
def response(self, flow: http.HTTPFlow) -> None:
"""处理响应,依据规则修改响应数据"""
request_url = flow.request.pretty_url # 获取完整响应的 URL
# 找到匹配的规则,并调用对应的修改函数
for rule, modify_func in self.response_rules.items():
if rule in request_url:
# 直接获取原始响应文本
original_response = flow.response
modified_response = modify_func(original_response) # 调用对应的修改函数
flow.response = self.all_response(modified_response) # 更新响应内容
break # 一旦找到匹配规则就退出循环
def request(self, flow: http.HTTPFlow) -> None:
"""处理请求,依据规则修改请求数据"""
request_url = flow.request.pretty_url # 获取完整请求 URL
# 找到匹配的规则,并调用对应的修改函数
for rule, modify_func in self.request_rules.items():
if rule in request_url:
# 直接修改请求内容,不需要解析
original_request = flow.request
modified_request = modify_func(original_request) # 调用对应的修改函数
flow.request = self.all_request(modified_request) # 更新请求内容
break # 一旦找到匹配规则就退出循环
# 加载插件
addons = [
ModifyResponse()
]
我们在burpsuite配置上游代理:
对接口进行重发测试,可以看到接口返回了解密后的明文,实现了自动化解密。
这时候细心的师傅可能发现了,这样操作因为我们修改了正常的返回包的信息,会使得浏览器中的页面无法正确解析数据,如下:
这时候解决的方法有以下几种:
-
修改返回包的加密标识
-
利用Mitmproxy双层代理
-
修改前端的JS文件(本地替换、插件、中间人等等)这个不太推荐
这个正好有一个参数is_encrypt
来向前端标识返回的数据是否是加密的,在这里我们将其删除,然后使其最外层的data等于我们解密后的数据, 其实就可以让前端解析成功数据,至于mitmproxy双层代理我们下一篇文章会详细和大家介绍。
我们将上方写法中的修改响应的函数改为这个样子:
def decrypt(self, response):
# print(response.text)
# 检查内容是否为空或只有空格等无效数据
if not response.content.strip(): # 如果为空或仅包含空格,跳过处理
return response # 返回原始响应或其他逻辑处理
first_json=json.loads(response.text)
data=first_json['data']['data']
url = 'http://127.0.0.1:12080/go'
data = {'param': data, 'group': 'cmm', 'action': 'getdata'} # 替换为你要传递的data
decrypt_data = requests.post(url, json=data).json()['data']
# 注意这里的json.loads(decrypt_data) 是将decrypt_data的字符串类型转化为字典
first_json['data']= json.loads(decrypt_data) #直接让data字段为解密后的数据即可
# 将修改后的数据转换为 JSON 字符串并更新响应内容
response.content = json.dumps(first_json, ensure_ascii=False).encode('utf-8', errors='ignore')
return response
然后就大功告成,成功使我们的burp看到的是明文,同时浏览器也可以正常响应啦,如下:
5.Mitmproxy插件模板:
from mitmproxy import http
import json
"mitmdump -p 9091 -s xxx.py --ssl-insecure -q"
class ModifyResponse:
def __init__(self):
# 定义要修改的 URL 及对应的请求修改规则(url为部分匹配)
self.request_rules = {
}
# 定义要修改的 URL 及对应的响应修改规则(url为部分匹配)
self.response_rules = {
}
def all_response(self,response):
"""修改全部的response"""
return response
def all_request(self,request):
"""修改全部的resquest"""
return request
# 下面的一般不用修改
def response(self, flow: http.HTTPFlow) -> None:
"""处理响应,依据规则修改响应数据"""
request_url = flow.request.pretty_url # 获取完整请求 URL
# 找到匹配的规则,并调用对应的修改函数
for rule, modify_func in self.response_rules.items():
if rule in request_url:
# 直接获取原始响应文本
original_response = flow.response
modified_response = modify_func(original_response) # 调用对应的修改函数
flow.response = self.all_response(modified_response) # 更新响应内容
break # 一旦找到匹配规则就退出循环
def request(self, flow: http.HTTPFlow) -> None:
"""处理请求,依据规则修改请求数据"""
request_url = flow.request.pretty_url # 获取完整请求 URL
# 找到匹配的规则,并调用对应的修改函数
for rule, modify_func in self.request_rules.items():
if rule in request_url:
# 直接修改请求内容,不需要解析
original_request = flow.request
modified_request = modify_func(original_request) # 调用对应的修改函数
flow.request = self.all_request(modified_request) # 更新请求内容
break # 一旦找到匹配规则就退出循环
# 加载插件
addons = [
ModifyResponse()
]
原文始发于微信公众号(安全君呀):【JS逆向渗透系列02】BurpSuite结合mitmproxy+JSRPC秒杀js加密的方案
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论