先说结论:给BurpSuite插件加一个“未授权访问请求”功能,本质上就是把[[手动测试中重复了无数次的“直接请求目标接口,看它到底要不要身份验证”这个动作,变成一个一键触发、自动审计的能力。
我在给某内部安全工具写扩展的时候,最深的感触是:未授权访问这个漏洞形态,单看危害评级可能不算高,但一旦目标系统存在大量内部接口、管理后台或者调试路由,它的实际风险会呈指数级上升。而且,测试这类漏洞的方法论其实特别固定——无非就是去掉Cookie、去掉Authorization头、改成直接访问静态资源路径或API端点,然后观察响应差异。这个逻辑完全适合固化到插件里。
这篇文章不打算从头教你怎么搭插件工程,而是围绕“添加未授权访问请求功能”这件事,把需求拆解、API选型、代码实现、调试技巧和踩坑记录完整走一遍。如果你已经在用Java写Burp插件,或者正打算把你的手工流程脚本化,这篇应该能给你一份可以直接抄作业的参考。
1. 未授权访问检测,到底在测什么
先说清楚这个功能的检测目标,不然代码写到一半容易失控。
未授权访问,通俗讲就是:一个接口本来应该要有登录态才能访问,但由于配置疏忽、鉴权中间件漏配、或者接口设计得太多太杂导致白名单覆盖过广,结果攻击者不带任何凭证直接打过去,服务器居然正常返回了数据。典型的场景包括:
- 管理后台路径直接可访问,比如
/admin、/system、/console; - API接口不校验身份,比如
/api/v1/user/info直接返回用户详情; - 内部服务暴露到公网,比如
/actuator、/druid、/swagger-ui.html; - 越权场景变种:带低权限的Cookie访问高权限接口(这个严格说叫越权,但很多扫描器会把这类响应差异也归类到未授权)。
测试手法上,最基础也最有效的就是“剥壳法”——把请求里跟身份认证相关的部分全部剥掉,再重新发一次,看响应码、响应体长度、关键返回字段是否跟原请求一致。
这里有个细节值得注意:如果未授权请求返回了401或302重定向,那说明鉴权是生效的,不算漏洞;如果返回200或者只做了很弱的拦截(比如前端跳转但后端数据照样返回),那就要深入看了。
我在给某跨平台系统做测试的时候,遇到过一种情况:前端路由做了登录拦截,但后端接口并没有校验,直接访问API路径返回了完整的业务数据。这种问题用浏览器手工测很容易漏,因为你在浏览器里访问接口时,浏览器会自动带上当前站点的Cookie,你根本意识不到“哦原来不加Cookie也能通”。所以把这个测试动作放到插件里自动执行,价值就在这里——它能剥掉所有环境自带的认证因素,纯粹地探测一次。
另外要说清楚:未授权访问检测跟普通的漏洞扫描不一样,它的重点是“响应对比”,不是“漏洞特征匹配”。普通的扫描器是拿着已知漏洞的签名去匹配响应内容,而未授权检测更像是一种行为审计——看的是接口在不同认证状态下的表现差异。
2. BurpSuite扩展API选型:IScannerCheck还是自定义右键菜单
做Burp插件开发,最核心的就是选对扩展点。加未授权访问请求功能,无非两条路:一条是主动扫描(挂到主动扫描器里),另一条是手动触发(通过右键菜单/自定义UI)。
2.1 主动扫描:适合批量资产检测
如果你希望这个功能在爬取到大量请求之后自动跑一遍,那应该实现IScannerCheck接口。这样Burp在主动扫描阶段就会把每个请求交给你注册的Scanner插件,开发者可以在doPassiveScan或doActiveScan里做检测逻辑。
主动扫描的优势是覆盖面广,爬虫抓到的所有请求都能过一遍筛子。但问题也很明显——误报率会比较高。因为爬虫抓到的请求五花八门,有很多压根就不是业务接口,比如静态资源、图片、第三方回调地址,这些路径不存在鉴权需求,你不带Cookie访问返回200是正常的,拿这个报漏洞那就闹笑话了。
所以如果要走IScannerCheck路线,必须加过滤规则:只对包含API特征、内含参数、返回JSON或XML的请求做检测,静态资源和纯文件下载路径直接跳过。
2.2 右键菜单:适合定向验证和渗透场景
我最终选择的是右键菜单方式,也就是实现IContextMenuFactory接口,把“未授权访问请求”做成一个右键菜单项,选中某个请求后点击触发。
这个选择的原因很实际:
- 渗透测试场景下,你关注的目标通常已经缩小到一个或几个站点,没必要全站批量扫;
- 右键菜单能保留当前请求的所有上下文信息(URL、参数、Headers),方便做精确的“剥壳”处理;
- 单个请求的检测结果可以直接展示在弹窗或Output里,调试起来直观得多。
插入点理解起来也很简单:BurpSuite的任何位置(Proxy、Repeater、HTTP History)右键菜单里都有Extensions这个子菜单,你实现IContextMenuFactory之后,返回的JMenuItem会自动挂到这个子菜单里。用户操作路径就是:右键 → Extensions → 你定义的菜单项 → 触发检测。
2.3 两个接口都实现,其实也不冲突
如果想把两个能力都占了,完全可以在同一个插件里同时实现IScannerCheck和IContextMenuFactory。我的做法是:
- 右键菜单里放“单个请求未授权检测”,用于手工定向测试;
- 主动扫描的
doActiveScan里放“批量对比检测”,但加了一个开关选项,默认不开启,避免误报刷屏。
3. 代码结构设计与核心实现
这个插件的工程结构我建议这样组织,后面扩展起来会省很多事:
src/ ├── BurpExtender.java // 入口,注册各类扩展点 ├── core/ │ ├── UnauthorizedAccessChecker.java // 核心检测逻辑 │ ├── HttpRequestBuilder.java // 请求构造器:剥壳/重放 │ └── ResponseComparator.java // 响应对比器 └── ui/ └── ContextMenuProvider.java // 右键菜单实现3.1 入口类注册扩展点
在BurpExtender里只需要做一件事:把菜单工厂和扫描器插件注册进去。
package burp; public class BurpExtender implements IBurpExtender, IContextMenuFactory, IScannerCheck { private static final String EXTENSION_NAME = "Unauthorized Access Checker"; private IExtensionHelpers helpers; private IBurpExtenderCallbacks callbacks; @Override public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { this.callbacks = callbacks; this.helpers = callbacks.getHelpers(); callbacks.setExtensionName(EXTENSION_NAME); // 注册右键菜单 callbacks.registerContextMenuFactory(this); // 注册主动扫描器 callbacks.registerScannerCheck(this); callbacks.printOutput("[*] Unauthorized Access Checker loaded."); } @Override public List<JMenuItem> createMenuItems(IContextMenuInvocation invocation) { // 只在有请求数据时显示菜单 IHttpRequestResponse[] messages = invocation.getSelectedMessages(); if (messages == null || messages.length == 0) { return null; } JMenuItem menuItem = new JMenuItem("Send Unauthorized Request"); menuItem.addActionListener(e -> { for (IHttpRequestResponse message : messages) { performUnauthorizedCheck(message); } }); List<JMenuItem> menuItems = new ArrayList<>(); menuItems.add(menuItem); return menuItems; } private void performUnauthorizedCheck(IHttpRequestResponse message) { // 交给核心检测器去执行 UnauthorizedAccessChecker checker = new UnauthorizedAccessChecker(callbacks, helpers); checker.check(message); } // IScannerCheck 相关方法省略,后面单独讲 }这段代码有几个值得注意的地方。IContextMenuInvocation.getSelectedMessages()返回的是当前选中的请求信息,可能是一个也可能是多个。这里我做了循环处理,意味着你可以在HTTP History里多选几条请求,一次性批量检测。这个交互细节很实用,手工测试时经常想对某个接口的几个变体请求同时验证。
3.2 请求构造器:如何干净地“剥掉”身份信息
未授权访问检测的第一步,是把原始请求里的身份凭证全部去掉。身份凭证存在的形式主要有四种:
- Cookie头(最常见)
- Authorization头(Bearer Token、Basic Auth)
- 自定义认证头(如 X-Token、Token、session-id 等)
- 请求体中携带的认证参数(相对少见,但存在)
public class HttpRequestBuilder { private IExtensionHelpers helpers; // 需要剥离的敏感头列表 private static final String[] SENSITIVE_HEADERS = { "Cookie", "Authorization", "X-Token", "X-Api-Key", "Token", "Session-ID", "X-Requested-With" // 某些框架会用它做CSRF判断,未授权访问检测时也可以去掉 }; public IHttpRequestResponse buildUnauthorizedRequest(IHttpRequestResponse originalRequest) { IHttpRequestResponse unauthorizedResponse = new MockHttpRequestResponse(); // 1. 解析原始请求 byte[] requestBytes = originalRequest.getRequest(); IRequestInfo requestInfo = helpers.analyzeRequest(originalRequest); // 2. 获取原始header列表 List<String> headers = requestInfo.getHeaders(); // 3. 过滤掉敏感header List<String> sanitizedHeaders = new ArrayList<>(); for (String header : headers) { String headerName = header.split(":")[0].trim(); if (!isSensitiveHeader(headerName)) { // 过滤掉cookie等身份标识 sanitizedHeaders.add(header); } } // 4. 如果有请求体,保留请求体(POST参数一般不剥,因为可能是业务参数) byte[] body = Arrays.copyOfRange(requestBytes, requestInfo.getBodyOffset(), requestBytes.length); // 5. 构建新请求 byte[] unauthRequest = helpers.buildHttpMessage(sanitizedHeaders, body); // 6. 发送请求 IHttpService httpService = originalRequest.getHttpService(); return originalRequest.getHttpService() != null ? callbacks.makeHttpRequest(httpService, unauthRequest) : null; } private boolean isSensitiveHeader(String headerName) { for (String sensitive : SENSITIVE_HEADERS) { if (sensitive.equalsIgnoreCase(headerName)) { return true; } } return false; } }这里有个很重要的设计决策:请求体里的业务参数要不要剥?
我的答案是不要剥。原因是很多API的请求体参数本身就属于业务逻辑的一部分,比如登录接口的username/password字段、查询接口的keyword字段,这些参数是接口正常工作需要的输入,不属于身份认证范畴。如果你把请求体整个去掉,那测出来的就不是“未授权访问”,而是“缺少参数的异常请求”,响应码大概率是400或422,全无参考价值。
但是有一种情况例外:如果请求体里出现了token、sessionId之类的字段,且你能明确判断它是用于身份校验的,那也可以剥掉。这个我建议做成可配置项,默认关掉,避免误伤业务参数。
3.3 核心检测器:响应对比与漏洞判定
请求发出去了,下一步就是对比响应。核心思路是:如果去掉身份凭证后的请求,跟原始请求的响应基本一致,那就大概率存在未授权访问问题。
public class UnauthorizedAccessChecker { private IBurpExtenderCallbacks callbacks; private IExtensionHelpers helpers; private static final int THRESHOLD_SCORE = 80; // 相似度阈值 public void check(IHttpRequestResponse originalMessage) { // 1. 构建未授权请求并发送 HttpRequestBuilder builder = new HttpRequestBuilder(helpers, callbacks); IHttpRequestResponse unauthorizedResponse = builder.buildUnauthorizedRequest(originalMessage); if (unauthorizedResponse == null) { callbacks.printOutput("[!] Request failed!"); return; } // 2. 分别解析两个响应 IResponseInfo originalResp = helpers.analyzeResponse(originalMessage.getResponse()); IResponseInfo unauthorizedResp = helpers.analyzeResponse(unauthorizedResponse.getResponse()); int originalStatus = originalResp.getStatusCode(); int unauthorizedStatus = unauthorizedResp.getStatusCode(); byte[] originalBody = getResponseBody(originalMessage.getResponse()); byte[] unauthorizedBody = getResponseBody(unauthorizedResponse.getResponse()); // 3. 判定逻辑 boolean isVulnerable = false; String reason = ""; // 情形一:未授权请求返回200或业务状态码,且响应体相似度高 if (unauthorizedStatus == 200 || unauthorizedStatus == 201) { double similarity = ResponseComparator.computeSimilarity(originalBody, unauthorizedBody); if (similarity >= THRESHOLD_SCORE) { isVulnerable = true; reason = String.format("Unauthorized request returns %d, response similarity: %.2f%%", unauthorizedStatus, similarity); } } // 情形二:两边响应码不同,但未授权响应暴露了敏感信息 // 比如原始请求200,未授权请求302重定向到登录页,这种情况不算漏洞 // 但未授权请求返回403却带着管理页面内容,这种要人工确认 // 4. 输出结果 if (isVulnerable) { callbacks.printOutput("[+] Potential Unauthorized Access Found!"); callbacks.printOutput(" URL: " + getURL(originalMessage)); callbacks.printOutput(" Reason: " + reason); } else { callbacks.printOutput("[-] No unauthorized access detected."); } } }3.4 响应相似度计算:别用Hash对比
在写响应对比器时,最容易犯的错就是用哈希值做严格对比。实际上未授权请求跟原始请求的响应不可能一模一样,差别主要来自这么几个地方:
- 未授权请求没有Cookie,服务器可能不会返回个性化的用户信息;
- 某些动态页面包含时间戳、随机数、CSRF Token,两次请求内容天然不同;
- 有的服务器对未认证请求会返回一个简化版的响应头,比如去掉Set-Cookie。
所以相似度算法要保留一定的弹性空间。我实际用的是“归一化后的文本相似度”方案:提取响应体,去掉所有HTML标签、空白字符、时间戳类动态字段,然后按行做包含度匹配。
public class ResponseComparator { // 计算响应体的相似度,归一化后做文本包含匹配 public static double computeSimilarity(byte[] originalBody, byte[] unauthorizedBody) { if (originalBody == null || unauthorizedBody == null) { return 0.0; } String originalStr = normalize(new String(originalBody)); String unauthorizedStr = normalize(new String(unauthorizedBody)); if (originalStr.isEmpty() || unauthorizedStr.isEmpty()) { return 0.0; } // 如果长度差异过大,直接认为不相似 if (originalStr.length() > unauthorizedStr.length() * 3) { return 0.0; } // 统计原始响应中出现在未授权响应中的行数 String[] originalLines = originalStr.split("\n"); int matchedLines = 0; int totalLines = Math.max(originalLines.length, 1); for (String line : originalLines) { line = line.trim(); if (line.length() < 20) continue; // 忽略过短文本行,减少干扰 if (unauthorizedStr.contains(line)) { matchedLines++; } } return (double) matchedLines / totalLines * 100; } private static String normalize(String input) { // 1. 去HTML标签 String noHtml = input.replaceAll("<[^>]*>", " "); // 2. 去空白字符 String noWhitespace = noHtml.replaceAll("\\s+", "\n"); // 3. 去常见动态字段(可选,djb哈希等) return noWhitespace.trim(); } }这个算法的思路跟人工判断逻辑是一致的:看主要内容在不在里面,而不是看两个字符串等不等。
在测试某内部系统时,原始请求返回了用户的订单列表,未授权请求直接访问接口,返回了同样的订单列表但少了右上角的用户名。因为大部分订单内容都是一样的,相似度算出来92%,超过了阈值,漏洞就标记出来了。如果我用哈希对比,这直接就漏报了。
3.5 IScannerCheck的接入(可选扩展)
把功能挂到主动扫描里,需要实现IScannerCheck的四个方法。
@Override public List<IScanIssue> doPassiveScan(IHttpRequestResponse baseRequestResponse) { // 被动扫描:不主动发包 // 这里可以检测响应中是否暴露了敏感信息,但不适合做未授权访问检测 return null; } @Override public List<IScanIssue> doActiveScan(IHttpRequestResponse baseRequestResponse, IScannerInsertionPoint insertionPoint) { // 主动扫描:对于每个插入点,构造未授权请求 List<IScanIssue> issues = new ArrayList<>(); // 只在URL插入点执行,避免对参数做过多变体 if (insertionPoint.getInsertionPointName().equals("URL")) { UnauthorizedAccessChecker checker = new UnauthorizedAccessChecker(callbacks, helpers); boolean vulnerable = checker.checkForIssue(baseRequestResponse); if (vulnerable) { issues.add(new UnauthorizedScanIssue(...)); } } return issues; } @Override public int consolidateDuplicateIssues(IScanIssue existingIssue, IScanIssue newIssue) { // 如果URL相同,认为是重复issue if (existingIssue.getUrl().equals(newIssue.getUrl())) { return -1; } return 0; }主动扫描的地狱级坑点是性能。每个请求、每个插入点都会触发doActiveScan,如果不加过滤,一次扫描会发出海量重复请求。我的建议是加两个限制:
- 只对包含API特征的路径执行检测(
/api/、/v1/、/admin/等); - 每个请求只测一次,不要对每个参数插入点都执行完整检测逻辑。
4. 交互体验优化:结果展示与UI设计
功能做完了只是第一步,难的是怎么让结果直观可读。我见过不少插件,功能逻辑没问题,但输出信息极简,只往Output窗口打一行“Possible vulnerability”,完全没法用。
我自己在UI上做了这几个优化。
4.1 结果面板:把对比信息完整展示
不要只打印结论,把对比的细节也打出来。方便在报告里引用,也方便人工复核:
callbacks.printOutput("=========================================="); callbacks.printOutput("[+] Unauthorized Access Detected"); callbacks.printOutput(" URL : " + url); callbacks.printOutput(" Method : " + method); callbacks.printOutput(" Original Status : " + originalStatus); callbacks.printOutput(" Unauthorized Status: " + unauthorizedStatus); callbacks.printOutput(" Original Length : " + originalRespLength); callbacks.printOutput(" Unauthorized Length: " + unauthRespLength); callbacks.printOutput(" Similarity Score : " + similarityScore + "%"); callbacks.printOutput(" Sensitive Headers Removed: Cookie, Authorization"); callbacks.printOutput("==========================================");打印这些信息看起来简单,但真到复盘的时候能省大量时间。比如你发现某条请求未授权访问返回了200,但响应体是空的,相似度只有10%,这类大概率是假阳性。如果只打印“Vulnerable”,你根本不知道误报在哪。
4.2 右键菜单异步执行:别卡UI
还有个容易被忽略的体验问题:网络请求是阻塞操作,如果在EDT(Event Dispatch Thread,Swing的事件分发线程)里同步执行,UI会直接卡死。尤其是批量检测多个请求时,界面会变成“未响应”状态。
解决方式是用SwingWorker做异步处理:
menuItem.addActionListener(e -> { SwingWorker<Void, String> worker = new SwingWorker<>() { @Override protected Void doInBackground() throws Exception { for (IHttpRequestResponse message : messages) { performUnauthorizedCheck(message); publish("Checked: " + getURL(message)); } return null; } @Override protected void process(List<String> chunks) { for (String chunk : chunks) { callbacks.printOutput(chunk); } } }; worker.execute(); });4.3 彩色Console输出
Burp的Console输出是纯文本,不原生支持颜色,但可以通过ANSI转义码实现:
private static final String ANSI_RESET = "\u001B[0m"; private static final String ANSI_RED = "\u001B[31m"; private static final String ANSI_GREEN = "\u001B[32m"; private static final String ANSI_YELLOW = "\u001B[33m";实测下来,Burp的Console是支持ANSI的基本颜色的。红色标记漏洞,绿色标记正常,黄色标记待确认,一眼就能扫到重点信息。
5. 常见问题、误报分析与排查技巧
这部分全是实际操作中踩出来的经验。功能逻辑看着简单,但用起来才知道坑在哪。
5.1 误报场景一:静态路径被误判为未授权访问
最常见也最头疼的误报来源。前端项目基本都会引用一堆JS、CSS、图片资源,这些资源大多不需要鉴权,去掉Cookie访问照样返回200,响应体跟原始请求几乎一致。然后插件就报“未授权访问”了。
排查思路:看响应Content-Type,JS、CSS、PNG、JPG这几种MIME类型直接跳过。HTML页面也不一定都是业务页面,纯静态展示页同样会误报。
private boolean isStaticResource(IHttpRequestResponse message) { IResponseInfo respInfo = helpers.analyzeResponse(message.getResponse()); String contentType = ""; for (String header : respInfo.getHeaders()) { if (header.toLowerCase().startsWith("content-type")) { contentType = header.split(":")[1].trim(); break; } } return contentType.contains("javascript") || contentType.contains("css") || contentType.contains("image") || contentType.contains("font") || contentType.contains("video"); }5.2 误报场景二:302跳转被当成漏洞
还有一种情况是未授权请求返回302,跳转到统一登录页。这个其实说明鉴权是生效的,不应该报漏洞。但有些系统设计得比较激进,前端做了302跳转,后端仍然返回部分业务数据——这种情况才需要标记。
处理方式:未授权请求返回302时,默认不做判定。但如果启用了“跟随重定向后再对比”的选项,可以进一步分析重定向后的响应。不过实测这个方法不太可靠,因为Burp的makeHttpRequest不会默认跟随重定向,你需要自己递归请求Location地址,逻辑会变得复杂。我建议这个功能做成手动确认项,别放进自动流程。
5.3 误报场景三:请求本身就带业务必填参数
有些接口必须在请求体里带有效业务参数,否则直接报400。如果你把Cookie剥掉了,但保留了业务参数,服务器返回400,这显然不是未授权访问。
处理方式:判定时对400、422这类客户端错误码做特殊处理。除非响应体里出现了明显的敏感数据,否则400一律视为“参数校验失败”,不报漏洞。
if (unauthorizedStatus == 400 || unauthorizedStatus == 422) { callbacks.printOutput("[i] Request rejected due to missing parameters, not an auth issue."); return; }5.4 真实漏洞却漏报的常见情况
误报是烦,但漏报才是真正的灾难。我在调试中发现,最容易被漏报的场景是“前后端分离”的系统。
典型情况:前端先调/user/current判断用户登录状态,后端如果检测到未登录,返回401。但业务接口/api/orders/list却没有做同样的检测,直接返回数据。
如果插件只对比了状态码(原请求200、未授权请求401),那相似度再高也会被状态码不一致给拦下来,导致漏报。
优化方案:不要只看状态码是否一致,而是看“未授权请求的状态码是否属于正常业务范畴”。如果未授权请求返回了200、201、301(非登录页跳转),且响应体里包含了业务特征字段(如"status":"success"、"data"、"list"、"total"等),就应该标记为可疑。
// 状态码兜底逻辑 boolean isBusinessStatusCode = unauthorizedStatus == 200 || unauthorizedStatus == 201 || unauthorizedStatus == 202; // 响应体业务特征 String body = new String(unauthorizedBody); boolean hasBusinessData = body.contains("\"status\"") || body.contains("\"data\"") || body.contains("\"success\"") || body.contains("\"list\"") || body.contains("\"total\""); if (isBusinessStatusCode && hasBusinessData) { // 进一步判定 isVulnerable = true; }5.5 目标限制:别把自己封了
做未授权检测要频繁发送篡改过的请求,目标系统的WAF或风控模块可能识别出这种异常行为并封IP。实测中踩过这个坑:对一个生产环境系统跑了20多个请求,触发风控,直接被限制访问。
经验:
- 默认并发数控制在1,不要同时发多个请求;
- 每次请求前加一个可配置的延时(默认300ms);
- 对同一Host不要连续发起超过N个检测请求,N可配置;
- 如果目标处于生产环境,建议先在测试环境验证插件逻辑,再切到真实目标。
5.6 请求头丢失问题:Referer和Origin
在剥离敏感头时,有个细节容易被忽略:Referer和Origin头。有些系统的鉴权逻辑依赖这两个头做来源判断,比如CSRF防护里校验Origin,SSO登录流程里校验Referer。虽然它们不直接携带凭证,但剥离后可能导致请求失败,从而产生误报。
我的策略是:这两个头保留。因为它们不属于身份凭证,而且去掉之后很容易破坏业务逻辑,导致响应异常,反而干扰判断。
6. 功能扩展思路:从单点检测到链式利用
基础功能写完,如果你还想继续深化,有几个扩展方向我认为价值很高。
6.1 从“检测”到“利用”:自动访问发现接口
未授权访问检测的价值如果只停留在“发现接口有问题”,其实只发挥了一半。更进阶的玩法是:检测到未授权可访问的接口后,自动爬取该接口暴露的关联资源,比如JSON里引用的/api/v1/user/{id},继续对子路径做二次未授权检测。
这样从“单个接口未授权”变成“整条接口链未授权”,漏洞报告的说服力会强很多。实现方案是解析响应JSON中的URL模式,用正则提取,然后递归检测。
6.2 支持自定义鉴权头配置
不同系统的鉴权方式差异很大,有的用X-Access-Token,有的用X-User-SessionId,有的甚至用自定义加密头。把敏感头列表做成可配置,是最实际的需求。
可以用Burp插件自带的配置UI框架(比如ITab接口),画一个简单的面板,让用户自己输入需要剥离的Header名称。也可以直接读配置文件,在扩展目录下放一个unauth_headers.conf,启动时解析。
private List<String> loadCustomSensitiveHeaders() { Path configPath = Paths.get(System.getProperty("user.home"), ".burp-unauth-checker", "headers.conf"); if (!Files.exists(configPath)) { return Collections.emptyList(); } try { return Files.readAllLines(configPath).stream() .map(String::trim) .filter(line -> !line.isEmpty() && !line.startsWith("#")) .collect(Collectors.toList()); } catch (IOException e) { callbacks.printError("Failed to load custom headers: " + e.getMessage()); return Collections.emptyList(); } }6.3 联动响应编辑器:一键发送到Repeater
很多安全测试流程里,用插件检测到可疑请求后,下一步操作是手动打开Repeater,人工做精细化验证。可以加一个联动按钮:检测到漏洞后,直接打开Repeater并把未授权请求加载进去。
Burp的API里,IBurpExtenderCallbacks.sendToRepeater就是干这个的:
callbacks.sendToRepeater(httpService.getHost(), httpService.getPort(), httpService.getProtocol().equals("https"), unauthorizedRequest, "Unauthorized-" + index);这样从“检测发现”到“人工验证”的闭环就打通了,操作效率高了不少。
6.4 生成未授权访问报告
最后,很多人写插件忽略报告输出环节。用了一段时间之后,检测记录散落在Console里,难回溯、难统计、难写进渗透测试报告。
我加了一个简单的CSV导出功能:点击自定义Tab上的“Export Report”,把当前所有检测结果按URL、Method、原始状态码、未授权状态码、相似度、判定结果、时间戳这些字段导出成CSV。实测对后期整理交付报告帮助很大。
private void exportReport(File outputFile) { try (PrintWriter writer = new PrintWriter(new FileWriter(outputFile))) { writer.println("URL,Method,OriginalStatus,UnauthorizedStatus,Similarity,Result,Timestamp"); for (CheckRecord record : checkRecords) { writer.printf("%s,%s,%d,%d,%.2f,%s,%s%n", record.getUrl(), record.getMethod(), record.getOriginalStatus(), record.getUnauthorizedStatus(), record.getSimilarity(), record.getResult(), record.getTimestamp()); } } catch (IOException e) { callbacks.printError("Failed to export report: " + e.getMessage()); } }7. 测试与调试方法总结
最后简单整理一下我验证插件逻辑的测试流程。插件开发最怕的就是在Burp界面和IDE之间来回切换,效率极低。我总结出一套相对顺畅的调试流程。
7.1 准备本地测试环境
用本地环境做靶场是最快的。起一个简单的Spring Boot应用,写几个测试接口:
/api/public/info:无需鉴权,正常返回;/api/private/userinfo:需要Interceptor校验Token;/api/private/admin:需要校验Token且要求admin角色;/static/app.js:静态资源。
这样就能覆盖所有的检测分支:正常漏洞、已拦截请求、静态资源过滤。
7.2 Burp加载扩展
在Burp的Extender选项卡里,Add → Java → 选择编译好的Jar包。加载成功后看Console输出,确认扩展注册成功。
调试时建议打开“Run in background”的日志开关,让Burp把插件内部的printOutput内容全部打印到Console。每次改动代码后要重新编译,然后重新加载Jar。
7.3 手动验证逻辑分支
按这个顺序逐项验证:
- 正常鉴权接口:未授权请求返回401,插件不报警;
- 真正未授权接口:未授权请求返回200且响应相似度高,插件报警;
- 静态资源:去掉Cookie请求CSS,插件不应报警;
- 参数不完整请求:返回400,插件不报警;
- 302跳转接口:插件提供待确认标记,不直接报告漏洞。
把这些分支全部跑通,功能才算达到可用状态。我在实际调试时发现第四种和第五种分支最容易出错,建议多调几轮。
标题命名上,建议右键菜单的名称不要起得太“攻击性”,像“Check Unauthorized Access”这种中性描述就足够。
插件开发这件事,逻辑本身往往不复杂,复杂的是把边缘情况想清楚。在写这篇实战笔记的过程中,我最大的一个体会是:未授权访问检测的本质不是“找一个Bug”,而是“理解这个系统的鉴权信任边界在哪里”。只有想清楚哪些部分是系统默认信任的、哪些是需要主动验证的,才能把检测规则设计得既减少误报又能覆盖真实风险。