Created
September 16, 2025 12:11
-
-
Save irsdl/28bfd1fe6c54c7c98288eeb86da23e6e to your computer and use it in GitHub Desktop.
A generic Burp Suite Bambdas Custom Action that finds the most recent Proxy history entry matching configurable filters (host/path/method/status/scope/highlight; plus request/response regex gates), extracts values (e.g., Cookie, aura.context, aura.token) via regex, and applies them to the current Repeater request—replacing the Cookie header and …
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ============================================================================ | |
| // Repeater Action: Pull tokens/values from Proxy History & Apply to THIS item | |
| // ---------------------------------------------------------------------------- | |
| // WHAT IT DOES | |
| // 1) Scans Proxy history (most recent first) for an entry that matches your filters. | |
| // 2) Extracts values (Cookie header, form params, etc.) via regex extractors. | |
| // 3) Applies the extracted values to the CURRENT Repeater request (requestResponse). | |
| // | |
| // HOW TO USE / EDIT (TL;DR) | |
| // - Set filters in CONFIG (host/path/method/status/in-scope/highlight). Blank = ignored. | |
| // - Add/remove request/response regex extractors in CONFIG > EXTRACT_* lists. | |
| // - (Included examples) Extract: | |
| // * Request header: Cookie (full value) | |
| // * Request body (x-www-form-urlencoded): aura.context, aura.token | |
| // - Replacement behavior (destination = current Repeater item): | |
| // * Replaces Cookie header if a Cookie value was extracted | |
| // * Replaces existing aura.context / aura.token params in body if extracted | |
| // * Leaves all other headers/params untouched | |
| // | |
| // NOTES | |
| // - All filter arrays use AND semantics: ALL non-empty patterns must match. | |
| // - Regex flags used (extract phase): CASE_INSENSITIVE + DOTALL + MULTILINE. | |
| // - For filter regex: add (?i) yourself where you want case-insensitive. | |
| // - Highlight filter matches rr.annotations().highlightColor().name(), e.g. "RED". | |
| // - If you need to *add* a missing form parameter (not just replace), see the | |
| // commented "ADD_IF_MISSING" example in the Apply section. | |
| // | |
| // Tested with Montoya API (Burp Suite) Bambdas Custom Action model. | |
| // ============================================================================ | |
| // ----------------------- | |
| // ---- CONFIG START ---- | |
| // ----------------------- | |
| // === High-level matching === | |
| // Leave any EMPTY ("") to ignore that filter. | |
| // TIP: Start with minimal filters (host + status) then add method/path/highlight as needed. | |
| // EDIT ME: Example host filter (regex). Use (?i) for case-insensitivity. | |
| final String REGEX_TARGET_HOST = "^(?i)example\\.com$"; | |
| // EDIT ME: Optional path prefix filter (regex). Example for Salesforce Aura: | |
| // final String REGEX_PATH_PREFIX = "^/s/sfsites/aura(?:/.*)?$"; | |
| final String REGEX_PATH_PREFIX = ""; | |
| // EDIT ME: Optional HTTP method filter (regex), e.g. POST only: | |
| final String REGEX_METHOD = "^(?i)POST$"; // "" to ignore | |
| // EDIT ME: Optional *response* status code filter. <=0 means ignore. | |
| final short FILTER_STATUS_CODE = (short)200; // set 0 to ignore | |
| // EDIT ME: Only consider requests that are "in scope" in Burp's Target tab. | |
| final boolean ONLY_IN_SCOPE = true; | |
| // EDIT ME: Optional highlight color regex filter (history row’s annotation color). | |
| // Valid names: RED, ORANGE, YELLOW, GREEN, CYAN, BLUE, PINK, MAGENTA, GRAY. | |
| // Example: any red-ish highlight -> "(?i)^red$". Leave "" to ignore. | |
| final String HIGHLIGHT_COLOR_REGEX = ""; // "(?i)^red$" | |
| // === Request-side filters (AND). Leave arrays empty to ignore. === | |
| // TIP: These filters DO NOT extract; they ONLY gate which history entry is chosen. | |
| // Add (?i) in your regex where you want case-insensitive matching. | |
| final java.util.List<String> FILTER_QUERY_REGEX = java.util.Arrays.asList( | |
| // e.g. "ui-identity-components-sessiontimeoutwarn\\.SessionTimeoutWarn\\.getSessionTimeoutConfig=1" | |
| ); | |
| final java.util.List<String> FILTER_REQUEST_HEADER_REGEX = java.util.Arrays.asList( | |
| // e.g. "(?i)^Referer:\\s?https://example\\.com/.*$" | |
| ); | |
| final java.util.List<String> FILTER_REQUEST_BODY_REGEX = java.util.Arrays.asList( | |
| // e.g. "\"grant_type\"\\s*=\\s*client_credentials" | |
| ); | |
| // === Response-side filters (AND). Leave arrays empty to ignore. === | |
| final java.util.List<String> FILTER_RESPONSE_HEADER_REGEX = java.util.Arrays.asList( | |
| // e.g. "(?i)^X-Request-Id:\\s*[^\\r\\n]+" | |
| ); | |
| final java.util.List<String> FILTER_RESPONSE_BODY_REGEX = java.util.Arrays.asList( | |
| // e.g. "\"token_type\"\\s*:\\s*\"Bearer\"" | |
| ); | |
| // === Extractors === | |
| // Each entry: new Object[]{ name(String), regex(String), group(Integer|omitted) } | |
| // - If group <= 0 or omitted => full match. | |
| // - Extractors run with flags: CASE_INSENSITIVE | DOTALL | MULTILINE. | |
| // - Extracted values go into four maps keyed by "name": | |
| // out_request_headers, out_request_body, out_response_headers, out_response_body | |
| // - If a name appears multiple times, values are collected into a list. | |
| // | |
| // Included examples (EDIT/REMOVE as needed): | |
| final java.util.List<Object[]> EXTRACT_REQUEST_HEADERS = new java.util.ArrayList<>(); | |
| final java.util.List<Object[]> EXTRACT_REQUEST_BODY = new java.util.ArrayList<>(); | |
| final java.util.List<Object[]> EXTRACT_RESPONSE_HEADERS = new java.util.ArrayList<>(); | |
| final java.util.List<Object[]> EXTRACT_RESPONSE_BODY = new java.util.ArrayList<>(); | |
| // --- Request header extractor examples --- | |
| EXTRACT_REQUEST_HEADERS.add(new Object[]{ "cookie", "(?i)^Cookie:\\s*([^\\r\\n]+)", 1 }); | |
| // --- Request body (x-www-form-urlencoded) extractor examples --- | |
| EXTRACT_REQUEST_BODY.add(new Object[]{ "aura.context", "(?i)(?:^|&)(?:aura\\.context)=([^&]*)", 1 }); | |
| EXTRACT_REQUEST_BODY.add(new Object[]{ "aura.token", "(?i)(?:^|&)(?:aura\\.token)=([^&]*)", 1 }); | |
| // --- Response examples (uncomment or add your own) --- | |
| // EXTRACT_RESPONSE_HEADERS.add(new Object[]{ "X-Request-Id", "(?i)^X-Request-Id:\\s*([^\\r\\n]+)", 1 }); | |
| // EXTRACT_RESPONSE_BODY.add( new Object[]{ "access_token", "\"access_token\"\\s*:\\s*\"([^\"]+)\"", 1 }); | |
| // --------------------- | |
| // ---- CONFIG END ---- | |
| // --------------------- | |
| // --------------------------- | |
| // ---- Build the history filter ---- | |
| burp.api.montoya.proxy.ProxyHistoryFilter filter = new burp.api.montoya.proxy.ProxyHistoryFilter() { | |
| @Override | |
| public boolean matches(burp.api.montoya.proxy.ProxyHttpRequestResponse rr) { | |
| if (rr == null || !rr.hasResponse()) return false; | |
| var req = rr.request(); | |
| var res = rr.response(); | |
| if (req == null || res == null) return false; | |
| // Status code (optional) | |
| if (FILTER_STATUS_CODE > 0 && res.statusCode() != FILTER_STATUS_CODE) return false; | |
| // Scope (optional) | |
| if (ONLY_IN_SCOPE && !req.isInScope()) return false; | |
| // Highlight color (optional) | |
| if (HIGHLIGHT_COLOR_REGEX != null && !HIGHLIGHT_COLOR_REGEX.trim().isEmpty()) { | |
| var hl = rr.annotations().highlightColor(); | |
| String hlName = (hl == null) ? "" : hl.name(); | |
| var phl = java.util.regex.Pattern.compile(HIGHLIGHT_COLOR_REGEX, | |
| java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); | |
| if (!phl.matcher(hlName).find()) return false; | |
| } | |
| // Method (optional) | |
| if (REGEX_METHOD != null && !REGEX_METHOD.trim().isEmpty()) { | |
| String method = req.method(); | |
| var pm = java.util.regex.Pattern.compile(REGEX_METHOD, | |
| java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); | |
| if (!pm.matcher(method == null ? "" : method).find()) return false; | |
| } | |
| // Host + Path (optional) | |
| var svc = req.httpService(); | |
| if (svc == null) return false; | |
| String host = svc.host(); | |
| String path = req.pathWithoutQuery(); | |
| if (REGEX_TARGET_HOST != null && !REGEX_TARGET_HOST.trim().isEmpty()) { | |
| var ph = java.util.regex.Pattern.compile(REGEX_TARGET_HOST, | |
| java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); | |
| if (!ph.matcher(host == null ? "" : host).find()) return false; | |
| } | |
| if (REGEX_PATH_PREFIX != null && !REGEX_PATH_PREFIX.trim().isEmpty()) { | |
| var pp = java.util.regex.Pattern.compile(REGEX_PATH_PREFIX, | |
| java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.MULTILINE); | |
| if (!pp.matcher(path == null ? "" : path).find()) return false; | |
| } | |
| // Build request-side strings for filter regexes | |
| String url = req.url(); | |
| String queryPart = ""; | |
| if (url != null) { | |
| int q = url.indexOf('?'); | |
| if (q >= 0) { | |
| queryPart = url.substring(q + 1); | |
| int hash = queryPart.indexOf('#'); | |
| if (hash >= 0) queryPart = queryPart.substring(0, hash); | |
| } | |
| } | |
| StringBuilder sbReqHeaders = new StringBuilder(); | |
| for (var h : req.headers()) { | |
| sbReqHeaders.append(h.name()).append(": ").append(h.value() == null ? "" : h.value()).append("\n"); | |
| } | |
| String reqHeadersJoined = sbReqHeaders.toString(); | |
| String reqBody = ""; | |
| try { reqBody = req.bodyToString(); } catch (Exception ignore) {} | |
| // Build response-side strings for filter regexes | |
| StringBuilder sbResHeaders = new StringBuilder(); | |
| for (var h : res.headers()) { | |
| sbResHeaders.append(h.name()).append(": ").append(h.value() == null ? "" : h.value()).append("\n"); | |
| } | |
| String resHeadersJoined = sbResHeaders.toString(); | |
| String resBody = ""; | |
| try { resBody = res.bodyToString(); } catch (Exception ignore) {} | |
| // ---- Apply request-side AND filters ---- | |
| if (FILTER_QUERY_REGEX != null && !FILTER_QUERY_REGEX.isEmpty()) { | |
| for (String pat : FILTER_QUERY_REGEX) { | |
| if (pat == null || pat.trim().isEmpty()) continue; | |
| var p = java.util.regex.Pattern.compile(pat, | |
| java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.MULTILINE); | |
| if (!p.matcher(queryPart).find()) return false; | |
| } | |
| } | |
| if (FILTER_REQUEST_HEADER_REGEX != null && !FILTER_REQUEST_HEADER_REGEX.isEmpty()) { | |
| for (String pat : FILTER_REQUEST_HEADER_REGEX) { | |
| if (pat == null || pat.trim().isEmpty()) continue; | |
| var p = java.util.regex.Pattern.compile(pat, | |
| java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); | |
| if (!p.matcher(reqHeadersJoined).find()) return false; | |
| } | |
| } | |
| if (FILTER_REQUEST_BODY_REGEX != null && !FILTER_REQUEST_BODY_REGEX.isEmpty()) { | |
| for (String pat : FILTER_REQUEST_BODY_REGEX) { | |
| if (pat == null || pat.trim().isEmpty()) continue; | |
| var p = java.util.regex.Pattern.compile(pat, | |
| java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.MULTILINE); | |
| if (!p.matcher(reqBody).find()) return false; | |
| } | |
| } | |
| // ---- Apply response-side AND filters ---- | |
| if (FILTER_RESPONSE_HEADER_REGEX != null && !FILTER_RESPONSE_HEADER_REGEX.isEmpty()) { | |
| for (String pat : FILTER_RESPONSE_HEADER_REGEX) { | |
| if (pat == null || pat.trim().isEmpty()) continue; | |
| var p = java.util.regex.Pattern.compile(pat, | |
| java.util.regex.Pattern.CASE_INSENSITIVE | java.util.regex.Pattern.MULTILINE); | |
| if (!p.matcher(resHeadersJoined).find()) return false; | |
| } | |
| } | |
| if (FILTER_RESPONSE_BODY_REGEX != null && !FILTER_RESPONSE_BODY_REGEX.isEmpty()) { | |
| for (String pat : FILTER_RESPONSE_BODY_REGEX) { | |
| if (pat == null || pat.trim().isEmpty()) continue; | |
| var p = java.util.regex.Pattern.compile(pat, | |
| java.util.regex.Pattern.DOTALL | java.util.regex.Pattern.MULTILINE); | |
| if (!p.matcher(resBody).find()) return false; | |
| } | |
| } | |
| return true; | |
| } | |
| }; | |
| // --------------------------- | |
| // ---- Pull history (source to extract from) ---- | |
| java.util.List<burp.api.montoya.proxy.ProxyHttpRequestResponse> matches = api.proxy().history(filter); | |
| if (matches == null || matches.isEmpty()) { | |
| logging.logToOutput("No Proxy history matches with current filters."); | |
| return; | |
| } | |
| // Select most recent matching entry as source | |
| burp.api.montoya.proxy.ProxyHttpRequestResponse source = | |
| matches.stream() | |
| .max(java.util.Comparator.comparing(burp.api.montoya.proxy.ProxyHttpRequestResponse::time)) | |
| .orElse(null); | |
| if (source == null) { | |
| logging.logToOutput("Unexpected: no latest after filtering."); | |
| return; | |
| } | |
| // --------------------------- | |
| // ---- Extract phase (from source) ---- | |
| var srcReq = source.finalRequest(); | |
| var srcRes = source.response(); | |
| // Build joined headers | |
| StringBuilder sbSrcReqH = new StringBuilder(); | |
| for (var h : srcReq.headers()) { | |
| sbSrcReqH.append(h.name()).append(": ").append(h.value() == null ? "" : h.value()).append("\n"); | |
| } | |
| String srcReqHeadersJoined = sbSrcReqH.toString(); | |
| StringBuilder sbSrcResH = new StringBuilder(); | |
| for (var h : srcRes.headers()) { | |
| sbSrcResH.append(h.name()).append(": ").append(h.value() == null ? "" : h.value()).append("\n"); | |
| } | |
| String srcResHeadersJoined = sbSrcResH.toString(); | |
| // Bodies | |
| String srcReqBody = ""; | |
| String srcResBody = ""; | |
| try { srcReqBody = srcReq.bodyToString(); } catch (Exception ignore) {} | |
| try { srcResBody = srcRes.bodyToString(); } catch (Exception ignore) {} | |
| // Output maps | |
| java.util.Map<String, java.util.List<String>> out_request_headers = new java.util.HashMap<>(); | |
| java.util.Map<String, java.util.List<String>> out_request_body = new java.util.HashMap<>(); | |
| java.util.Map<String, java.util.List<String>> out_response_headers = new java.util.HashMap<>(); | |
| java.util.Map<String, java.util.List<String>> out_response_body = new java.util.HashMap<>(); | |
| int extractorFlags = java.util.regex.Pattern.CASE_INSENSITIVE | |
| | java.util.regex.Pattern.DOTALL | |
| | java.util.regex.Pattern.MULTILINE; | |
| // --- extract from source.request headers | |
| for (Object[] ex : EXTRACT_REQUEST_HEADERS) { | |
| if (ex == null || ex.length < 2) continue; | |
| String name = (String) ex[0]; | |
| String regex = (String) ex[1]; | |
| int group = (ex.length >= 3 && ex[2] != null) | |
| ? (ex[2] instanceof Number ? ((Number)ex[2]).intValue() : Integer.parseInt(String.valueOf(ex[2]))) | |
| : 0; | |
| if (name == null || name.trim().isEmpty() || regex == null || regex.trim().isEmpty()) continue; | |
| var m = java.util.regex.Pattern.compile(regex, extractorFlags).matcher(srcReqHeadersJoined); | |
| while (m.find()) { | |
| String val = (group <= 0 || group > m.groupCount()) ? m.group(0) : m.group(group); | |
| if (val != null) out_request_headers.computeIfAbsent(name, k -> new java.util.ArrayList<>()).add(val); | |
| } | |
| } | |
| // --- extract from source.request body | |
| for (Object[] ex : EXTRACT_REQUEST_BODY) { | |
| if (ex == null || ex.length < 2) continue; | |
| String name = (String) ex[0]; | |
| String regex = (String) ex[1]; | |
| int group = (ex.length >= 3 && ex[2] != null) | |
| ? (ex[2] instanceof Number ? ((Number)ex[2]).intValue() : Integer.parseInt(String.valueOf(ex[2]))) | |
| : 0; | |
| if (name == null || name.trim().isEmpty() || regex == null || regex.trim().isEmpty()) continue; | |
| var m = java.util.regex.Pattern.compile(regex, extractorFlags).matcher(srcReqBody); | |
| while (m.find()) { | |
| String val = (group <= 0 || group > m.groupCount()) ? m.group(0) : m.group(group); | |
| if (val != null) out_request_body.computeIfAbsent(name, k -> new java.util.ArrayList<>()).add(val); | |
| } | |
| } | |
| // --- extract from source.response headers | |
| for (Object[] ex : EXTRACT_RESPONSE_HEADERS) { | |
| if (ex == null || ex.length < 2) continue; | |
| String name = (String) ex[0]; | |
| String regex = (String) ex[1]; | |
| int group = (ex.length >= 3 && ex[2] != null) | |
| ? (ex[2] instanceof Number ? ((Number)ex[2]).intValue() : Integer.parseInt(String.valueOf(ex[2]))) | |
| : 0; | |
| if (name == null || name.trim().isEmpty() || regex == null || regex.trim().isEmpty()) continue; | |
| var m = java.util.regex.Pattern.compile(regex, extractorFlags).matcher(srcResHeadersJoined); | |
| while (m.find()) { | |
| String val = (group <= 0 || group > m.groupCount()) ? m.group(0) : m.group(group); | |
| if (val != null) out_response_headers.computeIfAbsent(name, k -> new java.util.ArrayList<>()).add(val); | |
| } | |
| } | |
| // --- extract from source.response body | |
| for (Object[] ex : EXTRACT_RESPONSE_BODY) { | |
| if (ex == null || ex.length < 2) continue; | |
| String name = (String) ex[0]; | |
| String regex = (String) ex[1]; | |
| int group = (ex.length >= 3 && ex[2] != null) | |
| ? (ex[2] instanceof Number ? ((Number)ex[2]).intValue() : Integer.parseInt(String.valueOf(ex[2]))) | |
| : 0; | |
| if (name == null || name.trim().isEmpty() || regex == null || regex.trim().isEmpty()) continue; | |
| var m = java.util.regex.Pattern.compile(regex, extractorFlags).matcher(srcResBody); | |
| while (m.find()) { | |
| String val = (group <= 0 || group > m.groupCount()) ? m.group(0) : m.group(group); | |
| if (val != null) out_response_body.computeIfAbsent(name, k -> new java.util.ArrayList<>()).add(val); | |
| } | |
| } | |
| // Logging (source) | |
| logging.logToOutput("Matched source: " + srcReq.method() + " " + srcReq.url()); | |
| logging.logToOutput("[Extract] source request_headers keys: " + out_request_headers.keySet()); | |
| logging.logToOutput("[Extract] source request_body keys: " + out_request_body.keySet()); | |
| logging.logToOutput("[Extract] source response_headers keys: " + out_response_headers.keySet()); | |
| logging.logToOutput("[Extract] source response_body keys: " + out_response_body.keySet()); | |
| // --------------------------- | |
| // ---- APPLY to current Repeater request (destination) ---- | |
| // This is the request you are editing right now in Repeater. | |
| var dstReq = requestResponse.request(); | |
| String dstBody = ""; | |
| try { dstBody = dstReq.bodyToString(); } catch (Exception ignore) {} | |
| // Pull extracted values (first value per name) | |
| String newCookie = out_request_headers.containsKey("cookie") ? out_request_headers.get("cookie").get(0) : null; | |
| String auraContext = out_request_body.containsKey("aura.context") ? out_request_body.get("aura.context").get(0) : null; | |
| String auraToken = out_request_body.containsKey("aura.token") ? out_request_body.get("aura.token").get(0) : null; | |
| // Replace existing x-www-form-urlencoded params (if present) | |
| String newDstBody = dstBody; | |
| if (auraContext != null) { | |
| String repl = java.util.regex.Matcher.quoteReplacement(auraContext); | |
| newDstBody = newDstBody.replaceAll("(?s)(^|&)(aura\\.context)=[^&]*", "$1$2=" + repl); | |
| } | |
| if (auraToken != null) { | |
| String repl = java.util.regex.Matcher.quoteReplacement(auraToken); | |
| newDstBody = newDstBody.replaceAll("(?s)(^|&)(aura\\.token)=[^&]*", "$1$2=" + repl); | |
| } | |
| // OPTIONAL: ADD_IF_MISSING (commented). If the param does not exist and you want to add it: | |
| // if (auraContext != null && !newDstBody.matches("(?s).*([&^])aura\\.context=.*")) { | |
| // newDstBody = (newDstBody.isEmpty() ? "" : newDstBody + "&") + "aura.context=" + auraContext; | |
| // } | |
| // if (auraToken != null && !newDstBody.matches("(?s).*([&^])aura\\.token=.*")) { | |
| // newDstBody = (newDstBody.isEmpty() ? "" : newDstBody + "&") + "aura.token=" + auraToken; | |
| // } | |
| // Build updated destination request | |
| var updatedDst = dstReq; | |
| if (newCookie != null) { | |
| updatedDst = updatedDst.withUpdatedHeader("Cookie", newCookie); | |
| } | |
| updatedDst = updatedDst.withBody(newDstBody); | |
| // Push to editor | |
| httpEditor.requestPane().set(updatedDst); | |
| // Optional: log a short diff preview | |
| String bodyBefore = dstBody.length() > 160 ? dstBody.substring(0,160) + "..." : dstBody; | |
| String bodyAfter = newDstBody.length() > 160 ? newDstBody.substring(0,160) + "..." : newDstBody; | |
| logging.logToOutput("[Apply] Cookie replaced? " + (newCookie != null)); | |
| logging.logToOutput("[Apply] Body before: " + bodyBefore); | |
| logging.logToOutput("[Apply] Body after : " + bodyAfter); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment