Skip to content

Commit 5731f92

Browse files
author
githubnull
committed
feat(burp): add request deduplication for batch scanning
- Add RequestDeduplicator class for both Montoya and Legacy APIs - Deduplicate requests based on: protocol, method, host, port, path, query params, body params - Add autoDedupe config option in ConfigManager (default: enabled) - Add dedupe checkbox in DefaultConfigPanel UI - Log filtered duplicates in scan output - Works in conjunction with binary content filtering
1 parent fa9704f commit 5731f92

8 files changed

Lines changed: 712 additions & 11 deletions

File tree

src/burpEx/legacy-api/src/main/java/com/sqlmapwebui/burp/BurpExtender.java

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -251,24 +251,52 @@ private FilterResult filterBinaryRequests(IHttpRequestResponse[] messages) {
251251

252252
/**
253253
* 发送过滤后的纯文本请求,并记录被过滤的二进制请求日志
254+
* 第一步:过滤二进制请求
255+
* 第二步:去重处理(如果开启)
254256
*/
255257
private void sendFilteredRequests(FilterResult filterResult, ScanConfig config) {
256-
// 记录过滤统计
258+
// 第一步:记录二进制过滤统计
257259
if (filterResult.binaryCount() > 0) {
258-
uiTab.appendLog(String.format("[*] 过滤统计: 共选中 %d 个请求,%d 个纯文本可扫描,%d 个二进制已过滤",
259-
filterResult.totalCount(), filterResult.textCount(), filterResult.binaryCount()));
260+
uiTab.appendLog(String.format("[*] 二进制过滤: %d 个请求已跳过", filterResult.binaryCount()));
260261

261262
// 记录被过滤的二进制请求URL
262263
for (IHttpRequestResponse binaryMsg : filterResult.binaryMessages) {
263264
IRequestInfo reqInfo = helpers.analyzeRequest(binaryMsg);
264265
String url = reqInfo.getUrl().toString();
265266
BinaryContentDetector.DetectionResult detection = BinaryContentDetector.detect(binaryMsg, helpers);
266-
uiTab.appendLog(String.format(" [跳过] %s (原因: %s)", url, detection.getReason()));
267+
uiTab.appendLog(String.format(" [跳过-二进制] %s (原因: %s)", url, detection.getReason()));
267268
}
268269
}
269270

271+
// 第二步:去重处理
272+
List<IHttpRequestResponse> messagesToSend = filterResult.textMessages;
273+
int duplicateCount = 0;
274+
275+
if (configManager.isAutoDedupe() && messagesToSend.size() > 1) {
276+
RequestDeduplicator.DedupeResult dedupeResult = RequestDeduplicator.deduplicate(messagesToSend, helpers);
277+
278+
if (dedupeResult.hasDuplicates()) {
279+
duplicateCount = dedupeResult.duplicateCount();
280+
uiTab.appendLog(String.format("[*] 重复过滤: %d 个重复请求已跳过", duplicateCount));
281+
282+
// 记录被过滤的重复请求
283+
for (IHttpRequestResponse dupMsg : dedupeResult.getDuplicateMessages()) {
284+
String desc = RequestDeduplicator.getRequestDescription(dupMsg, helpers);
285+
uiTab.appendLog(String.format(" [跳过-重复] %s", desc));
286+
}
287+
288+
messagesToSend = dedupeResult.getUniqueMessages();
289+
}
290+
}
291+
292+
// 输出统计汇总
293+
if (filterResult.binaryCount() > 0 || duplicateCount > 0) {
294+
uiTab.appendLog(String.format("[*] 最终统计: 共选中 %d 个请求,实际发送 %d 个",
295+
filterResult.totalCount(), messagesToSend.size()));
296+
}
297+
270298
// 发送纯文本请求
271-
for (IHttpRequestResponse message : filterResult.textMessages) {
299+
for (IHttpRequestResponse message : messagesToSend) {
272300
sendRequestToBackend(message, config);
273301
}
274302
}

src/burpEx/legacy-api/src/main/java/com/sqlmapwebui/burp/ConfigManager.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public class ConfigManager {
2020
private static final String KEY_PRESET_CONFIGS = "presetConfigs";
2121
private static final String KEY_HISTORY_CONFIGS = "historyConfigs";
2222
private static final String KEY_MAX_HISTORY_SIZE = "maxHistorySize";
23+
private static final String KEY_AUTO_DEDUPE = "autoDedupe";
2324

2425
// 历史记录数量限制
2526
public static final int MIN_HISTORY_SIZE = 3;
@@ -32,6 +33,7 @@ public class ConfigManager {
3233
// 配置数据
3334
private String backendUrl = "http://localhost:5000";
3435
private int maxHistorySize = DEFAULT_HISTORY_SIZE;
36+
private boolean autoDedupe = true; // 默认开启自动去重
3537
private ScanConfig defaultConfig;
3638
private List<ScanConfig> presetConfigs; // 常用配置
3739
private List<ScanConfig> historyConfigs; // 历史配置
@@ -69,6 +71,12 @@ private void loadConfigurations() {
6971
}
7072
}
7173

74+
// 加载自动去重配置
75+
String savedAutoDedupe = callbacks.loadExtensionSetting(KEY_AUTO_DEDUPE);
76+
if (savedAutoDedupe != null && !savedAutoDedupe.isEmpty()) {
77+
autoDedupe = Boolean.parseBoolean(savedAutoDedupe);
78+
}
79+
7280
// 加载默认配置
7381
String defaultConfigJson = callbacks.loadExtensionSetting(KEY_DEFAULT_CONFIG);
7482
if (defaultConfigJson != null && !defaultConfigJson.isEmpty()) {
@@ -144,6 +152,17 @@ public void setMaxHistorySize(int size) {
144152
trimHistory();
145153
}
146154

155+
// ============ 自动去重配置 ============
156+
157+
public boolean isAutoDedupe() {
158+
return autoDedupe;
159+
}
160+
161+
public void setAutoDedupe(boolean enabled) {
162+
this.autoDedupe = enabled;
163+
callbacks.saveExtensionSetting(KEY_AUTO_DEDUPE, String.valueOf(enabled));
164+
}
165+
147166
// ============ 连接状态管理 ============
148167

149168
public boolean isConnected() {
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package com.sqlmapwebui.burp;
2+
3+
import burp.IExtensionHelpers;
4+
import burp.IHttpRequestResponse;
5+
import burp.IRequestInfo;
6+
7+
import java.net.URL;
8+
import java.nio.charset.StandardCharsets;
9+
import java.security.MessageDigest;
10+
import java.security.NoSuchAlgorithmException;
11+
import java.util.*;
12+
13+
/**
14+
* HTTP请求去重器 (Legacy API版本)
15+
*
16+
* 用于在批量发送扫描任务时自动过滤重复请求
17+
*
18+
* 重复判断标准:
19+
* - 协议 (http/https)
20+
* - 请求方法 (GET/POST等)
21+
* - 主机
22+
* - 端口
23+
* - Path
24+
* - URL参数 (Query Parameters)
25+
* - Body参数
26+
*
27+
* 以上所有条件都相同才认为是重复请求
28+
*
29+
* @author SQLMap WebUI Team
30+
* @version 1.0.0
31+
*/
32+
public class RequestDeduplicator {
33+
34+
/**
35+
* 去重结果类
36+
*/
37+
public static class DedupeResult {
38+
private final List<IHttpRequestResponse> uniqueMessages;
39+
private final List<IHttpRequestResponse> duplicateMessages;
40+
41+
public DedupeResult(List<IHttpRequestResponse> uniqueMessages,
42+
List<IHttpRequestResponse> duplicateMessages) {
43+
this.uniqueMessages = uniqueMessages;
44+
this.duplicateMessages = duplicateMessages;
45+
}
46+
47+
public List<IHttpRequestResponse> getUniqueMessages() {
48+
return uniqueMessages;
49+
}
50+
51+
public List<IHttpRequestResponse> getDuplicateMessages() {
52+
return duplicateMessages;
53+
}
54+
55+
public int uniqueCount() {
56+
return uniqueMessages.size();
57+
}
58+
59+
public int duplicateCount() {
60+
return duplicateMessages.size();
61+
}
62+
63+
public int totalCount() {
64+
return uniqueMessages.size() + duplicateMessages.size();
65+
}
66+
67+
public boolean hasDuplicates() {
68+
return !duplicateMessages.isEmpty();
69+
}
70+
}
71+
72+
/**
73+
* 对请求列表进行去重
74+
*
75+
* @param messages 原始请求数组
76+
* @param helpers Burp扩展帮助器
77+
* @return 去重结果
78+
*/
79+
public static DedupeResult deduplicate(IHttpRequestResponse[] messages, IExtensionHelpers helpers) {
80+
return deduplicate(Arrays.asList(messages), helpers);
81+
}
82+
83+
/**
84+
* 对请求列表进行去重
85+
*
86+
* @param messages 原始请求列表
87+
* @param helpers Burp扩展帮助器
88+
* @return 去重结果
89+
*/
90+
public static DedupeResult deduplicate(List<IHttpRequestResponse> messages, IExtensionHelpers helpers) {
91+
List<IHttpRequestResponse> uniqueMessages = new ArrayList<>();
92+
List<IHttpRequestResponse> duplicateMessages = new ArrayList<>();
93+
Set<String> seenFingerprints = new HashSet<>();
94+
95+
for (IHttpRequestResponse message : messages) {
96+
String fingerprint = generateFingerprint(message, helpers);
97+
98+
if (seenFingerprints.contains(fingerprint)) {
99+
duplicateMessages.add(message);
100+
} else {
101+
seenFingerprints.add(fingerprint);
102+
uniqueMessages.add(message);
103+
}
104+
}
105+
106+
return new DedupeResult(uniqueMessages, duplicateMessages);
107+
}
108+
109+
/**
110+
* 生成请求的唯一指纹
111+
*
112+
* 指纹包含:协议、方法、主机、端口、路径、查询参数、请求体
113+
*/
114+
public static String generateFingerprint(IHttpRequestResponse requestResponse, IExtensionHelpers helpers) {
115+
StringBuilder sb = new StringBuilder();
116+
117+
try {
118+
byte[] request = requestResponse.getRequest();
119+
IRequestInfo requestInfo = helpers.analyzeRequest(requestResponse);
120+
URL url = requestInfo.getUrl();
121+
122+
// 1. 协议
123+
String protocol = url.getProtocol().toLowerCase();
124+
sb.append("protocol:").append(protocol).append("|");
125+
126+
// 2. 请求方法
127+
String method = requestInfo.getMethod().toUpperCase();
128+
sb.append("method:").append(method).append("|");
129+
130+
// 3. 主机
131+
String host = url.getHost().toLowerCase();
132+
sb.append("host:").append(host).append("|");
133+
134+
// 4. 端口 (处理默认端口)
135+
int port = url.getPort();
136+
if (port == -1) {
137+
port = "https".equals(protocol) ? 443 : 80;
138+
}
139+
sb.append("port:").append(port).append("|");
140+
141+
// 5. Path
142+
String path = url.getPath();
143+
if (path == null || path.isEmpty()) {
144+
path = "/";
145+
}
146+
sb.append("path:").append(path).append("|");
147+
148+
// 6. 查询参数 (排序后比较,忽略顺序)
149+
String query = url.getQuery();
150+
String normalizedQuery = normalizeQueryParams(query);
151+
sb.append("query:").append(normalizedQuery).append("|");
152+
153+
// 7. Body参数 (对于POST/PUT等)
154+
int bodyOffset = requestInfo.getBodyOffset();
155+
String body = "";
156+
if (bodyOffset < request.length) {
157+
body = new String(request, bodyOffset, request.length - bodyOffset);
158+
}
159+
String normalizedBody = normalizeBody(body, getContentType(requestInfo));
160+
sb.append("body:").append(normalizedBody);
161+
162+
} catch (Exception e) {
163+
// 如果解析失败,使用原始请求的hash
164+
byte[] request = requestResponse.getRequest();
165+
sb.append("raw:").append(new String(request));
166+
}
167+
168+
// 生成MD5哈希作为指纹
169+
return md5Hash(sb.toString());
170+
}
171+
172+
/**
173+
* 规范化查询参数 (排序,忽略顺序差异)
174+
*/
175+
private static String normalizeQueryParams(String query) {
176+
if (query == null || query.isEmpty()) {
177+
return "";
178+
}
179+
180+
try {
181+
Map<String, List<String>> params = new TreeMap<>();
182+
String[] pairs = query.split("&");
183+
184+
for (String pair : pairs) {
185+
int idx = pair.indexOf("=");
186+
String key = idx > 0 ? pair.substring(0, idx) : pair;
187+
String value = idx > 0 && pair.length() > idx + 1 ? pair.substring(idx + 1) : "";
188+
189+
params.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
190+
}
191+
192+
// 对每个key的values也排序
193+
StringBuilder result = new StringBuilder();
194+
for (Map.Entry<String, List<String>> entry : params.entrySet()) {
195+
Collections.sort(entry.getValue());
196+
for (String value : entry.getValue()) {
197+
if (result.length() > 0) {
198+
result.append("&");
199+
}
200+
result.append(entry.getKey()).append("=").append(value);
201+
}
202+
}
203+
204+
return result.toString();
205+
206+
} catch (Exception e) {
207+
return query;
208+
}
209+
}
210+
211+
/**
212+
* 规范化请求体
213+
*/
214+
private static String normalizeBody(String body, String contentType) {
215+
if (body == null || body.isEmpty()) {
216+
return "";
217+
}
218+
219+
// 对于form-urlencoded,进行参数排序
220+
if (contentType != null && contentType.contains("application/x-www-form-urlencoded")) {
221+
return normalizeQueryParams(body);
222+
}
223+
224+
// 对于JSON,直接使用原始内容(或可以解析后规范化)
225+
// 这里简化处理,直接返回trim后的body
226+
return body.trim();
227+
}
228+
229+
/**
230+
* 获取Content-Type
231+
*/
232+
private static String getContentType(IRequestInfo requestInfo) {
233+
List<String> headers = requestInfo.getHeaders();
234+
for (String header : headers) {
235+
if (header.toLowerCase().startsWith("content-type:")) {
236+
return header.substring("content-type:".length()).trim().toLowerCase();
237+
}
238+
}
239+
return "";
240+
}
241+
242+
/**
243+
* 生成MD5哈希
244+
*/
245+
private static String md5Hash(String input) {
246+
try {
247+
MessageDigest md = MessageDigest.getInstance("MD5");
248+
byte[] digest = md.digest(input.getBytes(StandardCharsets.UTF_8));
249+
StringBuilder sb = new StringBuilder();
250+
for (byte b : digest) {
251+
sb.append(String.format("%02x", b));
252+
}
253+
return sb.toString();
254+
} catch (NoSuchAlgorithmException e) {
255+
// 降级方案:使用Java hashCode
256+
return String.valueOf(input.hashCode());
257+
}
258+
}
259+
260+
/**
261+
* 获取请求的简短描述(用于日志)
262+
*/
263+
public static String getRequestDescription(IHttpRequestResponse requestResponse, IExtensionHelpers helpers) {
264+
try {
265+
IRequestInfo requestInfo = helpers.analyzeRequest(requestResponse);
266+
URL url = requestInfo.getUrl();
267+
return requestInfo.getMethod() + " " + url.getHost() + url.getPath();
268+
} catch (Exception e) {
269+
return "Unknown Request";
270+
}
271+
}
272+
}

0 commit comments

Comments
 (0)