From f74ee962a3f9787596e7531d1ae07f8269b8fae7 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Thu, 9 May 2024 15:29:17 +0000 Subject: [PATCH 1/8] Refactor and update --- pom.xml | 21 ++- src/main/java/BurpExtension/BurpExtender.java | 10 +- src/main/java/BurpExtension/ContextMenu.java | 85 +++++---- src/main/java/BurpExtension/JWTScanCheck.java | 128 -------------- .../BurpExtension/JwtAuditIssueEquator.java | 28 +++ .../java/BurpExtension/JwtAuditIssues.java | 50 +++--- .../java/BurpExtension/JwtInsertionPoint.java | 75 -------- .../JwtInsertionPointProvider.java | 47 +++++ src/main/java/BurpExtension/JwtModifier.java | 13 +- src/main/java/BurpExtension/JwtScanCheck.java | 161 ++++++++++++++++++ 10 files changed, 337 insertions(+), 281 deletions(-) delete mode 100644 src/main/java/BurpExtension/JWTScanCheck.java create mode 100644 src/main/java/BurpExtension/JwtAuditIssueEquator.java delete mode 100644 src/main/java/BurpExtension/JwtInsertionPoint.java create mode 100644 src/main/java/BurpExtension/JwtInsertionPointProvider.java create mode 100644 src/main/java/BurpExtension/JwtScanCheck.java diff --git a/pom.xml b/pom.xml index 9a994a2..93708f0 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.compass-security JWT-scanner - 1.0-SNAPSHOT + 1.0.4 17 @@ -17,7 +17,12 @@ net.portswigger.burp.extensions montoya-api - 2023.3 + 2023.12.1 + + + org.apache.commons + commons-collections4 + 4.5.0-M1 io.jsonwebtoken @@ -32,22 +37,22 @@ io.jsonwebtoken jjwt-jackson - 0.11.5 + 0.12.5 org.json json - 20210307 + 20231013 org.bouncycastle - bcpkix-jdk15on - 1.57 + bcpkix-jdk18on + 1.78.1 org.bouncycastle - bcprov-jdk15on - 1.57 + bcprov-jdk18on + 1.78.1 com.nimbusds diff --git a/src/main/java/BurpExtension/BurpExtender.java b/src/main/java/BurpExtension/BurpExtender.java index 87e2d71..94d379b 100644 --- a/src/main/java/BurpExtension/BurpExtender.java +++ b/src/main/java/BurpExtension/BurpExtender.java @@ -2,7 +2,6 @@ import burp.api.montoya.BurpExtension; import burp.api.montoya.MontoyaApi; -import burp.api.montoya.logging.Logging; //Burp will auto-detect and load any class that extends BurpExtension. public class BurpExtender implements BurpExtension @@ -10,14 +9,9 @@ public class BurpExtender implements BurpExtension @Override public void initialize(MontoyaApi api) { - // set extension name api.extension().setName("JWT-scanner"); api.userInterface().registerContextMenuItemsProvider(new ContextMenu(api)); - - Logging logging = api.logging(); - logging.raiseInfoEvent("JWT-scanner loaded."); - - api.scanner().registerScanCheck(new JWTScanCheck(api)); - + api.scanner().registerScanCheck(new JwtScanCheck(api)); + api.scanner().registerInsertionPointProvider(new JwtInsertionPointProvider(api)); } } \ No newline at end of file diff --git a/src/main/java/BurpExtension/ContextMenu.java b/src/main/java/BurpExtension/ContextMenu.java index 36e20c7..c2f2b4d 100644 --- a/src/main/java/BurpExtension/ContextMenu.java +++ b/src/main/java/BurpExtension/ContextMenu.java @@ -9,16 +9,13 @@ package BurpExtension; import burp.api.montoya.MontoyaApi; -import burp.api.montoya.core.Range; import burp.api.montoya.core.ToolType; -import burp.api.montoya.http.Http; import burp.api.montoya.http.message.HttpRequestResponse; -import burp.api.montoya.scanner.Scanner; -import burp.api.montoya.ui.Selection; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; import burp.api.montoya.ui.contextmenu.ContextMenuEvent; import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; -import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse; -import burp.api.montoya.ui.editor.HttpRequestEditor; +import burp.api.montoya.scanner.AuditResult; +import burp.api.montoya.scanner.audit.issues.AuditIssue; import javax.swing.*; import java.awt.*; @@ -27,18 +24,18 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import org.apache.commons.collections4.IterableUtils; + public class ContextMenu implements ContextMenuItemsProvider { - private final MontoyaApi api; - private Scanner scanner; private final Executor executor = Executors.newSingleThreadExecutor(); + private final JwtAuditIssueEquator jwtAuditIssueEquator = new JwtAuditIssueEquator(); + public ContextMenu(MontoyaApi api) { - this.api = api; - scanner = api.scanner(); } @Override @@ -46,20 +43,18 @@ public List provideMenuItems(ContextMenuEvent event) { if (event.isFromTool(ToolType.PROXY, ToolType.REPEATER, ToolType.TARGET, ToolType.LOGGER, ToolType.INTRUDER)) { - MessageEditorHttpRequestResponse editorHttpRequestResponse = null; + List menuItemList = new ArrayList<>(); + HttpRequestResponse requestResponse; + // determine if context menu is triggered on message editor boolean editorIsPresent = event.messageEditorRequestResponse().isPresent(); - - List menuItemList = new ArrayList<>(); - if (editorIsPresent) { - editorHttpRequestResponse = event.messageEditorRequestResponse().get(); - requestResponse = editorHttpRequestResponse.requestResponse(); + requestResponse = event.messageEditorRequestResponse().get().requestResponse(); } else { List selectedRequests = event.selectedRequestResponses(); - // only 1 request is support at this time + // only 1 request is support at this time, otherwise no menu item is shown if (selectedRequests.size() == 1) { requestResponse = selectedRequests.get(0); } else { @@ -67,28 +62,54 @@ public List provideMenuItems(ContextMenuEvent event) } } - // Autodetect JWT - JMenuItem retrieveRequestItem = new JMenuItem("Autodetect JWT"); + JwtScanCheck scan = new JwtScanCheck(api); + JwtInsertionPointProvider insertionPointProvider = new JwtInsertionPointProvider(api); - JWTScanCheck scan = new JWTScanCheck(api); - JwtInsertionPoint insertionPoint = new JwtInsertionPoint(api,requestResponse.request()); - retrieveRequestItem.addActionListener(l -> SwingUtilities.invokeLater(() -> - this.executor.execute(() -> scan.activeAudit(requestResponse,insertionPoint))) + // Autodetect JWT + JMenuItem autodetectMenuItem = new JMenuItem("Autodetect JWT"); + autodetectMenuItem.addActionListener(l -> SwingUtilities.invokeLater(() -> + this.executor.execute(() -> { + List auditInsertionPoints = insertionPointProvider.provideInsertionPoints(requestResponse); + + for (AuditInsertionPoint insertionPoint : auditInsertionPoints) { + AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint); + + for (AuditIssue issue : auditResult.auditIssues()) { + if (!IterableUtils.contains(api.siteMap().issues(), issue, jwtAuditIssueEquator)) { + api.siteMap().add(issue); + } + } + } + })) ); - menuItemList.add(retrieveRequestItem); + menuItemList.add(autodetectMenuItem); // Selected JWT - if (editorIsPresent && editorHttpRequestResponse.selectionOffsets().isPresent()) { - JMenuItem retrieveSelectedRequestItem = new JMenuItem("Selected JWT"); + if (editorIsPresent && event.messageEditorRequestResponse().get().selectionOffsets().isPresent()) { + int startindex = event.messageEditorRequestResponse().get().selectionOffsets().get().startIndexInclusive(); int endindex = event.messageEditorRequestResponse().get().selectionOffsets().get().endIndexExclusive(); - JWTScanCheck scanSelected = new JWTScanCheck(api); - JwtInsertionPoint insertionPointSelected = new JwtInsertionPoint(api,requestResponse.request(),startindex,endindex); - retrieveSelectedRequestItem.addActionListener(l -> SwingUtilities.invokeLater(() -> - this.executor.execute(() -> scanSelected.activeAudit(requestResponse,insertionPointSelected))) - ); - menuItemList.add(retrieveSelectedRequestItem); + List auditInsertionPoints = insertionPointProvider.provideInsertionPointsInSelection(requestResponse, startindex, endindex); + + if (!auditInsertionPoints.isEmpty()) { + JMenuItem retrieveSelectedRequestItem = new JMenuItem("Selected JWT"); + retrieveSelectedRequestItem.addActionListener(l -> SwingUtilities.invokeLater(() -> + this.executor.execute(() -> { + for (AuditInsertionPoint insertionPoint : auditInsertionPoints) { + AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint); + + for (AuditIssue issue : auditResult.auditIssues()) { + if (!IterableUtils.contains(api.siteMap().issues(), issue, jwtAuditIssueEquator)) { + api.siteMap().add(issue); + } + } + } + })) + ); + + menuItemList.add(retrieveSelectedRequestItem); + } } return menuItemList; diff --git a/src/main/java/BurpExtension/JWTScanCheck.java b/src/main/java/BurpExtension/JWTScanCheck.java deleted file mode 100644 index 8bc1a4f..0000000 --- a/src/main/java/BurpExtension/JWTScanCheck.java +++ /dev/null @@ -1,128 +0,0 @@ -package BurpExtension; - -import burp.api.montoya.MontoyaApi; -import burp.api.montoya.http.message.HttpRequestResponse; -import burp.api.montoya.http.message.requests.HttpRequest; -import burp.api.montoya.scanner.AuditResult; -import burp.api.montoya.scanner.ConsolidationAction; -import burp.api.montoya.scanner.ScanCheck; -import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; -import burp.api.montoya.scanner.audit.issues.AuditIssue; - -import java.util.ArrayList; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import static burp.api.montoya.scanner.AuditResult.auditResult; -import static burp.api.montoya.core.ByteArray.byteArray; -import static burp.api.montoya.scanner.ConsolidationAction.KEEP_BOTH; -import static burp.api.montoya.scanner.ConsolidationAction.KEEP_EXISTING; - -class JWTScanCheck implements ScanCheck -{ - private final MontoyaApi api; - - JWTScanCheck(MontoyaApi api) - { - this.api = api; - } - - ArrayList algoList = new ArrayList<>(); - - - @Override - public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint) - { - JwtModifier jwtModifier = new JwtModifier(api); - String origJwt = ""; - String regex = "(ey[a-zA-Z0-9_=]+)\\.(ey[a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-\\+\\/=]*)"; - Pattern pattern = Pattern.compile(regex); - HttpRequest req = baseRequestResponse.request(); - Matcher matcher = pattern.matcher(req.toString()); - - if (matcher.find()) { - int startIndex = matcher.start(); - int endIndex = matcher.end(); - origJwt = req.toString().substring(startIndex,endIndex); - // Validate if the origJwt is still valid - if (jwtModifier.isJwtNotExpired(origJwt)) { - api.logging().logToOutput("using JWT:\n" + origJwt); - } else { - api.logging().raiseErrorEvent("JWT expired, please choose a valid one!"); - api.logging().logToOutput("JWT expired, please choose a valid one!"); - } - - } else { - api.logging().logToError("No JWT found."); - } - - HttpRequest checkRequestNoSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.removeSignature(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseNoSig = api.http().sendRequest(checkRequestNoSig); - if (checkRequestResponseNoSig.response().statusCode() == 200){ - api.siteMap().add(JwtAuditIssues.withoutSignature(baseRequestResponse.request().url(), checkRequestResponseNoSig)); - } - - HttpRequest checkRequestSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.wrongSignature(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestSig); - if (checkRequestResponseSig.response().statusCode() == 200){ - api.siteMap().add(JwtAuditIssues.invalidSignature(baseRequestResponse.request().url(), checkRequestResponseSig)); - } - - this.permute("none", ""); - - for(int i = 0; i< algoList.size(); i++) { - HttpRequest checkRequestNone = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.algNone(origJwt, algoList.get(i)))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseNone = api.http().sendRequest(checkRequestNone); - if (checkRequestResponseNone.response().statusCode() == 200) { - api.siteMap().add(JwtAuditIssues.getAlgNone(baseRequestResponse.request().url(), checkRequestResponseNone)); - } - } - - HttpRequest checkRequestEmpty = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.emptyPassword(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseEmpty = api.http().sendRequest(checkRequestEmpty); - if (checkRequestResponseEmpty.response().statusCode() == 200){ - api.siteMap().add(JwtAuditIssues.emptyPassword(baseRequestResponse.request().url(), checkRequestResponseEmpty)); - } - - HttpRequest checkRequestEcdsa = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.invalidEcdsa(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseEcdsa = api.http().sendRequest(checkRequestEcdsa); - if (checkRequestResponseEcdsa.response().statusCode() == 200){ - api.siteMap().add(JwtAuditIssues.invalidEcdsa(baseRequestResponse.request().url(), checkRequestResponseEcdsa)); - } - - HttpRequest checkRequestJwks = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.jwksInjection(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseJwks = api.http().sendRequest(checkRequestJwks); - if (checkRequestResponseJwks.response().statusCode() == 200){ - api.siteMap().add(JwtAuditIssues.jwksInjection(baseRequestResponse.request().url(), checkRequestResponseJwks)); - } - - return auditResult(); - } - - @Override - public AuditResult passiveAudit(HttpRequestResponse baseRequestResponse) - { - return auditResult(); - } - - @Override - public ConsolidationAction consolidateIssues(AuditIssue newIssue, AuditIssue existingIssue) - { - return existingIssue.name().equals(newIssue.name()) ? KEEP_EXISTING : KEEP_BOTH; - } - private void permute(String ip, String op) - { - // base case - if(ip.length() == 0){ - this.algoList.add(op); - return; - } - // pick lower and uppercase - String ch = ("" + ip.charAt(0)).toLowerCase(); - String ch2 = ("" + ip.charAt(0)).toUpperCase(); - ip = ip.substring(1, ip.length()) ; - - permute(ip, op + ch); - permute(ip, op + ch2); - } -} diff --git a/src/main/java/BurpExtension/JwtAuditIssueEquator.java b/src/main/java/BurpExtension/JwtAuditIssueEquator.java new file mode 100644 index 0000000..a7eaa8a --- /dev/null +++ b/src/main/java/BurpExtension/JwtAuditIssueEquator.java @@ -0,0 +1,28 @@ +package BurpExtension; + +import burp.api.montoya.scanner.audit.issues.AuditIssue; +import org.apache.commons.collections4.Equator; +import org.apache.commons.collections4.functors.DefaultEquator; + +public class JwtAuditIssueEquator implements Equator { + + @Override + public boolean equate(AuditIssue auditIssue, AuditIssue t1) { + // name of issue needs to match + if (auditIssue.name().equals(t1.name())) { + // HTTP Service needs to match + if (auditIssue.httpService().equals(t1.httpService())) { + // path needs to match + if (auditIssue.requestResponses().get(0).request().path().equals(t1.requestResponses().get(0).request().path())) { + return true; + } + } + } + return false; + } + + @Override + public int hash(AuditIssue auditIssue) { + return DefaultEquator.INSTANCE.hash(auditIssue); + } +} diff --git a/src/main/java/BurpExtension/JwtAuditIssues.java b/src/main/java/BurpExtension/JwtAuditIssues.java index 2a61b4c..90ff0a2 100644 --- a/src/main/java/BurpExtension/JwtAuditIssues.java +++ b/src/main/java/BurpExtension/JwtAuditIssues.java @@ -9,16 +9,29 @@ public abstract class JwtAuditIssues { - public static final AuditIssue withoutSignature(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue expired(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + return auditIssue("Expired JWT accepted", + "The server accepts JWTs that are expired \n " + + "An attacker can use an expired JWT.", + "A standard library should be used to handle the JWT in order to prevent " + + "implementation errors and vulnerabilities.\n" + + "There, the signature verification must be enabled.", + baseRequestResponse.request().url(), + AuditIssueSeverity.HIGH, + AuditIssueConfidence.FIRM, + null, + null, + AuditIssueSeverity.HIGH, + checkRequestResponse); + } + + public static AuditIssue withoutSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("JWT Signature not required", "The server accepts JWTs that are not signed \n " + "An attacker can forge a JWT and take over any account and role in the application. " + "This can be used to elevate privileges for instance.", - "A standard library should be used to handle the JWT in order to prevent " + - "implementation errors and vulnerabilities.\n" + - "There, the signature verification must be enabled.", - // baseRequestResponse.request().url(), - url, + "The server should not accept any expired JWTs.", + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, @@ -27,7 +40,7 @@ public static final AuditIssue withoutSignature(String url, HttpRequestResponse checkRequestResponse); } - public static final AuditIssue getAlgNone(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue getAlgNone(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("Algorithm none JWT attack", "The server accepts JWTs created with the \"none\" algorithm. \n " + "The JWT ànoneà algorithm is a waz of creating a JWT without adding a signature. " + @@ -36,8 +49,7 @@ public static final AuditIssue getAlgNone(String url, HttpRequestResponse checkR "The server should not accept tokens that were created using the \"none\" algorithm. " + "(Note that upper- and lower-case variations such as \"None\" or \"nONe\" must not be accepted either.)\n" + "The server should ignore the \"alg\" header claim and instead define a fixed signature algorithm in the application code.", - // baseRequestResponse.request().url(), - url, + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, @@ -46,15 +58,14 @@ public static final AuditIssue getAlgNone(String url, HttpRequestResponse checkR checkRequestResponse); } - public static final AuditIssue invalidSignature(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue invalidSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("Invalid JWT Signature", "The signature of the JSON Web Tokens (JWT) is not checked by the server.\n" + "An attacker can forge a JWT and take over any account and role in the application. " + "This can be used to elevate privileges for instance.", "A standard library should be used to handle the JWT in order to prevent implementation errors and vulnerabilities.\n" + "There, the signature verification must be enabled.", - // baseRequestResponse.request().url(), - url, + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, @@ -63,7 +74,7 @@ public static final AuditIssue invalidSignature(String url, HttpRequestResponse checkRequestResponse); } - public static final AuditIssue emptyPassword(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue emptyPassword(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("JWT signed with empty password", "The signature of the JSON Web Tokens (JWT) is created with an empty password.\n" + "An attacker can forge a JWT with an empty password and take over any account and role in the application. " + @@ -71,8 +82,7 @@ public static final AuditIssue emptyPassword(String url, HttpRequestResponse che "A standard library should be used to handle the JWT in order to prevent implementation errors and vulnerabilities.\n" + "No Empty secrets should be used to create signatures.\n"+ "The signature verification must be enabled.", - // baseRequestResponse.request().url(), - url, + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, @@ -80,15 +90,14 @@ public static final AuditIssue emptyPassword(String url, HttpRequestResponse che AuditIssueSeverity.HIGH, checkRequestResponse); } - public static final AuditIssue invalidEcdsa(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue invalidEcdsa(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("JWT signed invalid ECDSA parameters", "CVE-2022-21449 Vulnerability in the Oracle Java SE, Oracle GraalVM Enterprise Edition product of Oracle Java SE.\n" + "Easily exploitable vulnerability allows unauthenticated attacker with network access via multiple protocols to compromise Oracle Java SE," + " Oracle GraalVM Enterprise Edition. Successful attacks of this vulnerability can result in unauthorized creation, " + "deletion or modification access to critical data or all Oracle Java SE, Oracle GraalVM Enterprise Edition accessible data.", "Install available patch and refer to vendor advisory: https://www.oracle.com/security-alerts/cpuapr2022.html", - // baseRequestResponse.request().url(), - url, + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, @@ -96,12 +105,11 @@ public static final AuditIssue invalidEcdsa(String url, HttpRequestResponse chec AuditIssueSeverity.HIGH, checkRequestResponse); } - public static final AuditIssue jwksInjection(String url, HttpRequestResponse checkRequestResponse){ + public static AuditIssue jwksInjection(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ return auditIssue("JWT JWKs Injection", "It is possible to include the used public key in the JWK value of the header. The Application takes the included public key to validate the signature", "The JWK provided in the header should not be used to validate the signature.", - // baseRequestResponse.request().url(), - url, + baseRequestResponse.request().url(), AuditIssueSeverity.HIGH, AuditIssueConfidence.FIRM, null, diff --git a/src/main/java/BurpExtension/JwtInsertionPoint.java b/src/main/java/BurpExtension/JwtInsertionPoint.java deleted file mode 100644 index 33e0a76..0000000 --- a/src/main/java/BurpExtension/JwtInsertionPoint.java +++ /dev/null @@ -1,75 +0,0 @@ -package BurpExtension; - -import burp.api.montoya.MontoyaApi; -import burp.api.montoya.core.ByteArray; -import burp.api.montoya.core.Range; -import burp.api.montoya.http.HttpService; -import burp.api.montoya.http.message.requests.HttpRequest; -import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; -import burp.api.montoya.utilities.Utilities; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class JwtInsertionPoint implements AuditInsertionPoint { - private final MontoyaApi api; - private final HttpRequest requestResponse; - private String baseValue; - private final Utilities utilities; - - private String prefix; - private String suffix; - - private int headerPosition; - - JwtInsertionPoint(MontoyaApi api, HttpRequest baseHttpRequest){ - this.requestResponse = baseHttpRequest; - this.api = api; - this.utilities = api.utilities(); - - String regex = "(ey[a-zA-Z0-9_=]+)\\.(ey[a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-\\+\\/=]*)"; - Pattern pattern = Pattern.compile(regex); - String input = baseHttpRequest.toString(); - Matcher matcher = pattern.matcher(input); - - if (matcher.find()) { - int startIndex = matcher.start(); - int endIndex = matcher.end(); - this.prefix = input.substring(0, startIndex); - this.suffix = input.substring(endIndex,input.length()); - } else { - api.logging().logToError("No JWT found."); - } - } - - JwtInsertionPoint(MontoyaApi api, HttpRequest baseHttpRequest, int startIndex, int endIndex){ - this.requestResponse = baseHttpRequest; - this.api = api; - this.utilities = api.utilities(); - String input = baseHttpRequest.toString(); - - this.prefix = input.substring(0, startIndex); - this.suffix = input.substring(endIndex,input.length()); - } - @Override - public String name() { - return "JWT-Authorization-Header"; - } - - @Override - public String baseValue() { - return "demo-jwt"; - } - - @Override - public HttpRequest buildHttpRequestWithPayload(ByteArray payload){ - HttpRequest req = HttpRequest.httpRequest(this.prefix + payload.toString() + this.suffix); - HttpService service = this.requestResponse.httpService(); - return req.withService(service); - } - - @Override - public List issueHighlights(ByteArray byteArray) { - return null; - } -} diff --git a/src/main/java/BurpExtension/JwtInsertionPointProvider.java b/src/main/java/BurpExtension/JwtInsertionPointProvider.java new file mode 100644 index 0000000..ac0994c --- /dev/null +++ b/src/main/java/BurpExtension/JwtInsertionPointProvider.java @@ -0,0 +1,47 @@ +package BurpExtension; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPointProvider; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class JwtInsertionPointProvider implements AuditInsertionPointProvider { + private final MontoyaApi api; + + JwtInsertionPointProvider(MontoyaApi api) + { + this.api = api; + } + + private final Pattern jwtPattern = Pattern.compile("(ey[a-zA-Z0-9_=]+)\\.(ey[a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-+/=]*)"); + + @Override + public List provideInsertionPoints(HttpRequestResponse httpRequestResponse) { + return provideInsertionPointsInSelection(httpRequestResponse, 0, 0); + } + + public List provideInsertionPointsInSelection(HttpRequestResponse httpRequestResponse, int selectionStart, int selectionEnd) { + List auditInsertionPoints = new ArrayList<>(); + + String input; + + if (selectionEnd > selectionStart) { + input = httpRequestResponse.request().toString().substring(selectionStart, selectionEnd); + } else { + input = httpRequestResponse.request().toString(); + } + + Matcher matcher = jwtPattern.matcher(input); + + while (matcher.find()) { + auditInsertionPoints.add(AuditInsertionPoint.auditInsertionPoint("Detected JWT", httpRequestResponse.request(), selectionStart + matcher.start(), selectionStart + matcher.end())); + } + + return auditInsertionPoints; + } +} diff --git a/src/main/java/BurpExtension/JwtModifier.java b/src/main/java/BurpExtension/JwtModifier.java index b5191a4..d76cd34 100644 --- a/src/main/java/BurpExtension/JwtModifier.java +++ b/src/main/java/BurpExtension/JwtModifier.java @@ -5,7 +5,6 @@ import javax.crypto.Mac; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; -import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.*; import java.security.SignatureException; @@ -51,8 +50,7 @@ public String removeSignature(String jwt){ String header = jwtParts[0]; String claims = jwtParts[1]; - String concatenated = header + '.' + claims; - return concatenated; + return header + '.' + claims; } public String wrongSignature(String jwt){ @@ -99,8 +97,7 @@ public String jwksInjection(String jwt){ headerObject.put("jwk",jwk.toJSONObject()); String header = base64UrlEncodeNoPadding(headerObject.toString()); - final String newjwt = signJWTRSA(header, jwtParts[1], keyPair.getPrivate()); - return newjwt; + return signJWTRSA(header, jwtParts[1], keyPair.getPrivate()); } catch (Exception e) { api.logging().logToError(e.getMessage()); @@ -119,12 +116,11 @@ private static String base64UrlEncodeNoPadding(String input) { private static String base64UrlEncodeNoPadding(byte[] input) { return Base64.getUrlEncoder().withoutPadding().encodeToString(input); } - private static KeyPair generateRS256KeyPair() throws NoSuchAlgorithmException, IOException { + private static KeyPair generateRS256KeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); - KeyPair keyPair = keyPairGenerator.generateKeyPair(); - return keyPair; + return keyPairGenerator.generateKeyPair(); } private static String signJWTRSA(String header, String payload, PrivateKey privateKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { @@ -149,7 +145,6 @@ is ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || // Check if header and claim are already encoded. if (!header.startsWith("ey")) { header = encodeBase64Url(header); - String encodedClaim = encodeBase64Url(claim); } if (!claim.startsWith("ey")) { claim = encodeBase64Url(claim); diff --git a/src/main/java/BurpExtension/JwtScanCheck.java b/src/main/java/BurpExtension/JwtScanCheck.java new file mode 100644 index 0000000..0ec3d01 --- /dev/null +++ b/src/main/java/BurpExtension/JwtScanCheck.java @@ -0,0 +1,161 @@ +package BurpExtension; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.http.message.HttpRequestResponse; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.scanner.AuditResult; +import burp.api.montoya.scanner.ConsolidationAction; +import burp.api.montoya.scanner.ScanCheck; +import burp.api.montoya.scanner.audit.insertionpoint.AuditInsertionPoint; +import burp.api.montoya.scanner.audit.issues.AuditIssue; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static burp.api.montoya.scanner.AuditResult.auditResult; +import static burp.api.montoya.core.ByteArray.byteArray; +import static burp.api.montoya.scanner.ConsolidationAction.KEEP_BOTH; +import static burp.api.montoya.scanner.ConsolidationAction.KEEP_EXISTING; +import static java.util.Collections.emptyList; + +class JwtScanCheck implements ScanCheck +{ + private final MontoyaApi api; + + JwtScanCheck(MontoyaApi api) + { + this.api = api; + } + + ArrayList algoList = new ArrayList<>(); + + + @Override + public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint) + { + // initialise list of AuditIssue + List auditIssueList = new ArrayList<>(); + + // obtain baseValue of insertion point + String origJwt = auditInsertionPoint.baseValue(); + + // verify that the insertion point represents a JWT as this ScanCheck performs transformations + String jwtRegex = "(ey[a-zA-Z0-9_=]+)\\.(ey[a-zA-Z0-9_=]+)\\.([a-zA-Z0-9_\\-+/=]*)"; + Pattern pattern = Pattern.compile(jwtRegex); + Matcher matcher = pattern.matcher(origJwt); + + // insertion point has a valid JWT syntax + if (matcher.find()) { + + JwtModifier jwtModifier = new JwtModifier(api); + + // determine if JWT is not expired + if (jwtModifier.isJwtNotExpired(origJwt)) { + // Debug output + api.logging().logToOutput("JWT in original request:\n" + origJwt); + } else { + api.logging().raiseInfoEvent("Expired JWT identified. Use a valid token for additional checks!"); + + // send the expired JWT again to determine if the server accepts it + HttpRequest checkRequestExpired = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(origJwt)).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestExpired); + if (checkRequestResponseSig.response().statusCode() == 200){ + auditIssueList.add(JwtAuditIssues.expired(baseRequestResponse, checkRequestResponseSig)); + } + + return auditResult(auditIssueList); + } + + // send JWT without signature + HttpRequest checkRequestNoSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.removeSignature(origJwt))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseNoSig = api.http().sendRequest(checkRequestNoSig); + if (requestWasSuccessful(checkRequestResponseNoSig)){ + auditIssueList.add(JwtAuditIssues.withoutSignature(baseRequestResponse, checkRequestResponseNoSig)); + + // no need for further checks + return auditResult(auditIssueList); + } + + // send JWT with invalid signature + HttpRequest checkRequestSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.wrongSignature(origJwt))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestSig); + if (requestWasSuccessful(checkRequestResponseSig)) { + auditIssueList.add(JwtAuditIssues.invalidSignature(baseRequestResponse, checkRequestResponseSig)); + + // no need for further checks + return auditResult(auditIssueList); + } + + // send JWT with none algorithm + this.permute("none", ""); + for (String s : algoList) { + HttpRequest checkRequestNone = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.algNone(origJwt, s))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseNone = api.http().sendRequest(checkRequestNone); + if (requestWasSuccessful(checkRequestResponseNone)) { + auditIssueList.add(JwtAuditIssues.getAlgNone(baseRequestResponse, checkRequestResponseNone)); + + // stop after a valid none permutation has been found + break; + } + } + + // send JWT with empty password + HttpRequest checkRequestEmpty = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.emptyPassword(origJwt))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseEmpty = api.http().sendRequest(checkRequestEmpty); + if (requestWasSuccessful(checkRequestResponseEmpty)){ + auditIssueList.add(JwtAuditIssues.emptyPassword(baseRequestResponse, checkRequestResponseEmpty)); + } + + // send JWT with invalid ECDSA signature + HttpRequest checkRequestEcdsa = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.invalidEcdsa(origJwt))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseEcdsa = api.http().sendRequest(checkRequestEcdsa); + if (requestWasSuccessful(checkRequestResponseEcdsa)){ + auditIssueList.add(JwtAuditIssues.invalidEcdsa(baseRequestResponse, checkRequestResponseEcdsa)); + } + + // send JWT with JWKS injection + HttpRequest checkRequestJwks = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.jwksInjection(origJwt))).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseJwks = api.http().sendRequest(checkRequestJwks); + if (requestWasSuccessful(checkRequestResponseJwks)){ + auditIssueList.add(JwtAuditIssues.jwksInjection(baseRequestResponse, checkRequestResponseJwks)); + } + } + + return auditResult(auditIssueList); + } + + boolean requestWasSuccessful(HttpRequestResponse requestResponse) { + return (requestResponse.response().statusCode() == 200); + } + + @Override + public AuditResult passiveAudit(HttpRequestResponse baseRequestResponse) + { + List auditIssueList = emptyList(); + return auditResult(auditIssueList); + } + + @Override + public ConsolidationAction consolidateIssues(AuditIssue newIssue, AuditIssue existingIssue) + { + return existingIssue.name().equals(newIssue.name()) ? KEEP_EXISTING : KEEP_BOTH; + } + + private void permute(String ip, String op) + { + // base case + if(ip.length() == 0){ + this.algoList.add(op); + return; + } + // pick lower and uppercase + String ch = ("" + ip.charAt(0)).toLowerCase(); + String ch2 = ("" + ip.charAt(0)).toUpperCase(); + ip = ip.substring(1, ip.length()) ; + + permute(ip, op + ch); + permute(ip, op + ch2); + } +} From a6bd356fc24e3c64b84b6775318f162a6d6fd8ee Mon Sep 17 00:00:00 2001 From: bcyrill Date: Fri, 10 May 2024 21:14:12 +0000 Subject: [PATCH 2/8] Update pom.xml --- pom.xml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 93708f0..75964b9 100644 --- a/pom.xml +++ b/pom.xml @@ -27,12 +27,12 @@ io.jsonwebtoken jjwt-impl - 0.11.5 + 0.12.5 io.jsonwebtoken jjwt-api - 0.11.5 + 0.12.5 io.jsonwebtoken @@ -62,14 +62,6 @@ - - maven-compiler-plugin - 3.1 - - 1.8 - 1.8 - - maven-assembly-plugin From f4847d18aaa340fc65331abae843a4a90f887d9c Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Mon, 13 May 2024 17:24:44 +0000 Subject: [PATCH 3/8] Remove Bouncy Castle dependency --- pom.xml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index 75964b9..50ece1a 100644 --- a/pom.xml +++ b/pom.xml @@ -28,6 +28,7 @@ io.jsonwebtoken jjwt-impl 0.12.5 + runtime io.jsonwebtoken @@ -38,22 +39,13 @@ io.jsonwebtoken jjwt-jackson 0.12.5 + runtime org.json json 20231013 - - org.bouncycastle - bcpkix-jdk18on - 1.78.1 - - - org.bouncycastle - bcprov-jdk18on - 1.78.1 - com.nimbusds nimbus-jose-jwt From 3d05799868170309550aa62aa74297eb25f05455 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Mon, 13 May 2024 17:26:23 +0000 Subject: [PATCH 4/8] Rename variable --- .../java/BurpExtension/JwtAuditIssues.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/main/java/BurpExtension/JwtAuditIssues.java b/src/main/java/BurpExtension/JwtAuditIssues.java index 90ff0a2..0edf810 100644 --- a/src/main/java/BurpExtension/JwtAuditIssues.java +++ b/src/main/java/BurpExtension/JwtAuditIssues.java @@ -9,7 +9,7 @@ public abstract class JwtAuditIssues { - public static AuditIssue expired(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue expired(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("Expired JWT accepted", "The server accepts JWTs that are expired \n " + "An attacker can use an expired JWT.", @@ -22,10 +22,10 @@ public static AuditIssue expired(HttpRequestResponse baseRequestResponse, HttpRe null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue withoutSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue withoutSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("JWT Signature not required", "The server accepts JWTs that are not signed \n " + "An attacker can forge a JWT and take over any account and role in the application. " + @@ -37,10 +37,10 @@ public static AuditIssue withoutSignature(HttpRequestResponse baseRequestRespons null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue getAlgNone(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue getAlgNone(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("Algorithm none JWT attack", "The server accepts JWTs created with the \"none\" algorithm. \n " + "The JWT ànoneà algorithm is a waz of creating a JWT without adding a signature. " + @@ -55,10 +55,10 @@ public static AuditIssue getAlgNone(HttpRequestResponse baseRequestResponse, Htt null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue invalidSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue invalidSignature(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("Invalid JWT Signature", "The signature of the JSON Web Tokens (JWT) is not checked by the server.\n" + "An attacker can forge a JWT and take over any account and role in the application. " + @@ -71,10 +71,10 @@ public static AuditIssue invalidSignature(HttpRequestResponse baseRequestRespons null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue emptyPassword(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue emptyPassword(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("JWT signed with empty password", "The signature of the JSON Web Tokens (JWT) is created with an empty password.\n" + "An attacker can forge a JWT with an empty password and take over any account and role in the application. " + @@ -88,9 +88,9 @@ public static AuditIssue emptyPassword(HttpRequestResponse baseRequestResponse, null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue invalidEcdsa(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue invalidEcdsa(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("JWT signed invalid ECDSA parameters", "CVE-2022-21449 Vulnerability in the Oracle Java SE, Oracle GraalVM Enterprise Edition product of Oracle Java SE.\n" + "Easily exploitable vulnerability allows unauthenticated attacker with network access via multiple protocols to compromise Oracle Java SE," + @@ -103,9 +103,9 @@ public static AuditIssue invalidEcdsa(HttpRequestResponse baseRequestResponse, H null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } - public static AuditIssue jwksInjection(HttpRequestResponse baseRequestResponse, HttpRequestResponse checkRequestResponse){ + public static AuditIssue jwksInjection(HttpRequestResponse baseRequestResponse, HttpRequestResponse markedRequestResponse){ return auditIssue("JWT JWKs Injection", "It is possible to include the used public key in the JWK value of the header. The Application takes the included public key to validate the signature", "The JWK provided in the header should not be used to validate the signature.", @@ -115,7 +115,7 @@ public static AuditIssue jwksInjection(HttpRequestResponse baseRequestResponse, null, null, AuditIssueSeverity.HIGH, - checkRequestResponse); + markedRequestResponse); } } From e99d3ebb184b76d8869b11df2e8f8e773a228754 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Mon, 13 May 2024 17:27:05 +0000 Subject: [PATCH 5/8] Cleanup imports --- src/main/java/BurpExtension/JwtModifier.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/BurpExtension/JwtModifier.java b/src/main/java/BurpExtension/JwtModifier.java index d76cd34..06fcdc2 100644 --- a/src/main/java/BurpExtension/JwtModifier.java +++ b/src/main/java/BurpExtension/JwtModifier.java @@ -1,5 +1,5 @@ package BurpExtension; -import io.jsonwebtoken.*; +import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import burp.api.montoya.MontoyaApi; import javax.crypto.Mac; @@ -12,8 +12,8 @@ import java.util.Base64; import com.nimbusds.jose.jwk.RSAKey; +import org.json.JSONObject; -import org.json.*; public class JwtModifier { private final MontoyaApi api; private final SecretKey dummyKey = Keys.secretKeyFor(SignatureAlgorithm.HS256); From 5912cfcb006129c7d33942d739ebc71137d895f5 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Mon, 13 May 2024 17:29:07 +0000 Subject: [PATCH 6/8] Add highlighting. Differentiate between ActiveScan/ContextMenu --- src/main/java/BurpExtension/ContextMenu.java | 4 +- src/main/java/BurpExtension/JwtScanCheck.java | 108 ++++++++++++------ 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/src/main/java/BurpExtension/ContextMenu.java b/src/main/java/BurpExtension/ContextMenu.java index c2f2b4d..1462f8a 100644 --- a/src/main/java/BurpExtension/ContextMenu.java +++ b/src/main/java/BurpExtension/ContextMenu.java @@ -72,7 +72,7 @@ public List provideMenuItems(ContextMenuEvent event) List auditInsertionPoints = insertionPointProvider.provideInsertionPoints(requestResponse); for (AuditInsertionPoint insertionPoint : auditInsertionPoints) { - AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint); + AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint,true); for (AuditIssue issue : auditResult.auditIssues()) { if (!IterableUtils.contains(api.siteMap().issues(), issue, jwtAuditIssueEquator)) { @@ -97,7 +97,7 @@ public List provideMenuItems(ContextMenuEvent event) retrieveSelectedRequestItem.addActionListener(l -> SwingUtilities.invokeLater(() -> this.executor.execute(() -> { for (AuditInsertionPoint insertionPoint : auditInsertionPoints) { - AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint); + AuditResult auditResult = scan.activeAudit(requestResponse,insertionPoint,true); for (AuditIssue issue : auditResult.auditIssues()) { if (!IterableUtils.contains(api.siteMap().issues(), issue, jwtAuditIssueEquator)) { diff --git a/src/main/java/BurpExtension/JwtScanCheck.java b/src/main/java/BurpExtension/JwtScanCheck.java index 0ec3d01..2097ca6 100644 --- a/src/main/java/BurpExtension/JwtScanCheck.java +++ b/src/main/java/BurpExtension/JwtScanCheck.java @@ -1,6 +1,9 @@ package BurpExtension; import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.Marker; +import burp.api.montoya.core.Range; import burp.api.montoya.http.message.HttpRequestResponse; import burp.api.montoya.http.message.requests.HttpRequest; import burp.api.montoya.scanner.AuditResult; @@ -33,8 +36,21 @@ class JwtScanCheck implements ScanCheck @Override - public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint) - { + public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint) { + return activeAudit(baseRequestResponse, auditInsertionPoint, false); + } + + private List markersForPayload(AuditInsertionPoint auditInsertionPoint, ByteArray payload) { + List highlights = auditInsertionPoint.issueHighlights(payload); + List markers = new ArrayList<>(highlights.size()); + for (Range range : highlights) { + markers.add(Marker.marker(range)); + } + + return markers; + } + + public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditInsertionPoint auditInsertionPoint, boolean fromContextMenu) { // initialise list of AuditIssue List auditIssueList = new ArrayList<>(); @@ -50,6 +66,7 @@ public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditIns if (matcher.find()) { JwtModifier jwtModifier = new JwtModifier(api); + ByteArray payload; // determine if JWT is not expired if (jwtModifier.isJwtNotExpired(origJwt)) { @@ -59,67 +76,94 @@ public AuditResult activeAudit(HttpRequestResponse baseRequestResponse, AuditIns api.logging().raiseInfoEvent("Expired JWT identified. Use a valid token for additional checks!"); // send the expired JWT again to determine if the server accepts it - HttpRequest checkRequestExpired = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(origJwt)).withService(baseRequestResponse.httpService()); + payload = byteArray(origJwt); + HttpRequest checkRequestExpired = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestExpired); if (checkRequestResponseSig.response().statusCode() == 200){ - auditIssueList.add(JwtAuditIssues.expired(baseRequestResponse, checkRequestResponseSig)); + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.expired(baseRequestResponse, checkRequestResponseSig.withRequestMarkers(markers))); } return auditResult(auditIssueList); } // send JWT without signature - HttpRequest checkRequestNoSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.removeSignature(origJwt))).withService(baseRequestResponse.httpService()); + payload = byteArray(jwtModifier.removeSignature(origJwt)); + HttpRequest checkRequestNoSig = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseNoSig = api.http().sendRequest(checkRequestNoSig); if (requestWasSuccessful(checkRequestResponseNoSig)){ - auditIssueList.add(JwtAuditIssues.withoutSignature(baseRequestResponse, checkRequestResponseNoSig)); + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.withoutSignature(baseRequestResponse, checkRequestResponseNoSig.withRequestMarkers(markers))); // no need for further checks return auditResult(auditIssueList); } - // send JWT with invalid signature - HttpRequest checkRequestSig = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.wrongSignature(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestSig); - if (requestWasSuccessful(checkRequestResponseSig)) { - auditIssueList.add(JwtAuditIssues.invalidSignature(baseRequestResponse, checkRequestResponseSig)); + // send JWT with invalid signature (skip for active scan) + if (fromContextMenu) { + payload = byteArray(jwtModifier.wrongSignature(origJwt)); + HttpRequest checkRequestSig = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); - // no need for further checks - return auditResult(auditIssueList); - } + HttpRequestResponse checkRequestResponseSig = api.http().sendRequest(checkRequestSig); + if (requestWasSuccessful(checkRequestResponseSig)) { + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.invalidSignature(baseRequestResponse, checkRequestResponseSig.withRequestMarkers(markers))); - // send JWT with none algorithm - this.permute("none", ""); - for (String s : algoList) { - HttpRequest checkRequestNone = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.algNone(origJwt, s))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseNone = api.http().sendRequest(checkRequestNone); - if (requestWasSuccessful(checkRequestResponseNone)) { - auditIssueList.add(JwtAuditIssues.getAlgNone(baseRequestResponse, checkRequestResponseNone)); + // no need for further checks + return auditResult(auditIssueList); + } + } - // stop after a valid none permutation has been found - break; + // send JWT with none algorithm (skip for active scan) + if (fromContextMenu) { + this.permute("none", ""); + for (String s : algoList) { + payload = byteArray(jwtModifier.algNone(origJwt, s)); + HttpRequest checkRequestNone = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + + HttpRequestResponse checkRequestResponseNone = api.http().sendRequest(checkRequestNone); + if (requestWasSuccessful(checkRequestResponseNone)) { + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.getAlgNone(baseRequestResponse, checkRequestResponseNone.withRequestMarkers(markers))); + + // stop after a valid none permutation has been found + break; + } } } // send JWT with empty password - HttpRequest checkRequestEmpty = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.emptyPassword(origJwt))).withService(baseRequestResponse.httpService()); + payload = byteArray(jwtModifier.emptyPassword(origJwt)); + HttpRequest checkRequestEmpty = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseEmpty = api.http().sendRequest(checkRequestEmpty); if (requestWasSuccessful(checkRequestResponseEmpty)){ - auditIssueList.add(JwtAuditIssues.emptyPassword(baseRequestResponse, checkRequestResponseEmpty)); + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.emptyPassword(baseRequestResponse, checkRequestResponseEmpty.withRequestMarkers(markers))); } // send JWT with invalid ECDSA signature - HttpRequest checkRequestEcdsa = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.invalidEcdsa(origJwt))).withService(baseRequestResponse.httpService()); + payload = byteArray(jwtModifier.invalidEcdsa(origJwt)); + HttpRequest checkRequestEcdsa = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + HttpRequestResponse checkRequestResponseEcdsa = api.http().sendRequest(checkRequestEcdsa); if (requestWasSuccessful(checkRequestResponseEcdsa)){ - auditIssueList.add(JwtAuditIssues.invalidEcdsa(baseRequestResponse, checkRequestResponseEcdsa)); + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.invalidEcdsa(baseRequestResponse, checkRequestResponseEcdsa.withRequestMarkers(markers))); } - // send JWT with JWKS injection - HttpRequest checkRequestJwks = auditInsertionPoint.buildHttpRequestWithPayload(byteArray(jwtModifier.jwksInjection(origJwt))).withService(baseRequestResponse.httpService()); - HttpRequestResponse checkRequestResponseJwks = api.http().sendRequest(checkRequestJwks); - if (requestWasSuccessful(checkRequestResponseJwks)){ - auditIssueList.add(JwtAuditIssues.jwksInjection(baseRequestResponse, checkRequestResponseJwks)); + // send JWT with JWKS injection (skip for active scan) + if (fromContextMenu) { + payload = byteArray(jwtModifier.jwksInjection(origJwt)); + HttpRequest checkRequestJwks = auditInsertionPoint.buildHttpRequestWithPayload(payload).withService(baseRequestResponse.httpService()); + + HttpRequestResponse checkRequestResponseJwks = api.http().sendRequest(checkRequestJwks); + if (requestWasSuccessful(checkRequestResponseJwks)) { + List markers = markersForPayload(auditInsertionPoint, payload); + auditIssueList.add(JwtAuditIssues.jwksInjection(baseRequestResponse, checkRequestResponseJwks.withRequestMarkers(markers))); + } } } From d1fb39c938a13209550a9f2a310a838a96130e51 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Tue, 14 May 2024 15:54:31 +0000 Subject: [PATCH 7/8] Add persistence of key material --- src/main/java/BurpExtension/JwtModifier.java | 41 +++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/src/main/java/BurpExtension/JwtModifier.java b/src/main/java/BurpExtension/JwtModifier.java index 06fcdc2..1d6e89f 100644 --- a/src/main/java/BurpExtension/JwtModifier.java +++ b/src/main/java/BurpExtension/JwtModifier.java @@ -1,5 +1,7 @@ package BurpExtension; +import burp.api.montoya.core.ByteArray; import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.KeyPairBuilder; import io.jsonwebtoken.security.Keys; import burp.api.montoya.MontoyaApi; import javax.crypto.Mac; @@ -9,6 +11,9 @@ import java.security.*; import java.security.SignatureException; import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; import java.util.Base64; import com.nimbusds.jose.jwk.RSAKey; @@ -90,7 +95,7 @@ public String jwksInjection(String jwt){ JSONObject headerObject = new JSONObject(); headerObject.put("alg", "RS256"); - KeyPair keyPair = generateRS256KeyPair(); + KeyPair keyPair = loadRS256KeyPair(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAKey jwk = new RSAKey.Builder(publicKey).build(); @@ -116,13 +121,45 @@ private static String base64UrlEncodeNoPadding(String input) { private static String base64UrlEncodeNoPadding(byte[] input) { return Base64.getUrlEncoder().withoutPadding().encodeToString(input); } - private static KeyPair generateRS256KeyPair() throws NoSuchAlgorithmException { + private KeyPair generateRS256KeyPair() throws NoSuchAlgorithmException { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); + return keyPairGenerator.generateKeyPair(); } + private KeyPair loadRS256KeyPair() throws NoSuchAlgorithmException { + // load PrivateKey and PublicKey + ByteArray privateKeyByteArray = this.api.persistence().extensionData().getByteArray("privateKey"); + ByteArray publicKeyByteArray = this.api.persistence().extensionData().getByteArray("publicKey"); + + if (privateKeyByteArray == null || publicKeyByteArray == null) { + KeyPair keyPair = generateRS256KeyPair(); + + // store PrivateKey and PublicKey + byte[] privateKeyByte = keyPair.getPrivate().getEncoded(); + this.api.persistence().extensionData().setByteArray("privateKey", ByteArray.byteArray(privateKeyByte)); + byte[] publicKeyByte = keyPair.getPublic().getEncoded(); + this.api.persistence().extensionData().setByteArray("publicKey", ByteArray.byteArray(publicKeyByte)); + + return keyPair; + } else { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + PrivateKey privateKey = null; + PublicKey publicKey = null; + try { + privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyByteArray.getBytes())); + publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyByteArray.getBytes())); + } catch (InvalidKeySpecException e) { + this.api.logging().logToError(e.getMessage()); + } + + return new KeyPair(publicKey, privateKey); + } + } + private static String signJWTRSA(String header, String payload, PrivateKey privateKey) throws NoSuchAlgorithmException, SignatureException, InvalidKeyException { final String data = header + "." + payload; From 91122b8b4b05a7fccdc0bf2a01e66884e4178e98 Mon Sep 17 00:00:00 2001 From: Cyrill Bannwart Date: Thu, 16 May 2024 13:59:42 +0000 Subject: [PATCH 8/8] Fix highlighting --- src/main/java/BurpExtension/JwtScanCheck.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/BurpExtension/JwtScanCheck.java b/src/main/java/BurpExtension/JwtScanCheck.java index 2097ca6..83419eb 100644 --- a/src/main/java/BurpExtension/JwtScanCheck.java +++ b/src/main/java/BurpExtension/JwtScanCheck.java @@ -44,7 +44,9 @@ private List markersForPayload(AuditInsertionPoint auditInsertionPoint, List highlights = auditInsertionPoint.issueHighlights(payload); List markers = new ArrayList<>(highlights.size()); for (Range range : highlights) { - markers.add(Marker.marker(range)); + int startIndex = range.startIndexInclusive(); + int endIndex = range.startIndexInclusive() + payload.length(); + markers.add(Marker.marker(startIndex, endIndex)); } return markers;