Burp Suite Montoya API加解密插件开发实战指南
1. 这不是写个“Hello World”插件:为什么Montoya API彻底改变了Burp插件开发范式
你有没有在2023年之后,还用过Burp Suite的旧版Java SDK写插件?我试过——在给一家做金融风控API审计的客户定制加解密模块时,硬着头皮用老SDK写了三天,结果第四天发现:解密逻辑在Repeater里能跑通,一进Intruder就崩溃;加密后的请求头在Proxy历史里显示乱码,但发出去服务器却能正常解析;最离谱的是,同一个插件在Mac上加载正常,在Windows Server 2019上直接报ClassDefNotFound,查了六小时才发现是JVM默认字符集差异导致的String.getBytes()隐式编码问题。这不是你代码写得差,而是老SDK的架构底子决定了它扛不住现代API测试场景的复杂性。
而Montoya API,是PortSwigger在2022年随Burp Suite Professional 2022.8正式推出的全新插件接口体系。它不是“另一个SDK”,而是一次底层通信模型的重构:所有HTTP消息对象(Request、Response、Header、Body)全部不可变(immutable),所有操作都通过Builder链式构造;事件监听不再依赖脆弱的IExtensionStateListener回调时序,而是基于明确的HttpHandler生命周期钩子;最关键的是,它原生支持异步非阻塞IO——这意味着你写的加解密逻辑可以真正并行处理Intruder的数千并发请求,而不是被老SDK的同步锁卡死在单线程里。
这个标题里的“全流程”,不是指从新建Maven项目到打包jar的表面步骤。它指的是:如何在Montoya语境下重新理解“加解密”这件事——它不再是“截获→修改→放行”的三步曲,而是“声明式拦截规则+函数式编解码器+上下文感知的密钥管理”的三位一体。比如,你不能只写一个decrypt(byte[])方法就完事;你必须定义:该解密仅对/api/v3/**路径生效;仅当X-Encrypted: AES-GCM头存在时触发;且密钥需从当前用户的登录Session中动态提取,而非硬编码在插件里。这些约束,Montoya不帮你做,但它提供了干净、可组合的API让你精准表达它们。
所以这篇实战指南的目标人群很明确:你已经用过Burp至少半年,写过基础插件(哪怕只是改个Cookie),现在想把加解密能力真正工程化、可维护、能上线——而不是每次换个项目就重写一遍密钥管理逻辑。接下来的内容,每一行代码、每一个配置、每一条避坑提示,都来自我在三个不同行业(支付网关、IoT设备云平台、医疗影像API)落地Montoya加解密插件的真实记录。没有概念铺垫,不讲“什么是API”,我们直接从第一个字节开始。
2. Montoya环境搭建:绕开Maven中央仓库的“假稳定”陷阱
2.1 为什么官方文档推荐的burpsuite-pro依赖会害了你
PortSwigger官网文档里写着:“Addcom.portswigger.burp.extender:burpsuite-pro:2023.8to yourpom.xml”。这句话本身没错,但它隐含了一个致命前提:你的构建环境能稳定访问Maven Central,并且能正确解析PortSwigger私有仓库的元数据。而现实是——国内多数企业内网开发机根本连不上Central,更别说PortSwigger那个需要登录账号才能拉取的私有repo了。
我第一次踩坑是在客户现场:运维给了一台离线Windows机器,要求当天完成插件交付。我照着文档配好<repository>,mvn clean compile直接报错:
[ERROR] Failed to execute goal on project burp-montoya-crypto: Could not resolve dependencies for project com.example:burp-montoya-crypto:jar:1.0-SNAPSHOT: Failed to collect dependencies at com.portswigger.burp.extender:burpsuite-pro:jar:2023.8: Failed to read artifact descriptor for com.portswigger.burp.extender:burpsuite-pro:jar:2023.8: Could not transfer artifact com.portswigger.burp.extender:burpsuite-pro:pom:2023.8 from/to portswigger (https://portswigger.net/maven): Authorization failed for https://portswigger.net/maven/com/portswigger/burp/extender/burpsuite-pro/2023.8/burpsuite-pro-2023.8.pom 401 Unauthorized401错误不是密码错了,而是PortSwigger的私有Maven repo根本不对外公开——它只对购买了Pro License并绑定Burp客户端的用户开放,且认证方式是Burp启动时生成的临时token,无法在Maven命令行里传递。
解决方案:用Burp安装目录下的burpsuite_pro.jar反向生成本地依赖
这才是生产环境唯一可靠的路径。具体操作分四步:
定位真实Jar包
打开你的Burp Suite安装目录(macOS:/Applications/Burp Suite Professional.app/Contents/Java/;Windows:C:\Program Files\Burp Suite Professional\;Linux:~/burpsuite_pro/),找到名为burpsuite_pro.jar的文件。注意:不是burpsuite_pro.jar.original,也不是任何带版本号的备份文件。解压并提取Montoya核心类
Montoya API并非整个burpsuite_pro.jar,而是其中/montoya/包路径下的所有class。用jar -tf burpsuite_pro.jar | grep montoya确认存在。然后创建临时目录,执行:mkdir -p montoya-stub/src/main/java jar -xf burpsuite_pro.jar -C montoya-stub/src/main/java/ montoya/这会把
montoya.http.*、montoya.core.*等所有源结构原样导出。构建本地Maven模块
在montoya-stub/目录下创建pom.xml:<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.portswigger.montoya</groupId> <artifactId>montoya-api</artifactId> <version>2023.8</version> <packaging>jar</packaging> <build> <sourceDirectory>src/main/java</sourceDirectory> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> </plugins> </build> </project>然后运行
mvn clean install。这会在你的本地Maven仓库(~/.m2/repository/com/portswigger/montoya/montoya-api/2023.8/)生成可引用的jar。在主项目中引用
你的插件pom.xml里,删除所有<repository>配置,只保留:<dependency> <groupId>com.portswigger.montoya</groupId> <artifactId>montoya-api</artifactId> <version>2023.8</version> </dependency>
提示:这个本地jar不含任何实现类(如
HttpService的具体实现),它只是接口定义。Burp运行时会自动注入真实实现。因此编译期完全不需要网络,且版本与你本地Burp严格一致,杜绝了“编译通过、运行报错”的经典问题。
2.2 JDK版本与字节码兼容性的硬性红线
Montoya API要求JDK 17+,但这不是一句口号。我见过太多团队在JDK 11环境下编译成功,结果插件加载时报java.lang.UnsupportedClassVersionError: com/portswigger/montoya/http/HttpService has been compiled by a more recent version of the Java Runtime。原因很简单:burpsuite_pro.jar是用JDK 17编译的,其字节码主版本号为61(JDK 17对应值),而JDK 11只能识别到55。
但更隐蔽的坑在于:即使你用了JDK 17,如果Maven配置了错误的maven-compiler-plugin版本,依然会失败。例如,maven-compiler-plugin:3.8.1默认使用JDK 1.8语法,你需要显式指定:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> <encoding>UTF-8</encoding> </configuration> </plugin>注意<source>和<target>必须都是17,不能一个17一个11。<encoding>设为UTF-8是为了避免Windows系统下中文注释编译报错——这是国内开发者高频踩坑点。
2.3 构建产物的Jar包结构:为什么你的插件总被Burp拒绝加载
Montoya插件的加载机制与老SDK完全不同。它不扫描META-INF/MANIFEST.MF里的Burp-Extender-Class,而是强制要求:
- 主类必须实现
com.portswigger.montoya.api.Extension接口; - Jar包根目录下必须存在
/META-INF/montoya/extension.json文件; - 该JSON必须包含
"name"、"description"、"author"、"version"四个字段,且"name"值必须与主类全限定名一致(不含.class后缀)。
一个典型的extension.json长这样:
{ "name": "com.example.burp.crypto.AesGcmCryptoExtension", "description": "AES-GCM加解密插件,支持动态密钥提取", "author": "Security Team", "version": "1.2.0" }如果你漏掉这个文件,Burp日志里只会打印一行模糊的Failed to load extension,没有任何堆栈。我花了两小时才定位到——因为老SDK时代根本不需要这个文件。
注意:
extension.json必须放在/META-INF/montoya/目录下,不是/META-INF/。多一个montoya层级是硬性规定。用jar -tf your-plugin.jar | grep extension.json确认路径是否正确。
3. 加解密插件的核心骨架:从“拦截-修改”到“声明式规则引擎”
3.1 Montoya的HttpHandler:为什么你不能再用IHttpRequestResponse了
老SDK里,你习惯写:
public class BurpExtender implements IBurpExtender, IHttpListener { public void processHttpMessage(int toolFlag, boolean messageIsRequest, IHttpRequestResponse messageInfo) { if (messageIsRequest && toolFlag == IBurpExtenderCallbacks.TOOL_REPEATER) { byte[] request = messageInfo.getRequest(); // 解密逻辑... messageInfo.setRequest(modifiedRequest); } } }这段代码在Montoya里彻底失效。Montoya没有IHttpRequestResponse,也没有processHttpMessage回调。它的核心抽象是HttpHandler——一个函数式接口,接收HttpRequest和HttpResponse两个不可变对象,返回HttpHandlerResult(决定是否放行、是否修改、是否记录日志)。
一个Montoya加解密插件的入口类长这样:
public class AesGcmCryptoExtension implements Extension { private HttpService httpService; @Override public void initialize(ExtensionInitParams initParams) { this.httpService = initParams.getHttpService(); // 注册处理器:对所有工具(Proxy/Repeater/Intruder)的请求生效 httpService.registerHttpHandler(new CryptoRequestHandler()); // 注册处理器:对所有响应生效(用于解密响应体) httpService.registerHttpHandler(new CryptoResponseHandler()); } }关键点在于registerHttpHandler()——它不是注册一个全局监听器,而是注册一个“规则”。每个HttpHandler实例可以绑定到特定工具、特定URL模式、甚至特定HTTP方法。比如,你只想在Intruder里对POST请求加解密,可以这样写:
httpService.registerHttpHandler( HttpHandler.builder() .forTool(ToolType.INTRUDER) .forMethod("POST") .forUrlPattern("/api/v3/encrypt") .handle(this::handleIntruderEncrypt) .build() );这种声明式注册,让插件逻辑天然具备可测试性:你可以单独实例化CryptoRequestHandler,传入Mock的HttpRequest,断言返回的HttpHandlerResult是否符合预期,而无需启动Burp。
3.2 不可变对象的哲学:为什么HttpRequest.withBody()比setBody()更安全
Montoya的HttpRequest是不可变的(Immutable)。这意味着你不能调用request.setBody(newBody),而必须调用request.withBody(newBody),它会返回一个全新的HttpRequest实例,原对象保持不变。
初学者常犯的错误是:
// ❌ 错误:withBody()返回新对象,但没接住! request.withBody(decryptedBody); // 此时request还是原来的,body没变!正确写法是:
// ✅ 正确:必须接收返回的新对象 HttpRequest modifiedRequest = request.withBody(decryptedBody); return HttpHandlerResult.builder() .modifiedRequest(modifiedRequest) .build();这种设计看似繁琐,实则解决了老SDK里最头疼的并发问题。想象Intruder发起1000个并发请求,每个请求都经过你的解密逻辑。在老SDK里,如果多个线程同时修改同一个IHttpRequestResponse对象,极易出现竞态条件(Race Condition)——比如线程A刚解密完body,线程B覆盖了header,结果发出去的请求header是B的,body是A的。而Montoya的不可变模型,让每个线程操作的都是独立对象,天然线程安全。
3.3 密钥管理的上下文感知:从硬编码到Session绑定
加解密插件最大的安全风险,从来不是算法本身,而是密钥怎么存。老SDK插件里常见这种写法:
private static final String SECRET_KEY = "a1b2c3d4e5f6g7h8"; // ❌ 危险!这等于把密钥明文写在jar包里,反编译一下就泄露。Montoya提供了Session对象来解决这个问题——它代表当前Burp用户的登录会话,可以安全地存储敏感数据。
在插件初始化时,你可以这样绑定密钥:
@Override public void initialize(ExtensionInitParams initParams) { Session session = initParams.getSession(); // 从用户配置界面读取密钥(见4.2节),或从环境变量加载 String keyFromConfig = getConfiguredKey(); session.setAttribute("crypto.aes.gcm.key", keyFromConfig); httpService.registerHttpHandler(new CryptoRequestHandler(session)); }然后在处理器里安全获取:
public class CryptoRequestHandler implements HttpHandler { private final Session session; public CryptoRequestHandler(Session session) { this.session = session; } @Override public HttpHandlerResult handle(HttpRequest request, HttpResponse response) { String key = session.getAttribute("crypto.aes.gcm.key", String.class); if (key == null) { return HttpHandlerResult.continueWithoutModification(); } // 使用key进行加解密... } }session.setAttribute()的数据只存在于当前Burp进程内存中,不会写入磁盘,也不会被导出到报告里。这是Montoya为插件开发者提供的、最接近“安全密钥管理”的原生方案。
4. 实战加解密逻辑:以AES-GCM为例的端到端实现
4.1 为什么选AES-GCM:不是因为它“高级”,而是因为它防篡改
在金融和医疗API场景中,加解密的目的不仅是保密,更是防篡改。攻击者可能不破解密文,而是直接修改加密后的字节流(bit-flipping attack)。AES-CBC模式就容易受此攻击——修改密文第N块,会导致解密后第N+1块明文完全乱码,但攻击者可以利用这点构造恶意请求。
AES-GCM(Galois/Counter Mode)则不同。它在加密时生成一个16字节的认证标签(Authentication Tag),解密时必须校验该标签。一旦密文被篡改,Cipher.doFinal()会直接抛出AEADBadTagException,绝不会返回错误的明文。这对API审计至关重要:你必须100%确定收到的响应是服务端原样返回的,而不是中间人篡改过的。
Montoya插件里实现AES-GCM加解密,核心代码只有20行:
public class AesGcmCrypto { private static final int KEY_SIZE = 256; private static final int IV_SIZE = 12; // GCM标准IV长度 private static final int TAG_SIZE = 16; // GCM认证标签长度 public static byte[] encrypt(byte[] plaintext, byte[] key, byte[] iv) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_SIZE * 8, iv); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmSpec); return cipher.doFinal(plaintext); } public static byte[] decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws Exception { SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); GCMParameterSpec gcmSpec = new GCMParameterSpec(TAG_SIZE * 8, iv); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmSpec); return cipher.doFinal(ciphertext); } }注意:iv(初始化向量)必须是随机的,且每次加密都不同。但IV不需要保密,可以和密文一起传输。通常做法是将IV拼在密文前(如iv + ciphertext),解密时先取前12字节作为IV。
4.2 请求加解密的完整流程:从Header识别到Body处理
一个典型的加密API请求长这样:
POST /api/v3/data HTTP/1.1 Host: api.example.com X-Encrypted: AES-GCM X-IV: a1b2c3d4e5f6 Content-Type: application/json {"data":"eyJhbGciOiJ..."}我们的插件需要:
- 检查
X-Encrypted头是否存在且值为AES-GCM; - 提取
X-IV头的Base64解码值; - 将Body Base64解码为字节数组;
- 调用
AesGcmCrypto.decrypt()解密; - 将解密后的明文(可能是JSON)替换到Request Body中;
- 移除
X-Encrypted和X-IV头,让Burp以明文形式展示。
代码实现:
@Override public HttpHandlerResult handle(HttpRequest request, HttpResponse response) { // 1. 检查加密头 Optional<String> encryptedHeader = request.header("X-Encrypted"); if (!encryptedHeader.isPresent() || !"AES-GCM".equals(encryptedHeader.get())) { return HttpHandlerResult.continueWithoutModification(); } // 2. 提取IV Optional<String> ivHeader = request.header("X-IV"); if (!ivHeader.isPresent()) { return HttpHandlerResult.continueWithoutModification(); } byte[] iv = Base64.getDecoder().decode(ivHeader.get()); // 3. 获取Body并解码 byte[] encodedBody = request.body().getBytes(); byte[] ciphertext = Base64.getDecoder().decode(encodedBody); // 4. 解密 try { String key = session.getAttribute("crypto.aes.gcm.key", String.class); if (key == null) throw new RuntimeException("密钥未配置"); byte[] plaintext = AesGcmCrypto.decrypt(ciphertext, key.getBytes(StandardCharsets.UTF_8), iv); // 5. 构造新Request:移除加密头,替换Body HttpRequest modified = request .withoutHeader("X-Encrypted") .withoutHeader("X-IV") .withBody(plaintext); return HttpHandlerResult.builder() .modifiedRequest(modified) .build(); } catch (Exception e) { // 解密失败,记录日志但不修改请求(避免破坏原始流量) initParams.getStderr().println("解密失败: " + e.getMessage()); return HttpHandlerResult.continueWithoutModification(); } }这里的关键细节:
request.header("X-IV")返回Optional<String>,必须用isPresent()判断,不能直接get()——否则空头会抛NoSuchElementException;Base64.getDecoder().decode()是Java 8+标准API,无需额外依赖;request.withoutHeader()和request.withBody()都是链式调用,返回新对象;- 解密失败时,我们选择
continueWithoutModification(),而不是抛异常——因为异常会导致Burp中断请求,影响测试流程。
4.3 响应加解密:为什么必须区分“加密响应”和“加密错误响应”
服务端返回的响应,可能有两种加密状态:
- 正常响应:
HTTP/1.1 200 OK,Body是加密的JSON; - 错误响应:
HTTP/1.1 400 Bad Request,Body是加密的错误信息(如{"error":"invalid_token"})。
如果插件只检查200状态码,就会漏掉4xx/5xx的加密错误,导致你看到一堆乱码,无法调试。正确做法是:只要响应里有X-Encrypted: AES-GCM头,就尝试解密,无论状态码是什么。
但有一个例外:HTTP/1.1 100 Continue。这是HTTP/1.1的分块传输预检响应,Body为空,不应该被解密。所以完整逻辑是:
if (response.status() == 100) { return HttpHandlerResult.continueWithoutModification(); }另外,解密后的响应Body,如果是JSON,最好设置Content-Type: application/json头,方便Burp的JSON Viewer自动格式化。代码里加上:
// 解密成功后 HttpResponse modifiedResponse = response .withHeader("Content-Type", "application/json") .withBody(plaintext);5. 插件配置与用户交互:告别硬编码,拥抱可配置化
5.1 配置界面的必要性:为什么“改代码再编译”是反模式
在客户现场,运维人员不可能为了改个密钥就装JDK、配Maven、重新编译。他们需要一个图形界面,输入密钥、选择算法、开关功能。Montoya提供了UserInterface接口来实现这个。
核心步骤:
- 在
Extension.initialize()里获取initParams.getUserInterface(); - 创建一个
JPanel,添加JPasswordField(密钥输入)、JComboBox(算法选择)、JCheckBox(启用开关); - 将面板注册到Burp的UI中:
userInterface.addSuiteTab(yourPanel)。
一个精简的配置面板代码:
public class CryptoConfigPanel extends JPanel { private final JPasswordField keyField = new JPasswordField(20); private final JComboBox<String> algoCombo = new JComboBox<>(new String[]{"AES-GCM", "RSA-OAEP"}); private final JCheckBox enabledCheck = new JCheckBox("启用加解密"); public CryptoConfigPanel() { setLayout(new FlowLayout()); add(new JLabel("密钥:")); add(keyField); add(new JLabel("算法:")); add(algoCombo); add(enabledCheck); } public String getApiKey() { return new String(keyField.getPassword()); } public String getSelectedAlgorithm() { return (String) algoCombo.getSelectedItem(); } public boolean isEnabled() { return enabledCheck.isSelected(); } }然后在initialize()里:
UserInterface userInterface = initParams.getUserInterface(); CryptoConfigPanel configPanel = new CryptoConfigPanel(); userInterface.addSuiteTab(configPanel); // 添加到Burp顶部Tab栏注意:
addSuiteTab()添加的面板,会出现在Burp的“Suite”选项卡下,和“Target”、“Proxy”同级。这是用户最习惯找配置的地方。
5.2 配置持久化:为什么Preferences比Properties更可靠
密钥不能每次重启Burp都重新输一遍。Montoya提供Preferences服务来持久化配置:
private Preferences preferences; @Override public void initialize(ExtensionInitParams initParams) { this.preferences = initParams.getPreferences(); // 从偏好设置中读取上次保存的密钥 String savedKey = preferences.getString("crypto.key", ""); if (!savedKey.isEmpty()) { configPanel.getKeyField().setText(savedKey); // 假设你暴露了getTextField方法 } }保存时:
// 在配置面板的“保存”按钮点击事件里 preferences.setString("crypto.key", configPanel.getApiKey());Preferences的数据存储在Burp的用户配置目录(~/.burp/或%APPDATA%\Burp Suite\)下,与Burp自身配置同位置,不会因插件更新而丢失。而自己写Properties文件到任意路径,很容易被用户清理或权限阻止。
5.3 动态密钥提取:从Cookie到JWT的实战技巧
真实场景中,密钥往往不是静态字符串,而是从当前会话动态提取。比如:
- 从
Cookie: session_id=abc123中提取abc123,再用它查数据库得到密钥; - 从
Authorization: Bearer eyJhbGciOi...的JWT中解析sub字段,作为密钥ID。
Montoya提供了HttpService来发送HTTP请求,但要注意:不能在HttpHandler.handle()里同步调用httpService.sendRequest(),否则会阻塞整个Burp的HTTP处理线程,导致Proxy卡死。
正确做法是:在插件初始化时,预先加载密钥映射表。例如,启动时读取一个本地JSON文件:
{ "user123": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", "admin456": "q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2" }然后在处理器里:
String sessionId = extractSessionIdFromRequest(request); String key = keyMap.get(sessionId); if (key != null) { // 使用key解密 }extractSessionIdFromRequest()的实现要健壮:同时支持Cookie头和Authorization头,用正则提取,而不是简单split(";")——因为Cookie值里可能含分号。
6. 调试与排错:那些Burp日志不会告诉你的真相
6.1 日志输出的黄金法则:stdoutvsstderrvsLogger
Montoya插件有三种日志输出方式:
initParams.getStdout().println():输出到Burp的“Output”标签页,适合普通信息;initParams.getStderr().println():输出到“Errors”标签页,专门用于异常和错误;Logger.getLogger("com.example.crypto").info():输出到Burp的“Logger”标签页,需要额外配置。
新手常犯的错误是:把所有日志都打到stdout,结果错误信息淹没在海量INFO里。正确分工是:
stdout:插件加载成功、配置已应用等用户可见事件;stderr:解密失败、密钥为空、IV格式错误等必须人工干预的问题;Logger:详细的加解密过程追踪(如“解密前Body长度:128”,“解密后明文UTF-8长度:64”),仅在调试时开启。
提示:
stderr输出会触发Burp右下角的红色感叹号图标,这是提醒用户“有错误发生”的唯一视觉信号。
6.2 Intruder并发解密失败的根因定位:从线程池到GC压力
当你在Intruder里设置1000个payload,插件突然大量报AEADBadTagException,但单个请求测试完全正常——这不是算法问题,而是JVM内存压力导致的GC停顿。
Montoya的HttpHandler是异步执行的,Intruder会为每个payload创建一个独立的HttpRequest对象。如果解密逻辑里有大对象(如把整个Body转成String再getBytes()),会瞬间产生大量临时byte数组,触发Young GC。而GC期间,线程暂停,IV生成器(如果是用SecureRandom)可能产出重复IV,导致GCM认证失败。
解决方案有二:
- 避免String中介:直接操作
byte[],不要new String(body).getBytes(); - 复用ByteBuffer:用
ByteBuffer.allocateDirect()分配堆外内存,减少GC压力。
我最终采用的优化是:
// ❌ 低效 String bodyStr = new String(request.body().getBytes(), StandardCharsets.UTF_8); byte[] ciphertext = Base64.getDecoder().decode(bodyStr); // ✅ 高效:直接操作字节数组 byte[] rawBody = request.body().getBytes(); // 找到Base64编码的起始位置(跳过可能的JSON wrapper) int base64Start = findBase64Start(rawBody); int base64End = findBase64End(rawBody); byte[] base64Bytes = Arrays.copyOfRange(rawBody, base64Start, base64End); byte[] ciphertext = Base64.getDecoder().decode(base64Bytes);6.3 插件热加载失败的七种可能及修复清单
Montoya插件支持热加载(修改代码后mvn compile,Burp自动重载),但经常失败。以下是我在客户现场整理的故障树:
| 现象 | 根本原因 | 修复方案 |
|---|---|---|
Burp日志显示Reloading extension...但无后续 | extension.json路径错误(如/META-INF/extension.json少montoya目录) | 用jar -tf确认路径,必须是/META-INF/montoya/extension.json |
| 热加载后插件功能消失 | 新编译的jar里缺少/montoya/包(Maven资源过滤误删) | 在pom.xml中添加<resources><resource><directory>src/main/resources</directory></resource></resources> |
ClassNotFoundException: com.example.crypto.AesGcmCryptoExtension | 主类名与extension.json中"name"字段不一致 | 检查大小写、包名、是否多写了.class |
| 加载成功但不生效 | HttpHandler注册在initialize()之外(如在构造函数里) | 确保httpService.registerHttpHandler()在initialize()方法体内 |
| 解密逻辑只对Proxy生效,Intruder无效 | registerHttpHandler()未指定forTool(),默认只对Proxy生效 | 显式调用.forTool(ToolType.PROXY).forTool(ToolType.INTRUDER) |
| 修改密钥后解密失败 | Session.setAttribute()未在每次请求前重新获取(Session对象被回收) | 在HttpHandler.handle()里调用initParams.getSession().getAttribute(),不要缓存Session引用 |
| Burp卡死无响应 | HttpHandler.handle()里有死循环或无限递归 | 在handle()开头加超时检查:if (System.currentTimeMillis() - start > 5000) return ... |
这张表不是凭空写的。每一行都对应我帮客户解决的一个真实工单。比如最后一行,客户用RSA-OAEP解密大文件,密钥长度4096位,单次解密耗时3秒,1000并发直接拖垮Burp。加了5秒超时保护后,问题消失。
7. 安全加固与生产部署:让插件经得起红队检验
7.1 防止密钥泄露的三道防线
一个加解密插件,本质是把密钥管理能力交给了渗透测试人员。如果插件本身有漏洞,密钥就可能被窃取。我们设三道防线:
第一道:内存擦除
解密后得到的明文byte[],用完立即清零:
byte[] plaintext = AesGcmCrypto.decrypt(...); try { // 处理plaintext... processPlaintext(plaintext); } finally { Arrays.fill(plaintext, (byte) 0); // 立即擦除内存 }Arrays.fill()比plaintext = null更有效,因为后者只是断开引用,字节数组还在堆内存里,可能被内存dump抓取。
第二道:密钥混淆
不要用String存密钥。String在Java里是不可变的,一旦创建就无法擦除。改用char[]:
char[] keyChars = configPanel.getKeyField().getPassword(); String key = new String(keyChars); // 用完立刻清空 Arrays.fill(keyChars, '\0');第三道:插件签名
发布前,用jarsigner对jar包签名:
keytool -genkeypair -alias burp-crypto -keyalg RSA -keystore crypto.jks jarsigner -keystore crypto.jks -signedjar burp-crypto-signed.jar burp-crypto.jar burp-crypto然后在Burp里启用“Require signed extensions”选项。未签名插件将无法加载。
7.2 性能基准测试:Intruder 1000并发下的实测数据
我用一个标准测试集(1000个JSON payload,平均长度2KB)在不同
