├── settings.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ ├── app │ │ ├── algorithm │ │ │ ├── AlgorithmType.java │ │ │ └── AlgorithmWrapper.java │ │ ├── controllers │ │ │ ├── JWTRequestTabController.java │ │ │ ├── JWTResponseTabController.java │ │ │ ├── JWTRequestInterceptTabController.java │ │ │ ├── JWTResponseInterceptTabController.java │ │ │ ├── ReadableTokenFormat.java │ │ │ ├── HighLightController.java │ │ │ ├── JWTSuiteTabController.java │ │ │ └── JwtTabController.java │ │ ├── helpers │ │ │ ├── PublicKeyBroker.java │ │ │ ├── KeyValuePair.java │ │ │ ├── TokenChecker.java │ │ │ ├── Output.java │ │ │ ├── CookieFlagWrapper.java │ │ │ ├── DelayedDocumentListener.java │ │ │ ├── O365.java │ │ │ ├── SecretFinder.java │ │ │ ├── KeyHelper.java │ │ │ └── Config.java │ │ └── tokenposition │ │ │ ├── PostBody.java │ │ │ ├── ITokenPosition.java │ │ │ ├── AuthorizationBearerHeader.java │ │ │ ├── Body.java │ │ │ └── Cookie.java │ │ ├── gui │ │ ├── ThemeDetector.java │ │ ├── RSyntaxTextAreaFactory.java │ │ ├── JWTViewTab.java │ │ ├── JWTSuiteTab.java │ │ └── JLabelLink.java │ │ ├── model │ │ ├── TimeClaim.java │ │ ├── Settings.java │ │ ├── JWTSuiteTabModel.java │ │ ├── JWTInterceptModel.java │ │ ├── JWTTabModel.java │ │ ├── Strings.java │ │ └── CustomJWToken.java │ │ └── burp │ │ ├── JWT4BExtension.java │ │ ├── JWT4BContextMenuItemsProvider.java │ │ └── JWT4BEditorProvider.java └── test │ └── java │ ├── burp │ └── api │ │ └── montoya │ │ ├── core │ │ ├── FakeHttpHeader.java │ │ ├── FakeParsedHttpParameter.java │ │ ├── FakeHttpParameter.java │ │ ├── FakeRange.java │ │ ├── FakeHttpResponse.java │ │ ├── FakeHttpMessage.java │ │ ├── FakeByteArray.java │ │ └── FakeHttpRequest.java │ │ └── MontoyaExtension.java │ └── app │ ├── TestJWTValidCheck.java │ ├── TestInvalidJSONToken.java │ ├── TestKeyHelper.java │ ├── TestCustomJWTDecoder.java │ ├── controllers │ └── ReadableTokenFormatTest.java │ ├── TestFindTokenInHeader.java │ ├── TestAlgorithmWrapper.java │ ├── TestCookieDetection.java │ ├── TestPostDetection.java │ ├── TestAuthorizationDetection.java │ ├── TestBodyDetection.java │ ├── TestSetCookieDetection.java │ └── TestConstants.java ├── .travis.yml ├── BappManifest.bmf ├── .gitignore ├── BappDescription.html ├── gradlew.bat ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'JWT4B_Montoya' 2 | 3 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | extender_version = 2023.12.1 2 | junit_version = 5.11.0-M2 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/json-web-tokens/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/app/algorithm/AlgorithmType.java: -------------------------------------------------------------------------------- 1 | package app.algorithm; 2 | 3 | public enum AlgorithmType { 4 | NONE, SYMMETRIC, ASYMMETRIC 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | sudo: false 4 | 5 | dist: trusty 6 | 7 | jdk: 8 | - openjdk10 9 | - openjdk11 10 | 11 | branches: 12 | only: 13 | - master 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 25 16:59:06 CEST 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/JWTRequestTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import gui.JWTViewTab; 4 | import model.JWTTabModel; 5 | 6 | public class JWTRequestTabController extends JwtTabController { 7 | 8 | public JWTRequestTabController(JWTTabModel jwtTM, JWTViewTab jwtVT) { 9 | super(jwtTM, jwtVT, true); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/JWTResponseTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import gui.JWTViewTab; 4 | import model.JWTTabModel; 5 | 6 | public class JWTResponseTabController extends JwtTabController { 7 | 8 | public JWTResponseTabController(JWTTabModel jwtTM, JWTViewTab jwtVT) { 9 | super(jwtTM, jwtVT, false); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/PublicKeyBroker.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | /** 4 | * Created by mvetsch on 24.04.2017. 5 | */ 6 | public class PublicKeyBroker { 7 | 8 | PublicKeyBroker() { 9 | 10 | } 11 | 12 | // This hack is used to get public Key from the Random Key generator to the 13 | // controller. Just for logging 14 | public static String publicKey; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/JWTRequestInterceptTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import gui.JWTInterceptTab; 4 | import model.JWTInterceptModel; 5 | 6 | public class JWTRequestInterceptTabController extends JWTInterceptTabController { 7 | 8 | public JWTRequestInterceptTabController(JWTInterceptModel jwtTM, JWTInterceptTab jwtVT) { 9 | super(jwtTM, jwtVT, true); 10 | } 11 | 12 | } -------------------------------------------------------------------------------- /src/main/java/app/controllers/JWTResponseInterceptTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import gui.JWTInterceptTab; 4 | import model.JWTInterceptModel; 5 | 6 | public class JWTResponseInterceptTabController extends JWTInterceptTabController { 7 | 8 | public JWTResponseInterceptTabController(JWTInterceptModel jwtTM, JWTInterceptTab jwtVT) { 9 | super(jwtTM, jwtVT, false); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: f923cbf91698420890354c1d8958fee6 2 | ExtensionType: 1 3 | Name: JSON Web Tokens 4 | RepoName: json-web-tokens 5 | ScreenVersion: 2.8.2 6 | SerialVersion: 33 7 | MinPlatformVersion: 15 8 | ProOnly: False 9 | Author: Oussama Zgheb 10 | ShortDescription: Enables Burp to decode and manipulate JSON web tokens. 11 | EntryPoint: build/libs/JWT4B_Montoya-1.0-SNAPSHOT.jar 12 | BuildCommand: ./gradlew jar 13 | SupportedProducts: Pro, Community 14 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeHttpHeader.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import burp.api.montoya.http.message.HttpHeader; 4 | 5 | public class FakeHttpHeader implements HttpHeader { 6 | 7 | private final String name; 8 | private final String value; 9 | 10 | public FakeHttpHeader(String name, String value) { 11 | this.name = name; 12 | this.value = value; 13 | } 14 | 15 | @Override 16 | public String name() { 17 | return name; 18 | } 19 | 20 | @Override 21 | public String value() { 22 | return value; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/KeyValuePair.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | public class KeyValuePair { 4 | 5 | private String name; 6 | private String value; 7 | 8 | public KeyValuePair(String name, String value) { 9 | this.setName(name); 10 | this.setValue(value); 11 | } 12 | 13 | public String getName() { 14 | return name; 15 | } 16 | 17 | public void setName(String name) { 18 | this.name = name; 19 | } 20 | 21 | public String getValue() { 22 | return value; 23 | } 24 | 25 | public void setValue(String value) { 26 | this.value = value; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/gui/ThemeDetector.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import burp.api.montoya.ui.UserInterface; 4 | import model.Settings; 5 | 6 | import static burp.api.montoya.ui.Theme.LIGHT; 7 | 8 | import java.awt.Font; 9 | 10 | public class ThemeDetector { 11 | 12 | private final UserInterface userInterface; 13 | 14 | ThemeDetector(UserInterface userInterface) { 15 | this.userInterface = userInterface; 16 | } 17 | 18 | boolean isLightTheme() { 19 | boolean isLight = userInterface.currentTheme() == LIGHT; 20 | Settings.isLight = isLight; // TODO not so nice, since settings are static 21 | return isLight; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/app/TestJWTValidCheck.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import model.CustomJWToken; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static app.TestConstants.*; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class TestJWTValidCheck { 10 | 11 | @Test 12 | void testValid() { 13 | assertThat(CustomJWToken.isValidJWT(HS256_TOKEN,true)).isTrue(); 14 | } 15 | 16 | @Test 17 | void testInvalidHeader() { 18 | assertThat(CustomJWToken.isValidJWT(INVALID_HEADER_TOKEN,true)).isFalse(); 19 | } 20 | 21 | @Test 22 | void testInvalidHeader2() { 23 | assertThat(CustomJWToken.isValidJWT(INVALID_HEADER_TOKEN_2,true)).isFalse(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | /target/ 41 | 42 | 43 | ## BURP ## 44 | /src/burp/* -------------------------------------------------------------------------------- /src/test/java/app/TestInvalidJSONToken.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import com.auth0.jwt.algorithms.Algorithm; 4 | 5 | import model.CustomJWToken; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static app.TestConstants.INVALID_JSON_TOKEN; 9 | import static org.junit.jupiter.api.Assertions.assertEquals; 10 | 11 | 12 | class TestInvalidJSONToken { 13 | @Test 14 | void newTest() throws IllegalArgumentException { 15 | Algorithm algo = Algorithm.HMAC256("test"); 16 | CustomJWToken cjt = new CustomJWToken(INVALID_JSON_TOKEN); 17 | cjt.calculateAndSetSignature(algo); 18 | String getToken = cjt.getToken(); 19 | // we now assume that invalid tokens will be returned as received 20 | 21 | assertEquals(getToken, getToken); // TODO 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/app/TestKeyHelper.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.helpers.KeyHelper; 4 | import org.junit.jupiter.params.ParameterizedTest; 5 | import org.junit.jupiter.params.provider.ValueSource; 6 | 7 | import java.security.Key; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class TestKeyHelper { 12 | 13 | @ValueSource(strings = { "RSA", "EC" }) 14 | @ParameterizedTest 15 | void testGetKeyInstanceWithNullPublicKey(String algorithm) { 16 | Key key = KeyHelper.getKeyInstance(null, algorithm, false); 17 | 18 | assertThat(key).isNull(); 19 | } 20 | 21 | @ValueSource(strings = { "RSA", "EC" }) 22 | @ParameterizedTest 23 | void testGetKeyInstanceWithNullPrivateKey(String algorithm) { 24 | Key key = KeyHelper.getKeyInstance(null, algorithm, true); 25 | 26 | assertThat(key).isNull(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeParsedHttpParameter.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import burp.api.montoya.http.message.params.HttpParameterType; 4 | import burp.api.montoya.http.message.params.ParsedHttpParameter; 5 | 6 | public class FakeParsedHttpParameter extends FakeHttpParameter implements ParsedHttpParameter { 7 | 8 | private final Range nameOffset; 9 | private final Range valueOffset; 10 | 11 | public FakeParsedHttpParameter(String name, String value, HttpParameterType type, Range nameOffset, Range valueOffset) { 12 | super(name, value, type); 13 | 14 | this.nameOffset = nameOffset; 15 | this.valueOffset = valueOffset; 16 | } 17 | 18 | @Override 19 | public Range nameOffsets() { 20 | return nameOffset; 21 | } 22 | 23 | @Override 24 | public Range valueOffsets() { 25 | return valueOffset; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/TokenChecker.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import org.apache.commons.lang.StringUtils; 4 | 5 | import com.auth0.jwt.JWT; 6 | import com.auth0.jwt.interfaces.DecodedJWT; 7 | 8 | public class TokenChecker { 9 | 10 | private TokenChecker() { 11 | 12 | } 13 | 14 | public static final String JWT_ALLOWED_CHARS_REGEXP = "[A-Za-z0-9+/=_-]+"; 15 | 16 | public static boolean isValidJWT(String jwt) { 17 | int dotCount = StringUtils.countMatches(jwt, "."); 18 | if (dotCount != 2) { 19 | return false; 20 | } 21 | 22 | jwt = jwt.trim(); 23 | if (StringUtils.contains(jwt, " ")) { 24 | return false; 25 | } 26 | 27 | for (String part : StringUtils.split(jwt, ".")) { 28 | if (!part.matches(JWT_ALLOWED_CHARS_REGEXP)) { 29 | return false; 30 | } 31 | } 32 | 33 | try { 34 | DecodedJWT decoded = JWT.decode(jwt); 35 | decoded.getAlgorithm(); 36 | return true; 37 | } catch (Exception ignored) { 38 | // ignored 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/model/TimeClaim.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.util.List; 4 | 5 | import lombok.Data; 6 | import lombok.RequiredArgsConstructor; 7 | 8 | @Data 9 | @RequiredArgsConstructor 10 | public class TimeClaim { 11 | 12 | private final String claim; 13 | private final String date; 14 | private final long unixTimestamp; 15 | private final boolean valid; 16 | private final boolean canBeValid; 17 | 18 | public static String getTimeClaimsAsHTML(List tcl) { 19 | StringBuilder timeClaimSB = new StringBuilder(); 20 | timeClaimSB.append(""); 21 | if (tcl != null && !tcl.isEmpty()) { 22 | for (TimeClaim timeClaim : tcl) { 23 | String resultString = timeClaim.isValid() ? "passed" : "failed"; 24 | timeClaimSB.append("" + timeClaim.getClaim() + (timeClaim.isCanBeValid() ? " check " + resultString : "") + " - " + timeClaim.getDate() + "
"); 25 | } 26 | } 27 | timeClaimSB.append(""); 28 | return timeClaimSB.toString(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeHttpParameter.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import burp.api.montoya.http.message.params.HttpParameter; 4 | import burp.api.montoya.http.message.params.HttpParameterType; 5 | 6 | public class FakeHttpParameter implements HttpParameter { 7 | 8 | private final String name; 9 | private final String value; 10 | private final HttpParameterType type; 11 | 12 | public FakeHttpParameter(String name, String value, HttpParameterType type) { 13 | this.name = name; 14 | this.value = value; 15 | this.type = type; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | return "FakeHttpParameter{" + "name='" + name + '\'' + ", value='" + value + '\'' + ", type=" + type + '}'; 21 | } 22 | 23 | public String toNameValueString() { 24 | return name + "=" + value; 25 | } 26 | 27 | @Override 28 | public HttpParameterType type() { 29 | return type; 30 | } 31 | 32 | @Override 33 | public String name() { 34 | return name; 35 | } 36 | 37 | @Override 38 | public String value() { 39 | return value; 40 | } 41 | } -------------------------------------------------------------------------------- /src/test/java/app/TestCustomJWTDecoder.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import model.CustomJWToken; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import static app.TestConstants.*; 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class TestCustomJWTDecoder { 10 | 11 | @Test 12 | void testIfTokenCanBeDecoded() { 13 | CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); 14 | assertThat(reConstructedToken.getToken()).isEqualTo(HS256_TOKEN); 15 | assertThat(reConstructedToken.isBuiltSuccessful()).isTrue(); 16 | } 17 | 18 | @Test 19 | void testBrokenToken() { 20 | CustomJWToken reConstructedToken = new CustomJWToken(INVALID_HEADER_TOKEN); 21 | assertThat(reConstructedToken.isBuiltSuccessful()).isFalse(); 22 | } 23 | 24 | @Test 25 | void testIfTokenIsMinified() { 26 | CustomJWToken reConstructedToken = new CustomJWToken(HS256_TOKEN); 27 | assertThat(reConstructedToken.isMinified()).isTrue(); 28 | } 29 | 30 | @Test 31 | void testIfTokenIsNotMinified() { 32 | CustomJWToken reConstructedToken = new CustomJWToken(HS256_BEAUTIFIED_TOKEN); 33 | assertThat(reConstructedToken.isMinified()).isFalse(); 34 | } 35 | } -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

JSON Web Tokens (JWT4B) lets you decode and manipulate JSON web tokens on the fly, 2 | check their validity and automate common attacks.

3 | 4 |

Features

5 | 6 | 15 | 16 |

Configuration

17 | 18 |

A configuration file is generated at %user.home%/.JWT4B/config.json

19 | 20 |

You can use the "Change config" button to open this file from the extension-generated tab and make any adjustments.

21 | 22 |

Changes to the configuration require a reload. If the file is deleted, it will regenerate with default settings. Setting resetEditor to false preserves editor state across requests, useful for testing in Repeater.

23 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeRange.java: -------------------------------------------------------------------------------- 1 | // copied from https://github.com/Hannah-PortSwigger/WebSocketTurboIntruder/blob/main/src/test/java/burp/api/montoya/core/FakeRange.java 2 | 3 | package burp.api.montoya.core; 4 | 5 | import java.util.Objects; 6 | 7 | public class FakeRange implements Range { 8 | private final int start; 9 | private final int end; 10 | 11 | public FakeRange(int start, int end) { 12 | this.start = start; 13 | this.end = end; 14 | } 15 | 16 | @Override 17 | public int startIndexInclusive() { 18 | return start; 19 | } 20 | 21 | @Override 22 | public int endIndexExclusive() { 23 | return end; 24 | } 25 | 26 | @Override 27 | public boolean contains(int i) { 28 | throw new UnsupportedOperationException(); 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) { 34 | return true; 35 | } 36 | 37 | return o instanceof Range range && startIndexInclusive() == range.startIndexInclusive() && endIndexExclusive() == range.endIndexExclusive(); 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(start, end); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return "Range{" + "start=" + start + ", end=" + end + '}'; 48 | } 49 | 50 | public static Range rangeOf(int start, int end) { 51 | return new FakeRange(start, end); 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/java/app/helpers/Output.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import burp.api.montoya.logging.Logging; 4 | 5 | import java.text.DateFormat; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | import java.util.TimeZone; 10 | 11 | public class Output { 12 | private static final DateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss.SSS"); 13 | 14 | private static Logging logging; 15 | 16 | public static void initialise(Logging _logging) { 17 | System.out.println("init"); 18 | logging = _logging; 19 | } 20 | 21 | public static void output(String string, boolean log) { 22 | if(log) { 23 | output(string); 24 | } 25 | } 26 | 27 | public static void output(String string) { 28 | if (logging != null) { 29 | logging.logToOutput(formatString(string)); 30 | } else { 31 | System.out.println(string); 32 | } 33 | } 34 | 35 | public static void outputError(String string, boolean log) { 36 | if(log) { 37 | outputError(string); 38 | } 39 | } 40 | 41 | public static void outputError(String string) { 42 | if (logging != null) { 43 | logging.logToError(formatString(string)); 44 | } else { 45 | System.err.println(string); 46 | } 47 | } 48 | 49 | private static String formatString(String string) { 50 | Date cal = Calendar.getInstance(TimeZone.getDefault()).getTime(); 51 | return DATE_FORMAT.format(cal.getTime()) + " | " + string; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/model/Settings.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.awt.Color; 4 | 5 | import javax.swing.JButton; 6 | 7 | public class Settings { 8 | 9 | Settings() { 10 | 11 | } 12 | 13 | public static final String TAB_NAME = "JSON Web Tokens"; 14 | public static final String EXTENSION_NAME = "JSON Web Tokens"; 15 | 16 | public static boolean isLight = true; 17 | 18 | private static final Color COLOR_VALID_LIGHT = new Color(89, 207, 120); 19 | private static final Color COLOR_VALID_DARK = new Color(16, 48, 25); 20 | 21 | private static final Color COLOR_INVALID_LIGHT = new Color(199, 69, 60); 22 | private static final Color COLOR_INVALID_DARK = new Color(51, 9, 6); 23 | 24 | private static final Color COLOR_PROBLEM_INVALID_LIGHT = new Color(200, 204, 88); 25 | private static final Color COLOR_PROBLEM_INVALID_DARK = new Color(64, 62, 3); 26 | 27 | public static Color getValidColor() { 28 | if (isLight) { 29 | return COLOR_VALID_LIGHT; 30 | } 31 | return COLOR_VALID_DARK; 32 | } 33 | 34 | public static Color getInvalidColor() { 35 | if (isLight) { 36 | return COLOR_INVALID_LIGHT; 37 | } 38 | return COLOR_INVALID_DARK; 39 | } 40 | 41 | public static Color getProblemColor() { 42 | if (isLight) { 43 | return COLOR_PROBLEM_INVALID_LIGHT; 44 | } 45 | return COLOR_PROBLEM_INVALID_DARK; 46 | } 47 | 48 | public static final Color COLOR_UNDEFINED = new JButton().getBackground(); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/CookieFlagWrapper.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | public class CookieFlagWrapper { 4 | 5 | private final boolean secureFlag; 6 | private final boolean httpOnlyFlag; 7 | private final boolean isCookie; 8 | 9 | public CookieFlagWrapper(boolean isCookie, boolean secureFlag, boolean httpOnlyFlag) { 10 | this.isCookie = isCookie; 11 | this.secureFlag = secureFlag; 12 | this.httpOnlyFlag = httpOnlyFlag; 13 | } 14 | 15 | public boolean isCookie() { 16 | return isCookie; 17 | } 18 | 19 | public boolean hasHttpOnlyFlag() { 20 | if (isCookie) { 21 | return httpOnlyFlag; 22 | } 23 | return false; 24 | } 25 | 26 | public boolean hasSecureFlag() { 27 | if (isCookie) { 28 | return secureFlag; 29 | } 30 | return false; 31 | } 32 | 33 | public String toHTMLString() { 34 | if (!isCookie) { 35 | return ""; 36 | } 37 | String returnString = "
"; 38 | if (!hasSecureFlag()) { 39 | returnString += "No secure flag set. Token may be transmitted by HTTP.
"; 40 | } else { 41 | returnString += "Secure Flag set.
"; 42 | } 43 | if (!hasHttpOnlyFlag()) { 44 | returnString += "No HttpOnly flag set. Token may accessed by JavaScript (XSS)."; 45 | } else { 46 | returnString += "HttpOnly Flag set."; 47 | } 48 | returnString += "
"; 49 | return returnString; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/model/JWTSuiteTabModel.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.awt.Color; 4 | import java.util.List; 5 | 6 | public class JWTSuiteTabModel { 7 | 8 | private String jwtInput; 9 | private String jwtKey; 10 | private Color jwtSignatureColor; 11 | private String jwtJSON; 12 | private String verificationLabel; 13 | private List tcl; 14 | private String verificationResult; 15 | 16 | public String getJwtInput() { 17 | return jwtInput; 18 | } 19 | 20 | public void setJwtInput(String jwtInput) { 21 | this.jwtInput = jwtInput; 22 | } 23 | 24 | public String getJwtKey() { 25 | return jwtKey; 26 | } 27 | 28 | public void setJwtKey(String jwtKey) { 29 | this.jwtKey = jwtKey; 30 | } 31 | 32 | public Color getJwtSignatureColor() { 33 | return jwtSignatureColor; 34 | } 35 | 36 | public void setJwtSignatureColor(Color jwtSignatureColor) { 37 | this.jwtSignatureColor = jwtSignatureColor; 38 | } 39 | 40 | public String getJwtJSON() { 41 | return jwtJSON; 42 | } 43 | 44 | public void setJwtJSON(String jwtJSON) { 45 | this.jwtJSON = jwtJSON; 46 | } 47 | 48 | public void setVerificationLabel(String label) { 49 | this.verificationLabel = label; 50 | } 51 | 52 | public void setVerificationResult(String result) { 53 | this.verificationResult = result; 54 | } 55 | 56 | public String getVerificationLabel() { 57 | return this.verificationLabel; 58 | } 59 | 60 | public void setTimeClaims(List tcl) { 61 | this.tcl = tcl; 62 | } 63 | 64 | public String getTimeClaimsAsText() { 65 | return TimeClaim.getTimeClaimsAsHTML(tcl); 66 | } 67 | 68 | public String getVerificationResult() { 69 | return this.verificationResult; 70 | } 71 | } -------------------------------------------------------------------------------- /src/main/java/model/JWTInterceptModel.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.util.List; 4 | 5 | import app.helpers.CookieFlagWrapper; 6 | 7 | public class JWTInterceptModel { 8 | 9 | private String jwtSignatureKey; 10 | private CustomJWToken jwToken; 11 | private String originalJWT; 12 | private String problemDetail; 13 | private CookieFlagWrapper cFW; 14 | private List tcl; 15 | private CustomJWToken originalJWToken; 16 | 17 | public String getJWTKey() { 18 | return jwtSignatureKey; 19 | } 20 | 21 | public void setJWTSignatureKey(String jwtSignatureKey) { 22 | this.jwtSignatureKey = jwtSignatureKey; 23 | } 24 | 25 | public String getProblemDetail() { 26 | return problemDetail; 27 | } 28 | 29 | public void setProblemDetail(String problemDetail) { 30 | this.problemDetail = problemDetail; 31 | } 32 | 33 | public CookieFlagWrapper getcFW() { 34 | return cFW; 35 | } 36 | 37 | public void setcFW(CookieFlagWrapper cFW) { 38 | this.cFW = cFW; 39 | } 40 | 41 | public void setTimeClaims(List tcl) { 42 | this.tcl = tcl; 43 | } 44 | 45 | public String getTimeClaimsAsText() { 46 | return TimeClaim.getTimeClaimsAsHTML(tcl); 47 | } 48 | 49 | public CustomJWToken getJwToken() { 50 | return jwToken; 51 | } 52 | 53 | public void setJwToken(CustomJWToken jwToken) { 54 | this.jwToken = jwToken; 55 | } 56 | 57 | public void setOriginalJWT(String originalJWT) { 58 | this.originalJWT = originalJWT; 59 | } 60 | 61 | public String getOriginalJWT() { 62 | return originalJWT; 63 | } 64 | 65 | public void setOriginalJWToken(CustomJWToken originalJWToken) { 66 | this.originalJWToken = originalJWToken; 67 | } 68 | 69 | public CustomJWToken getOriginalJWToken() { 70 | return originalJWToken; 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/app/tokenposition/PostBody.java: -------------------------------------------------------------------------------- 1 | package app.tokenposition; 2 | 3 | import java.util.List; 4 | 5 | import burp.api.montoya.http.message.params.HttpParameter; 6 | import burp.api.montoya.http.message.params.HttpParameterType; 7 | import burp.api.montoya.http.message.params.ParsedHttpParameter; 8 | 9 | import burp.api.montoya.http.message.HttpMessage; 10 | import burp.api.montoya.http.message.requests.HttpRequest; 11 | import burp.api.montoya.http.message.responses.HttpResponse; 12 | 13 | import app.helpers.Config; 14 | import app.helpers.TokenChecker; 15 | 16 | public class PostBody extends ITokenPosition { 17 | 18 | private HttpParameter httpParameter; 19 | 20 | public PostBody(HttpMessage httpMessage, boolean isRequest) { 21 | super(httpMessage, isRequest); 22 | } 23 | 24 | @Override 25 | public boolean positionFound() { 26 | if (isRequest) { 27 | httpParameter = getJWTFromPostBody(); 28 | if (httpParameter != null) { 29 | token = httpParameter.value(); 30 | return true; 31 | } 32 | } 33 | return false; 34 | } 35 | 36 | private HttpParameter getJWTFromPostBody() { 37 | if (isRequest) { 38 | HttpRequest httpRequest = (HttpRequest) httpMessage; 39 | List parsedHttpParameters = httpRequest.parameters(HttpParameterType.BODY); 40 | 41 | return parsedHttpParameters.stream().filter(parameter -> Config.tokenKeywords.contains(parameter.name()) && TokenChecker.isValidJWT(parameter.value())).findFirst().orElse(null); 42 | } 43 | 44 | return null; 45 | } 46 | 47 | @Override 48 | public HttpRequest getRequest() { 49 | HttpRequest httpRequest = (HttpRequest) httpMessage; 50 | 51 | return httpRequest.withParameter(HttpParameter.bodyParameter(httpParameter.name(), token)); 52 | } 53 | 54 | @Override 55 | public HttpResponse getResponse() { 56 | return (HttpResponse) httpMessage; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/DelayedDocumentListener.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import java.awt.event.ActionEvent; 4 | import java.awt.event.ActionListener; 5 | 6 | import javax.swing.Timer; 7 | import javax.swing.event.DocumentEvent; 8 | import javax.swing.event.DocumentListener; 9 | 10 | // Source: https://raw.githubusercontent.com/bonifaido/JIT-Tree/master/src/main/java/me/nandork/jittree/DelayedDocumentListener.java 11 | // License: MIT (https://opensource.org/licenses/MIT) 12 | 13 | public class DelayedDocumentListener implements DocumentListener { 14 | 15 | public static final int DELAY = 400; 16 | 17 | private final Timer timer; 18 | private DocumentEvent lastEvent; 19 | 20 | public DelayedDocumentListener(DocumentListener delegate) { 21 | this(delegate, DELAY); 22 | } 23 | 24 | public DelayedDocumentListener(final DocumentListener delegate, int delay) { 25 | timer = new Timer(delay, new ActionListener() { 26 | 27 | @Override 28 | public void actionPerformed(ActionEvent e) { 29 | timer.stop(); 30 | fireLastEventOn(delegate); 31 | } 32 | }); 33 | } 34 | 35 | private void fireLastEventOn(DocumentListener delegate) { 36 | if (lastEvent.getType() == DocumentEvent.EventType.INSERT) { 37 | delegate.insertUpdate(lastEvent); 38 | } else if (lastEvent.getType() == DocumentEvent.EventType.REMOVE) { 39 | delegate.removeUpdate(lastEvent); 40 | } else { 41 | delegate.changedUpdate(lastEvent); 42 | } 43 | } 44 | 45 | private void storeUpdate(DocumentEvent e) { 46 | lastEvent = e; 47 | timer.restart(); 48 | } 49 | 50 | @Override 51 | public void insertUpdate(DocumentEvent e) { 52 | storeUpdate(e); 53 | } 54 | 55 | @Override 56 | public void removeUpdate(DocumentEvent e) { 57 | storeUpdate(e); 58 | } 59 | 60 | @Override 61 | public void changedUpdate(DocumentEvent e) { 62 | storeUpdate(e); 63 | } 64 | } -------------------------------------------------------------------------------- /src/test/java/app/controllers/ReadableTokenFormatTest.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import org.junit.jupiter.params.ParameterizedTest; 4 | import org.junit.jupiter.params.provider.Arguments; 5 | import org.junit.jupiter.params.provider.MethodSource; 6 | 7 | import java.util.stream.Stream; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | import static org.junit.jupiter.params.provider.Arguments.arguments; 11 | 12 | class ReadableTokenFormatTest { 13 | 14 | static Stream data() { 15 | return Stream.of( 16 | arguments(null, ""), 17 | arguments("", ""), 18 | arguments(" ", ""), 19 | arguments("\t", ""), 20 | arguments("{\"alg\":\"HS256\",\"typ\":", "{\"alg\":\"HS256\",\"typ\":"), 21 | arguments("{\"alg\":\"HS256\",\"typ\":\"JWT\"}", "{\n \"alg\": \"HS256\",\n \"typ\": \"JWT\"\n}"), 22 | arguments("{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"admin\":true}", "{\n \"sub\": \"1234567890\",\n \"name\": \"John Doe\",\n \"admin\": true\n}"), 23 | arguments("{\n \"sub\":\"1234567890\",\n \"name\":\"John Doe\",\n \"admin\":true\n}", "{\n \"sub\": \"1234567890\",\n \"name\": \"John Doe\",\n \"admin\": true\n}"), 24 | arguments("{\"sub\":\"1234567890\",\"name\":\"Max Musterli\",\"admin\":true}", "{\n \"sub\": \"1234567890\",\n \"name\": \"Max Musterli\",\n \"admin\": true\n}"), 25 | arguments("{\"alg\":\"HS256\",\"kid\":\"Z4osLouitTFO+A+xOZ/YcdtlW04=\"}", "{\n \"alg\": \"HS256\",\n \"kid\": \"Z4osLouitTFO+A+xOZ/YcdtlW04=\"\n}") 26 | ); 27 | } 28 | 29 | @ParameterizedTest 30 | @MethodSource("data") 31 | void testJsonBeautify(String json, String expectedBeautifulJson) { 32 | String beautifulJson = ReadableTokenFormat.jsonBeautify(json); 33 | 34 | assertThat(beautifulJson).isEqualTo(expectedBeautifulJson); 35 | } 36 | } -------------------------------------------------------------------------------- /src/test/java/app/TestFindTokenInHeader.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.List; 6 | import java.util.Optional; 7 | 8 | import org.junit.jupiter.api.Test; 9 | 10 | import app.tokenposition.AuthorizationBearerHeader; 11 | 12 | class TestFindTokenInHeader { 13 | 14 | String jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30"; 15 | 16 | @Test 17 | void testCustomAuthKeyword() { 18 | AuthorizationBearerHeader authorizationBearerHeader = new AuthorizationBearerHeader(null, true); 19 | Optional containedJwt = authorizationBearerHeader.containsJwt("X-ABC "+jwt, List.of("Bearer","bearer","BEARER")); 20 | assertThat(containedJwt).isPresent(); 21 | assertThat(containedJwt.get()).startsWith(jwt); 22 | } 23 | 24 | @Test 25 | void testRegularAuthKeyword() { 26 | AuthorizationBearerHeader authorizationBearerHeader = new AuthorizationBearerHeader(null, true); 27 | Optional containedJwt = authorizationBearerHeader.containsJwt("Bearer "+jwt, List.of("Bearer","bearer","BEARER")); 28 | assertThat(containedJwt).isPresent(); 29 | assertThat(containedJwt.get()).startsWith(jwt); 30 | } 31 | 32 | @Test 33 | void testCustomAuthKeywordNegative() { 34 | AuthorizationBearerHeader authorizationBearerHeader = new AuthorizationBearerHeader(null, true); 35 | Optional containedJwt = authorizationBearerHeader.containsJwt("X-ABC foo.bar", List.of("Bearer","bearer","BEARER")); 36 | assertThat(containedJwt).isEmpty(); 37 | } 38 | 39 | @Test 40 | void testRegularAuthKeywordNegative() { 41 | AuthorizationBearerHeader authorizationBearerHeader = new AuthorizationBearerHeader(null, true); 42 | Optional containedJwt = authorizationBearerHeader.containsJwt("Bearer foo.bar", List.of("Bearer","bearer","BEARER")); 43 | assertThat(containedJwt).isEmpty(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/ReadableTokenFormat.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import static com.eclipsesource.json.WriterConfig.PRETTY_PRINT; 4 | 5 | import static org.apache.commons.lang.StringUtils.isBlank; 6 | 7 | import com.eclipsesource.json.Json; 8 | import com.eclipsesource.json.JsonValue; 9 | 10 | import app.helpers.Output; 11 | import gui.JWTInterceptTab; 12 | import model.CustomJWToken; 13 | 14 | public class ReadableTokenFormat { 15 | 16 | ReadableTokenFormat() { 17 | 18 | } 19 | 20 | private static final String NEW_LINE = System.getProperty("line.separator"); 21 | private static final String TITTLE_HEADERS = "Headers = "; 22 | private static final String TITLE_PAYLOAD = NEW_LINE + NEW_LINE + "Payload = "; 23 | private static final String TITLE_SIGNATURE = NEW_LINE + NEW_LINE + "Signature = "; 24 | 25 | public static String getReadableFormat(CustomJWToken token) { 26 | 27 | return TITTLE_HEADERS + jsonBeautify(token.getHeaderJson()) + TITLE_PAYLOAD + jsonBeautify(token.getPayloadJson()) + TITLE_SIGNATURE + "\"" + token.getSignature() + "\""; 28 | } 29 | 30 | public static String jsonBeautify(String input) { 31 | if (isBlank(input)) { 32 | return ""; 33 | } 34 | 35 | try { 36 | JsonValue value = Json.parse(input); 37 | return value.toString(PRETTY_PRINT); 38 | } catch (RuntimeException e) { 39 | Output.outputError("Exception beautifying JSON: " + e.getMessage()); 40 | return input; 41 | } 42 | } 43 | 44 | public static CustomJWToken getTokenFromView(JWTInterceptTab jwtST) { 45 | String header = jwtST.getJwtHeaderArea().getText(); 46 | String payload = jwtST.getJwtPayloadArea().getText(); 47 | String signature = jwtST.getJwtSignatureArea().getText(); 48 | return new CustomJWToken(header, payload, signature); 49 | } 50 | 51 | public static class InvalidTokenFormat extends Exception { 52 | 53 | private static final long serialVersionUID = 1L; 54 | 55 | public InvalidTokenFormat(String message) { 56 | super(message); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/MontoyaExtension.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya; 2 | 3 | import static org.mockito.ArgumentMatchers.any; 4 | import static org.mockito.ArgumentMatchers.anyString; 5 | import static org.mockito.Mockito.mock; 6 | import static org.mockito.Mockito.when; 7 | 8 | import org.junit.jupiter.api.extension.BeforeAllCallback; 9 | import org.junit.jupiter.api.extension.ExtensionContext; 10 | import org.mockito.stubbing.Answer; 11 | 12 | import burp.api.montoya.core.FakeHttpHeader; 13 | import burp.api.montoya.core.FakeHttpParameter; 14 | import burp.api.montoya.core.FakeHttpRequest; 15 | import burp.api.montoya.core.FakeHttpResponse; 16 | import burp.api.montoya.http.message.HttpHeader; 17 | import burp.api.montoya.http.message.params.HttpParameter; 18 | import burp.api.montoya.http.message.params.HttpParameterType; 19 | import burp.api.montoya.http.message.requests.HttpRequest; 20 | import burp.api.montoya.http.message.responses.HttpResponse; 21 | import burp.api.montoya.internal.MontoyaObjectFactory; 22 | import burp.api.montoya.internal.ObjectFactoryLocator; 23 | 24 | public class MontoyaExtension implements BeforeAllCallback { 25 | @Override 26 | public void beforeAll(ExtensionContext extensionContext) { 27 | ObjectFactoryLocator.FACTORY = mock(MontoyaObjectFactory.class); 28 | 29 | MontoyaObjectFactory factory = ObjectFactoryLocator.FACTORY; 30 | when(factory.httpResponse(anyString())).then((Answer) i -> new FakeHttpResponse(i.getArgument(0))); 31 | when(factory.httpRequest(anyString())).then((Answer) i -> new FakeHttpRequest(i.getArgument(0))); 32 | when(factory.httpHeader(anyString(), anyString())).then((Answer) i -> new FakeHttpHeader(i.getArgument(0), i.getArgument(1))); 33 | when(factory.parameter(anyString(), anyString(), any(HttpParameterType.class))).then((Answer) i -> new FakeHttpParameter(i.getArgument(0), i.getArgument(1), i.getArgument(2))); 34 | when(factory.bodyParameter(anyString(), anyString())).then((Answer) i -> new FakeHttpParameter(i.getArgument(0), i.getArgument(1), HttpParameterType.BODY)); 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/java/burp/JWT4BExtension.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import app.controllers.HighLightController; 4 | import app.controllers.JWTSuiteTabController; 5 | import app.helpers.Config; 6 | import app.helpers.Output; 7 | import burp.api.montoya.BurpExtension; 8 | import burp.api.montoya.MontoyaApi; 9 | import burp.api.montoya.extension.Extension; 10 | import burp.api.montoya.http.Http; 11 | import burp.api.montoya.logging.Logging; 12 | import burp.api.montoya.ui.UserInterface; 13 | import gui.JWTSuiteTab; 14 | import gui.RSyntaxTextAreaFactory; 15 | import model.JWTSuiteTabModel; 16 | import model.Settings; 17 | 18 | public class JWT4BExtension implements BurpExtension { 19 | @Override 20 | public void initialize(MontoyaApi api) { 21 | Extension extension = api.extension(); 22 | UserInterface userInterface = api.userInterface(); 23 | Logging logging = api.logging(); 24 | Http http = api.http(); 25 | 26 | RSyntaxTextAreaFactory rSyntaxTextAreaFactory = new RSyntaxTextAreaFactory(userInterface); 27 | 28 | // Logging 29 | Output.initialise(logging); 30 | Output.output("JWT4B says hi!"); 31 | 32 | // Editor 33 | JWT4BEditorProvider editorProvider = new JWT4BEditorProvider(rSyntaxTextAreaFactory,api); 34 | userInterface.registerHttpRequestEditorProvider(editorProvider); 35 | userInterface.registerHttpResponseEditorProvider(editorProvider); 36 | 37 | // Settings 38 | Config.loadConfig(); 39 | 40 | // Request & Response Highlighter 41 | final HighLightController highLightController = new HighLightController(); 42 | http.registerHttpHandler(highLightController); 43 | 44 | // SuiteTab 45 | JWTSuiteTabModel jwtSuiteTabModel = new JWTSuiteTabModel(); 46 | JWTSuiteTab suiteTab = new JWTSuiteTab(jwtSuiteTabModel, rSyntaxTextAreaFactory,api); 47 | api.userInterface().registerSuiteTab(Settings.TAB_NAME, suiteTab); 48 | // Context Menu 49 | JWTSuiteTabController tabController = new JWTSuiteTabController(jwtSuiteTabModel, suiteTab); 50 | userInterface.registerContextMenuItemsProvider(new JWT4BContextMenuItemsProvider(tabController)); 51 | 52 | extension.setName(Settings.EXTENSION_NAME); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/app/TestAlgorithmWrapper.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.algorithm.AlgorithmWrapper; 4 | import com.auth0.jwt.JWT; 5 | import com.auth0.jwt.JWTVerifier; 6 | import com.auth0.jwt.exceptions.SignatureVerificationException; 7 | import com.auth0.jwt.interfaces.DecodedJWT; 8 | import model.CustomJWToken; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import java.util.stream.Stream; 14 | 15 | import static app.TestConstants.*; 16 | import static org.assertj.core.api.Assertions.assertThat; 17 | import static org.junit.jupiter.api.Assertions.assertThrows; 18 | import static org.junit.jupiter.params.provider.Arguments.arguments; 19 | 20 | class TestAlgorithmWrapper { 21 | 22 | static Stream tokensAndValidKeys() { 23 | return Stream.of(arguments("HS256", HS256_TOKEN, "secret"), arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB)); 24 | } 25 | 26 | @MethodSource("tokensAndValidKeys") 27 | @ParameterizedTest(name = "{0}") 28 | void testTokenWithProperKey(String type, String token, String key) throws IllegalArgumentException { 29 | CustomJWToken tokenObj = new CustomJWToken(token); 30 | JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(tokenObj.getAlgorithm(), key)).build(); 31 | 32 | DecodedJWT test = verifier.verify(token); 33 | 34 | assertThat(test.getAlgorithm()).isEqualTo(type); 35 | } 36 | 37 | static Stream tokensAndInvalidKeys() { 38 | return Stream.of(arguments("HS256", HS256_TOKEN, "invalid"), arguments("ES256", ES256_TOKEN, ES256_TOKEN_PUB.replace("Z", "Y"))); 39 | } 40 | 41 | @MethodSource("tokensAndInvalidKeys") 42 | @ParameterizedTest(name = "{0}") 43 | void testHSWithFalseKey(String type, String token, String key) throws IllegalArgumentException { 44 | CustomJWToken tokenObj = new CustomJWToken(token); 45 | JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(tokenObj.getAlgorithm(), key)).build(); 46 | 47 | assertThrows(SignatureVerificationException.class, () -> verifier.verify(token)); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/gui/RSyntaxTextAreaFactory.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import static app.helpers.Output.outputError; 4 | 5 | import java.io.IOException; 6 | 7 | import burp.api.montoya.ui.UserInterface; 8 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 9 | import org.fife.ui.rsyntaxtextarea.Theme; 10 | 11 | public class RSyntaxTextAreaFactory { 12 | 13 | private final ThemeDetector themeDetector; 14 | 15 | public RSyntaxTextAreaFactory(UserInterface userInterface) { 16 | this.themeDetector = new ThemeDetector(userInterface); 17 | } 18 | 19 | RSyntaxTextArea rSyntaxTextArea() { 20 | return new BurpThemeAwareRSyntaxTextArea(themeDetector); 21 | } 22 | 23 | RSyntaxTextArea rSyntaxTextArea(int rows, int cols) { 24 | return new BurpThemeAwareRSyntaxTextArea(themeDetector, rows, cols); 25 | } 26 | 27 | private static class BurpThemeAwareRSyntaxTextArea extends RSyntaxTextArea { 28 | 29 | private static final long serialVersionUID = 1L; 30 | private static final String THEME_PATH = "/org/fife/ui/rsyntaxtextarea/themes/"; //NOSONAR 31 | private static final String DARK_THEME = THEME_PATH + "dark.xml"; 32 | private static final String LIGHT_THEME = THEME_PATH + "default.xml"; 33 | 34 | private final ThemeDetector themeDetector; 35 | 36 | private BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector) { 37 | this.themeDetector = themeDetector; 38 | applyTheme(); 39 | } 40 | 41 | public BurpThemeAwareRSyntaxTextArea(ThemeDetector themeDetector, int rows, int cols) { 42 | super(rows, cols); 43 | this.themeDetector = themeDetector; 44 | applyTheme(); 45 | } 46 | 47 | @Override 48 | public void updateUI() { 49 | super.updateUI(); 50 | 51 | if (themeDetector != null) { 52 | applyTheme(); 53 | } 54 | } 55 | 56 | private void applyTheme() { 57 | String themeResource = themeDetector.isLightTheme() ? LIGHT_THEME : DARK_THEME; 58 | 59 | try { 60 | Theme theme = Theme.load(getClass().getResourceAsStream(themeResource)); 61 | theme.apply(this); 62 | } catch (IOException e) { 63 | outputError("Unable to apply rsyntax theme: " + e.getMessage()); 64 | } 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/burp/JWT4BContextMenuItemsProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.ui.contextmenu.ContextMenuEvent; 4 | import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; 5 | import burp.api.montoya.http.message.HttpRequestResponse; 6 | import burp.api.montoya.ui.contextmenu.MessageEditorHttpRequestResponse.SelectionContext; 7 | 8 | import app.controllers.JWTSuiteTabController; 9 | import model.Strings; 10 | 11 | import javax.swing.*; 12 | import java.awt.*; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class JWT4BContextMenuItemsProvider implements ContextMenuItemsProvider { 17 | private final JWTSuiteTabController jstC; 18 | 19 | public JWT4BContextMenuItemsProvider(JWTSuiteTabController jstC) { 20 | this.jstC = jstC; 21 | } 22 | 23 | @Override 24 | public List provideMenuItems(ContextMenuEvent event) { 25 | List menuItemList = new ArrayList<>(); 26 | 27 | // editor and selection need to be present 28 | if (event.messageEditorRequestResponse().isPresent() && event.messageEditorRequestResponse().get().selectionOffsets().isPresent()) { 29 | HttpRequestResponse requestResponse = event.messageEditorRequestResponse().get().requestResponse(); 30 | 31 | SelectionContext selectionContext = event.messageEditorRequestResponse().get().selectionContext(); 32 | int startIndex = event.messageEditorRequestResponse().get().selectionOffsets().get().startIndexInclusive(); 33 | int endIndex = event.messageEditorRequestResponse().get().selectionOffsets().get().endIndexExclusive(); 34 | 35 | String selectedText; 36 | 37 | if (selectionContext == SelectionContext.REQUEST) { 38 | selectedText = requestResponse.request().toString().substring(startIndex, endIndex); 39 | } else { 40 | selectedText = requestResponse.response().toString().substring(startIndex, endIndex); 41 | } 42 | 43 | JMenuItem retrieveSelectedRequestItem = new JMenuItem(Strings.CONTEXT_MENU_STRING); 44 | retrieveSelectedRequestItem.addActionListener(e -> jstC.contextActionSendJWTtoSuiteTab(selectedText, true)); 45 | 46 | menuItemList.add(retrieveSelectedRequestItem); 47 | } 48 | 49 | return menuItemList; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/burp/JWT4BEditorProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.api.montoya.MontoyaApi; 4 | import burp.api.montoya.ui.editor.extension.*; 5 | 6 | import app.controllers.JWTRequestTabController; 7 | import app.controllers.JWTResponseTabController; 8 | import app.controllers.JWTRequestInterceptTabController; 9 | import app.controllers.JWTResponseInterceptTabController; 10 | import gui.JWTInterceptTab; 11 | import gui.JWTViewTab; 12 | import gui.RSyntaxTextAreaFactory; 13 | import model.JWTInterceptModel; 14 | import model.JWTTabModel; 15 | 16 | public class JWT4BEditorProvider implements HttpRequestEditorProvider, HttpResponseEditorProvider { 17 | private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; 18 | private MontoyaApi api; 19 | 20 | public JWT4BEditorProvider(RSyntaxTextAreaFactory rSyntaxTextAreaFactory, MontoyaApi api) { 21 | this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; 22 | this.api = api; 23 | } 24 | 25 | @Override 26 | public ExtensionProvidedHttpRequestEditor provideHttpRequestEditor(EditorCreationContext creationContext) { 27 | ExtensionProvidedHttpRequestEditor jwtTC; 28 | 29 | if (creationContext.editorMode() == EditorMode.DEFAULT) { 30 | JWTInterceptModel jwtSTM = new JWTInterceptModel(); 31 | JWTInterceptTab jwtST = new JWTInterceptTab(jwtSTM, rSyntaxTextAreaFactory,api); 32 | jwtTC = new JWTRequestInterceptTabController(jwtSTM, jwtST); 33 | } else { 34 | // Read Only 35 | JWTTabModel jwtTM = new JWTTabModel(); 36 | JWTViewTab jwtVT = new JWTViewTab(jwtTM, rSyntaxTextAreaFactory, api); 37 | jwtTC = new JWTRequestTabController(jwtTM, jwtVT); 38 | } 39 | 40 | return jwtTC; 41 | } 42 | 43 | @Override 44 | public ExtensionProvidedHttpResponseEditor provideHttpResponseEditor(EditorCreationContext creationContext) { 45 | ExtensionProvidedHttpResponseEditor jwtTC; 46 | 47 | if (creationContext.editorMode() == EditorMode.DEFAULT) { 48 | JWTInterceptModel jwtSTM = new JWTInterceptModel(); 49 | JWTInterceptTab jwtST = new JWTInterceptTab(jwtSTM, rSyntaxTextAreaFactory,api); 50 | jwtTC = new JWTResponseInterceptTabController(jwtSTM, jwtST); 51 | } else { 52 | // Read Only 53 | JWTTabModel jwtTM = new JWTTabModel(); 54 | JWTViewTab jwtVT = new JWTViewTab(jwtTM, rSyntaxTextAreaFactory, api); 55 | jwtTC = new JWTResponseTabController(jwtTM, jwtVT); 56 | } 57 | 58 | return jwtTC; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/HighLightController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import app.helpers.Config; 4 | import app.helpers.SecretFinder; 5 | import app.tokenposition.ITokenPosition; 6 | import burp.api.montoya.core.Annotations; 7 | import burp.api.montoya.core.HighlightColor; 8 | import burp.api.montoya.http.handler.HttpHandler; 9 | import burp.api.montoya.http.handler.HttpRequestToBeSent; 10 | import burp.api.montoya.http.handler.HttpResponseReceived; 11 | import burp.api.montoya.http.handler.RequestToBeSentAction; 12 | import burp.api.montoya.http.handler.ResponseReceivedAction; 13 | 14 | // This controller handles the highlighting of entries in the HTTP history tab 15 | public class HighLightController implements HttpHandler { 16 | 17 | private String validSecret; 18 | 19 | @Override 20 | public RequestToBeSentAction handleHttpRequestToBeSent(HttpRequestToBeSent requestToBeSent) { 21 | ITokenPosition tokenPosition = ITokenPosition.findTokenPositionImplementation(requestToBeSent, true); 22 | boolean containsJWT = tokenPosition != null; 23 | if (containsJWT) { 24 | SecretFinder sfh = new SecretFinder(tokenPosition, requestToBeSent); 25 | 26 | this.validSecret = sfh.collectSecrets().stream() 27 | .filter(sfh::checkSecret) 28 | .findFirst() 29 | .orElse(null); 30 | 31 | updateAnnotations(requestToBeSent.annotations()); 32 | }else { 33 | this.validSecret = null; 34 | } 35 | 36 | return RequestToBeSentAction.continueWith(requestToBeSent); 37 | } 38 | 39 | @Override 40 | public ResponseReceivedAction handleHttpResponseReceived(HttpResponseReceived responseReceived) { 41 | boolean containsJWT = ITokenPosition.findTokenPositionImplementation(responseReceived, false) != null; 42 | if (containsJWT) { 43 | updateAnnotations(responseReceived.annotations()); 44 | } 45 | return ResponseReceivedAction.continueWith(responseReceived); 46 | } 47 | 48 | private void updateAnnotations(Annotations annotations) { 49 | if (!Config.interceptComment.isEmpty()) { 50 | annotations.setNotes(Config.interceptComment); 51 | } 52 | if (!Config.highlightColor.equals("None")) { 53 | annotations.setHighlightColor(HighlightColor.highlightColor(Config.highlightColor)); 54 | } 55 | 56 | if(this.validSecret != null) { 57 | annotations.setNotes(Config.SecretFoundInterceptComment.concat(this.validSecret)); 58 | annotations.setHighlightColor(HighlightColor.highlightColor(Config.SecretFoundHighlightColor)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/app/TestCookieDetection.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import java.util.Map; 4 | import java.util.stream.Stream; 5 | 6 | import app.tokenposition.Cookie; 7 | import burp.api.montoya.MontoyaExtension; 8 | import burp.api.montoya.http.message.requests.HttpRequest; 9 | import org.apache.commons.text.StringSubstitutor; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.junit.jupiter.params.ParameterizedTest; 12 | import org.junit.jupiter.params.provider.Arguments; 13 | import org.junit.jupiter.params.provider.MethodSource; 14 | 15 | import static app.TestConstants.*; 16 | import static app.TestConstants.REQUEST_TEMPLATE; 17 | import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.params.provider.Arguments.arguments; 20 | 21 | @ExtendWith(MontoyaExtension.class) 22 | class TestCookieDetection { 23 | 24 | static Stream cookieHeaderAndToken() { 25 | return Stream.of( 26 | arguments("Cookie: token=" + HS256_TOKEN + "; othercookie=1234", HS256_TOKEN), 27 | arguments("Cookie: token=" + HS256_TOKEN + "; othercookie=1234;", HS256_TOKEN), 28 | arguments("Cookie: othercookie=1234; secondcookie=4321; token=" + HS256_TOKEN + ";", HS256_TOKEN), 29 | arguments("Cookie: emptycookie=; secondcookie=4321; token=" + HS256_TOKEN + ";", HS256_TOKEN), 30 | arguments("Cookie: secondcookie=4321; emptycookie=; token=" + HS256_TOKEN + ";", HS256_TOKEN), 31 | arguments("Cookie: secondcookie=4321; weirdcookie ; token=" + HS256_TOKEN + ";", HS256_TOKEN), 32 | arguments("Cookie: othercookie=1234; token=" + HS256_TOKEN, HS256_TOKEN), 33 | arguments("Cookie: token=" + HS256_TOKEN, HS256_TOKEN), 34 | arguments("Cookie: token=" + INVALID_HEADER_TOKEN, null), 35 | arguments("Cookie: test=besst", null) 36 | ); 37 | } 38 | 39 | @MethodSource("cookieHeaderAndToken") 40 | @ParameterizedTest(name = "{0}") 41 | void testCookie(String cookieHeader, String cookieToken) { 42 | Map params = Map.of( 43 | "ADD_HEADER", cookieHeader); 44 | 45 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 46 | 47 | Cookie cookie = new Cookie(httpRequest,true); 48 | 49 | assertThat(cookie.positionFound()).isEqualTo(cookieToken != null); 50 | assertThat(cookie.getToken()).isEqualTo((cookieToken != null) ? cookieToken : ""); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/O365.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import java.nio.charset.StandardCharsets; 4 | import java.security.MessageDigest; 5 | import java.security.NoSuchAlgorithmException; 6 | import java.util.Base64; 7 | 8 | import org.apache.commons.codec.binary.Hex; 9 | 10 | import com.auth0.jwt.algorithms.Algorithm; 11 | import com.fasterxml.jackson.databind.JsonNode; 12 | 13 | import app.algorithm.AlgorithmWrapper; 14 | import model.CustomJWToken; 15 | 16 | public class O365 { 17 | 18 | O365() { 19 | 20 | } 21 | 22 | public static boolean isO365Request(CustomJWToken token, String tokenalgo) { 23 | return token.getHeaderJsonNode().get("ctx") != null && tokenalgo.toUpperCase().contains(AlgorithmWrapper.HS256.name()); 24 | } 25 | 26 | public static void handleO365(String key, CustomJWToken token) throws NoSuchAlgorithmException { 27 | String label = "AzureAD-SecureConversation"; 28 | String ctx = token.getHeaderJsonNode().get("ctx").asText(); 29 | byte[] ctxbytes = Base64.getDecoder().decode(ctx); 30 | 31 | JsonNode kdfVer = token.getHeaderJsonNode().get("kdf_ver"); 32 | boolean tokenCreatedWithKDFv2 = kdfVer != null && kdfVer.asInt() == 2; 33 | if (tokenCreatedWithKDFv2) { 34 | byte[] fullctxbytes = new byte[24 + token.getPayloadJson().replace(" ", "").replace("\n", "").length()]; 35 | System.arraycopy(ctxbytes, 0, fullctxbytes, 0, 24); 36 | System.arraycopy(token.getPayloadJson().replace(" ", "").replace("\n", "").getBytes(StandardCharsets.ISO_8859_1), 0, fullctxbytes, 24, 37 | token.getPayloadJson().replace(" ", "").replace("\n", "").length()); 38 | MessageDigest digest = MessageDigest.getInstance("SHA-256"); 39 | ctxbytes = digest.digest(fullctxbytes); 40 | } 41 | 42 | byte[] newArr = new byte[4 + label.getBytes(StandardCharsets.UTF_8).length + 1 + ctxbytes.length + 4]; 43 | System.arraycopy(new byte[] { (byte) 0x00, 0x00, 0x00, 0x01 }, 0, newArr, 0, 4); 44 | System.arraycopy(label.getBytes(StandardCharsets.UTF_8), 0, newArr, 4, 26); 45 | System.arraycopy(new byte[] { (byte) 0x00 }, 0, newArr, 30, 1); 46 | System.arraycopy(ctxbytes, 0, newArr, 31, ctxbytes.length); 47 | System.arraycopy(new byte[] { (byte) 0x00, 0x00, 0x01, 0x00 }, 0, newArr, newArr.length - 4, 4); 48 | byte[] keyData = key.getBytes(StandardCharsets.ISO_8859_1); 49 | byte[] hmacSha256 = KeyHelper.calcHmacSha256(keyData, newArr); 50 | 51 | Algorithm algo = AlgorithmWrapper.getSignerAlgorithm(token.getAlgorithm(), hmacSha256); 52 | Output.output("Signing with MS O365 derived key: " + Hex.encodeHexString(hmacSha256)); 53 | token.calculateAndSetSignature(algo); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/model/JWTTabModel.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.awt.Color; 4 | import java.util.List; 5 | 6 | import app.helpers.CookieFlagWrapper; 7 | 8 | public class JWTTabModel { 9 | 10 | private String key = ""; 11 | private String keyLabel = ""; 12 | private int hashCode; 13 | private String verificationLabel = ""; 14 | private Color verificationColor; 15 | private String jwt; 16 | private String jwtJSON; 17 | private CookieFlagWrapper cFW; 18 | private List tcl; 19 | 20 | public JWTTabModel() { 21 | } 22 | 23 | public JWTTabModel(String keyValue, byte[] content) { 24 | this.key = keyValue; 25 | this.hashCode = new String(content).hashCode(); 26 | this.verificationColor = Settings.COLOR_UNDEFINED; 27 | } 28 | 29 | @Override 30 | public boolean equals(Object otherObj) { 31 | if (otherObj instanceof JWTTabModel otherViewState) { 32 | return (otherViewState.getHashCode() == this.getHashCode()); 33 | } 34 | return false; 35 | } 36 | 37 | public String getKey() { 38 | return key; 39 | } 40 | 41 | public int getHashCode() { 42 | return hashCode; 43 | } 44 | 45 | public void setKeyValueAndHash(String keyValue, int hashCode) { 46 | this.key = keyValue; 47 | this.hashCode = hashCode; 48 | } 49 | 50 | public void setVerificationResult(String verificationResult) { 51 | this.verificationLabel = verificationResult; 52 | } 53 | 54 | public String getKeyLabel() { 55 | return keyLabel; 56 | } 57 | 58 | public String getVerificationLabel() { 59 | return key.isEmpty() ? "" : verificationLabel; 60 | } 61 | 62 | public void setVerificationLabel(String verificationLabel) { 63 | this.verificationLabel = verificationLabel; 64 | } 65 | 66 | public Color getVerificationColor() { 67 | return key.isEmpty() ? Settings.COLOR_UNDEFINED : verificationColor; 68 | } 69 | 70 | public void setVerificationColor(Color verificationColor) { 71 | this.verificationColor = verificationColor; 72 | } 73 | 74 | public void setKey(String key) { 75 | this.key = key; 76 | } 77 | 78 | public void setJWT(String token) { 79 | this.jwt = token; 80 | } 81 | 82 | public String getJWT() { 83 | return jwt; 84 | } 85 | 86 | public String getJWTJSON() { 87 | return jwtJSON; 88 | } 89 | 90 | public void setJWTJSON(String readableFormat) { 91 | jwtJSON = readableFormat; 92 | } 93 | 94 | public void setcFW(CookieFlagWrapper cFW) { 95 | this.cFW = cFW; 96 | } 97 | 98 | public CookieFlagWrapper getcFW() { 99 | return cFW; 100 | } 101 | 102 | public void setTimeClaims(List tcl) { 103 | this.tcl = tcl; 104 | } 105 | 106 | public String getTimeClaimsAsText() { 107 | return TimeClaim.getTimeClaimsAsHTML(tcl); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/app/tokenposition/ITokenPosition.java: -------------------------------------------------------------------------------- 1 | package app.tokenposition; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import app.helpers.CookieFlagWrapper; 7 | import app.helpers.Output; 8 | 9 | import burp.api.montoya.http.message.HttpMessage; 10 | import burp.api.montoya.http.message.requests.HttpRequest; 11 | import burp.api.montoya.http.message.responses.HttpResponse; 12 | import model.Strings; 13 | 14 | public abstract class ITokenPosition { 15 | 16 | protected boolean isRequest; 17 | protected HttpMessage httpMessage; 18 | protected String token; 19 | 20 | protected ITokenPosition(HttpMessage httpMessage, boolean isRequest) { 21 | this.httpMessage = httpMessage; 22 | this.isRequest = isRequest; 23 | } 24 | 25 | public abstract boolean positionFound(); 26 | 27 | public abstract HttpRequest getRequest(); 28 | 29 | public abstract HttpResponse getResponse(); 30 | 31 | private static CookieFlagWrapper cookieFlagWrap; 32 | 33 | public static ITokenPosition findTokenPositionImplementation(HttpMessage httpMessage, boolean isRequest) { 34 | List> implementations = Arrays.asList(AuthorizationBearerHeader.class, PostBody.class, Body.class, Cookie.class); 35 | 36 | for (Class implClass : implementations) { 37 | try { 38 | ITokenPosition impl = (ITokenPosition) implClass.getConstructors()[0].newInstance(httpMessage, isRequest); 39 | 40 | if (impl.positionFound()) { 41 | if (impl instanceof Cookie) { 42 | cookieFlagWrap = ((Cookie) impl).getcFW(); 43 | } else { 44 | cookieFlagWrap = new CookieFlagWrapper(false, false, false); 45 | } 46 | return impl; 47 | } 48 | } catch (Exception e) { 49 | // sometimes 'isEnabled' is called in order to build the views 50 | // before an actual request / response passes through - in that case 51 | // it is not worth reporting 52 | if (!e.getMessage().equals("Request cannot be null") && !e.getMessage().equals("1")) { 53 | Output.outputError(e.getMessage()); 54 | } 55 | return null; 56 | } 57 | } 58 | return null; 59 | } 60 | 61 | public String getToken() { 62 | return (this.token != null) ? this.token : ""; 63 | } 64 | 65 | public void replaceToken(String newToken) { 66 | this.token = newToken; 67 | } 68 | 69 | public void addHeader(String name, String value) { 70 | // add header 71 | if (isRequest) { 72 | HttpRequest request = (HttpRequest) httpMessage; 73 | httpMessage = request.withAddedHeader(name, value); 74 | } else { 75 | HttpResponse response = (HttpResponse) httpMessage; 76 | httpMessage = response.withAddedHeader(name, value); 77 | } 78 | } 79 | 80 | public void cleanJWTHeaders() { 81 | // remove headers that start with Strings.JWTHeaderPrefix) 82 | if (isRequest) { 83 | HttpRequest request = (HttpRequest) httpMessage; 84 | httpMessage = request.withRemovedHeader(Strings.JWT_HEADER_PREFIX); 85 | } else { 86 | HttpResponse response = (HttpResponse) httpMessage; 87 | httpMessage = response.withRemovedHeader(Strings.JWT_HEADER_PREFIX); 88 | } 89 | } 90 | 91 | public CookieFlagWrapper getcFW() { 92 | return cookieFlagWrap; 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/app/tokenposition/AuthorizationBearerHeader.java: -------------------------------------------------------------------------------- 1 | package app.tokenposition; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | 6 | import burp.api.montoya.http.message.HttpHeader; 7 | import burp.api.montoya.http.message.HttpMessage; 8 | import burp.api.montoya.http.message.requests.HttpRequest; 9 | import burp.api.montoya.http.message.responses.HttpResponse; 10 | import model.CustomJWToken; 11 | 12 | // finds and replaces JWT's in authorization headers 13 | public class AuthorizationBearerHeader extends ITokenPosition { 14 | 15 | private Optional containedJwt; 16 | private String headerName; 17 | private String headerKeyword; 18 | 19 | public AuthorizationBearerHeader(HttpMessage httpMessage, boolean isRequest) { 20 | super(httpMessage, isRequest); 21 | } 22 | 23 | public boolean positionFound() { 24 | try { 25 | for (HttpHeader header : httpMessage.headers()) { 26 | containedJwt = containsJwt(header.value(), List.of("Bearer","bearer","BEARER")); 27 | if (containedJwt.isPresent()) { 28 | headerName = header.name(); 29 | this.token = containedJwt.get(); 30 | return true; 31 | } 32 | } 33 | } catch (Exception ignored) { 34 | System.out.println(ignored.getMessage()); 35 | } 36 | return false; 37 | } 38 | 39 | public Optional containsJwt(String headerValue, List jwtKeywords) { 40 | for (String keyword : jwtKeywords) { 41 | boolean usesCustomAuthType = !headerValue.startsWith(keyword) && (headerValue.contains(" ") && headerValue.contains("ey")); 42 | if (usesCustomAuthType) { 43 | keyword = headerValue.split(" ")[0]; 44 | } 45 | String potentialJwt = headerValue.replace(keyword, "").trim(); 46 | if (CustomJWToken.isValidJWT(potentialJwt,false)) { 47 | headerKeyword = keyword; 48 | return Optional.of(potentialJwt); 49 | } 50 | } 51 | if(headerValue.toLowerCase().startsWith("ey") || containsExactlyTwoDots(headerValue)) { 52 | String potentialJwt = headerValue.trim(); 53 | if (CustomJWToken.isValidJWT(potentialJwt,false)) { 54 | headerKeyword = ""; 55 | return Optional.of(potentialJwt); 56 | } 57 | } 58 | return Optional.empty(); 59 | } 60 | 61 | @Override 62 | public HttpRequest getRequest() { 63 | HttpRequest httpRequest = HttpRequest.httpRequest(httpMessage.toString()); 64 | if (containedJwt.isEmpty()) { 65 | return httpRequest; 66 | } 67 | return httpRequest.withUpdatedHeader(headerName, headerKeyword + needsSpace(headerKeyword) + token); 68 | } 69 | 70 | @Override 71 | public HttpResponse getResponse() { 72 | HttpResponse httpResponse = HttpResponse.httpResponse(httpMessage.toString()); 73 | if (containedJwt.isEmpty()) { 74 | return httpResponse; 75 | } 76 | return httpResponse.withUpdatedHeader(headerName, headerKeyword + needsSpace(headerKeyword) + token); 77 | } 78 | 79 | 80 | private String needsSpace(String headerKeyword) { 81 | return headerKeyword.equals("")?"":" "; 82 | } 83 | 84 | 85 | private boolean containsExactlyTwoDots(String str) { 86 | int firstDotIndex = str.indexOf('.'); 87 | if (firstDotIndex == -1) { 88 | return false; 89 | } 90 | int secondDotIndex = str.indexOf('.', firstDotIndex + 1); 91 | if (secondDotIndex == -1) { 92 | return false; 93 | } 94 | return str.indexOf('.', secondDotIndex + 1) == -1; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/test/java/app/TestPostDetection.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | 4 | import app.tokenposition.PostBody; 5 | import burp.api.montoya.MontoyaExtension; 6 | import burp.api.montoya.http.message.requests.HttpRequest; 7 | import org.apache.commons.text.StringSubstitutor; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import java.util.Map; 15 | import java.util.stream.Stream; 16 | 17 | import static app.TestConstants.*; 18 | import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | import static org.junit.jupiter.params.provider.Arguments.arguments; 21 | 22 | @ExtendWith(MontoyaExtension.class) 23 | class TestPostDetection { 24 | 25 | static Stream postDataAndDetectedTokens() { 26 | return Stream.of( 27 | arguments("test=best&token=" + HS256_TOKEN, HS256_TOKEN), 28 | arguments("token=" + HS256_TOKEN, HS256_TOKEN), 29 | arguments("token=" + HS256_TOKEN + "&test=best", HS256_TOKEN) 30 | ); 31 | } 32 | 33 | @MethodSource("postDataAndDetectedTokens") 34 | @ParameterizedTest 35 | void testPostBody(String body, String bodyToken) { 36 | Map params = Map.of( 37 | "METHOD", METHOD_POST, 38 | "BODY", body); 39 | 40 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 41 | 42 | PostBody pb = new PostBody(httpRequest, true); 43 | 44 | assertThat(pb.positionFound()).isTrue(); 45 | assertThat(pb.getToken()).isEqualTo(bodyToken); 46 | } 47 | 48 | 49 | static Stream postDataWhereNoTokenDetected() { 50 | return Stream.of( 51 | arguments("token=" + INVALID_HEADER_TOKEN + "&test=best"), 52 | arguments("") 53 | ); 54 | } 55 | 56 | @MethodSource("postDataWhereNoTokenDetected") 57 | @ParameterizedTest 58 | void testPostBodyNoToken(String body) { 59 | Map params = Map.of( 60 | "METHOD", METHOD_POST, 61 | "BODY", body); 62 | 63 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 64 | 65 | PostBody pb = new PostBody(httpRequest, true); 66 | 67 | assertThat(pb.positionFound()).isFalse(); 68 | assertThat(pb.getToken()).isEmpty(); 69 | } 70 | 71 | @Test 72 | void testPostBodyReplace() { 73 | String body1 = "test=best&token=" + HS256_TOKEN; 74 | Map params1 = Map.of( 75 | "METHOD", METHOD_POST, 76 | "BODY", body1); 77 | 78 | HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); 79 | 80 | PostBody pb1 = new PostBody(httpRequest1, true); 81 | pb1.positionFound(); 82 | 83 | pb1.replaceToken(HS256_TOKEN_2); 84 | 85 | // 86 | String body2 = "test=best&token=" + HS256_TOKEN_2; 87 | Map params2 = Map.of( 88 | "METHOD", METHOD_POST, 89 | "BODY", body2); 90 | 91 | HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); 92 | 93 | PostBody pb2 = new PostBody(httpRequest2, true); 94 | pb2.positionFound(); 95 | 96 | // 97 | assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/model/Strings.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.FileReader; 5 | import java.io.IOException; 6 | 7 | public class Strings { 8 | 9 | private Strings() { 10 | } 11 | 12 | public static final String CONTEXT_MENU_STRING = "Send selected text to JSON Web Tokens Tab to decode"; 13 | 14 | public static final String ORIGINAL_TOKEN_STATE = "Original"; 15 | public static final String UPDATED_TOKEN_STATE = "Token updated"; 16 | 17 | public static final String ACCEPT_CHANGES = "Accept Changes"; 18 | public static final String RECALC_SIGNATURE = "Recalculate Signature"; 19 | public static final String ORIGINAL_TOKEN = "Original Token"; 20 | public static final String UPDATE_ALGO_SIG = "Update Algorithm / Signature"; 21 | public static final String NO_SECRET_PROVIDED = "No secret provided"; 22 | public static final String DECODED_JWT = "Decoded JWT"; 23 | public static final String ENTER_JWT = "Enter JWT"; 24 | 25 | public static final String VALID_VERFICIATION = "Signature verified"; 26 | public static final String INVALID_KEY_VERIFICATION = "Invalid Key"; 27 | public static final String INVALID_SIGNATURE_VERIFICATION = "Cannot verify Signature"; 28 | public static final String INVALID_CLAIM_VERIFICATION = "Not all Claims accepted"; 29 | public static final String GENERIC_ERROR_VERIFICATION = "Invalid Signature / wrong key / claim failed"; 30 | 31 | public static final String RECALC_KEY_INTERCEPT = "Secret / Key for Signature recalculation:"; 32 | 33 | public static final String DONT_MODIFY = "Do not automatically modify signature"; 34 | public static final String KEEP_ORIG_SIG = "Keep original signature"; 35 | public static final String RANDOM_KEY = "Sign with random key pair"; 36 | public static final String ENTER_SECRET_KEY = "Enter Secret / Key"; 37 | public static final String CHOOSE_SIG = "Load Secret / Key from File"; 38 | 39 | public static final String DONT_MODIFY_TT = "The signature will be taken straight out of the editable field to the left"; 40 | public static final String RECALC_SIG_TT = "The signature will be recalculated depending
on the content and algorithm set"; 41 | public static final String KEEP_ORIG_SIG_TT = "The signature originally sent will be preserved and sent unchanged"; 42 | public static final String RANDOM_KEY_TT = "The signature will be recalculated depending
on the content and algorithm set
by a random signature / key"; 43 | public static final String CHOOSE_SIG_TT = "Load the secret / key from a file chosen by your OS file picker"; 44 | 45 | public static final String CREDIT_TITLE = "JSON Web Tokens - About"; 46 | 47 | public static final String JWT_HEADER_PREFIX = "JWT4B"; 48 | public static final String JWT_HEADER_INFO = "The following headers are added automatically, in order to log the keys"; 49 | 50 | public static String filePathToString(String filePath) { 51 | StringBuilder contentBuilder = new StringBuilder(); 52 | try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { 53 | 54 | String sCurrentLine; 55 | while ((sCurrentLine = br.readLine()) != null) { 56 | contentBuilder.append(sCurrentLine).append(System.lineSeparator()); 57 | } 58 | } catch (IOException e) { 59 | return "Failed to load file ("+e.getMessage()+")"; 60 | } 61 | String result = contentBuilder.toString(); 62 | return result.substring(0, result.length() - System.lineSeparator().length()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeHttpResponse.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import burp.api.montoya.http.message.Cookie; 4 | import burp.api.montoya.http.message.HttpHeader; 5 | import burp.api.montoya.http.message.MimeType; 6 | import burp.api.montoya.http.message.StatusCodeClass; 7 | import burp.api.montoya.http.message.responses.HttpResponse; 8 | import burp.api.montoya.http.message.responses.analysis.Attribute; 9 | import burp.api.montoya.http.message.responses.analysis.AttributeType; 10 | import burp.api.montoya.http.message.responses.analysis.KeywordCount; 11 | 12 | import java.util.List; 13 | 14 | public class FakeHttpResponse extends FakeHttpMessage implements HttpResponse { 15 | 16 | public FakeHttpResponse(String message) { 17 | super(message); 18 | } 19 | 20 | @Override 21 | public short statusCode() { 22 | return 0; 23 | } 24 | 25 | @Override 26 | public String reasonPhrase() { 27 | return ""; 28 | } 29 | 30 | @Override 31 | public boolean isStatusCodeClass(StatusCodeClass statusCodeClass) { 32 | return false; 33 | } 34 | 35 | @Override 36 | public List cookies() { 37 | return List.of(); 38 | } 39 | 40 | @Override 41 | public Cookie cookie(String name) { 42 | return null; 43 | } 44 | 45 | @Override 46 | public String cookieValue(String name) { 47 | return ""; 48 | } 49 | 50 | @Override 51 | public boolean hasCookie(String name) { 52 | return false; 53 | } 54 | 55 | @Override 56 | public boolean hasCookie(Cookie cookie) { 57 | return false; 58 | } 59 | 60 | @Override 61 | public MimeType mimeType() { 62 | return null; 63 | } 64 | 65 | @Override 66 | public MimeType statedMimeType() { 67 | return null; 68 | } 69 | 70 | @Override 71 | public MimeType inferredMimeType() { 72 | return null; 73 | } 74 | 75 | @Override 76 | public List keywordCounts(String... keywords) { 77 | return List.of(); 78 | } 79 | 80 | @Override 81 | public List attributes(AttributeType... types) { 82 | return List.of(); 83 | } 84 | 85 | @Override 86 | public HttpResponse copyToTempFile() { 87 | return null; 88 | } 89 | 90 | @Override 91 | public HttpResponse withStatusCode(short statusCode) { 92 | return null; 93 | } 94 | 95 | @Override 96 | public HttpResponse withReasonPhrase(String reasonPhrase) { 97 | return null; 98 | } 99 | 100 | @Override 101 | public HttpResponse withHttpVersion(String httpVersion) { 102 | return null; 103 | } 104 | 105 | @Override 106 | public HttpResponse withBody(String body) { 107 | return null; 108 | } 109 | 110 | @Override 111 | public HttpResponse withBody(ByteArray body) { 112 | return null; 113 | } 114 | 115 | @Override 116 | public HttpResponse withAddedHeader(HttpHeader header) { 117 | return null; 118 | } 119 | 120 | @Override 121 | public HttpResponse withAddedHeader(String name, String value) { 122 | return null; 123 | } 124 | 125 | @Override 126 | public HttpResponse withUpdatedHeader(HttpHeader header) { 127 | return null; 128 | } 129 | 130 | @Override 131 | public HttpResponse withUpdatedHeader(String name, String value) { 132 | return null; 133 | } 134 | 135 | @Override 136 | public HttpResponse withRemovedHeader(HttpHeader header) { 137 | return null; 138 | } 139 | 140 | @Override 141 | public HttpResponse withRemovedHeader(String name) { 142 | return null; 143 | } 144 | 145 | @Override 146 | public HttpResponse withMarkers(List markers) { 147 | return null; 148 | } 149 | 150 | @Override 151 | public HttpResponse withMarkers(Marker... markers) { 152 | return null; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/app/TestAuthorizationDetection.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; 4 | 5 | import static app.TestConstants.*; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | import burp.api.montoya.MontoyaExtension; 10 | import burp.api.montoya.http.message.requests.HttpRequest; 11 | 12 | import app.tokenposition.AuthorizationBearerHeader; 13 | 14 | import org.apache.commons.text.StringSubstitutor; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | 18 | import java.util.Map; 19 | 20 | @ExtendWith(MontoyaExtension.class) 21 | class TestAuthorizationDetection { 22 | 23 | @Test 24 | void testAuthValid() { 25 | Map params = Map.of("ADD_HEADER", "Authorization: Bearer " + HS256_TOKEN); 26 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 27 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 28 | 29 | assertThat(abh.positionFound()).isTrue(); 30 | assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); 31 | } 32 | 33 | @Test 34 | void testAuthValidLowerCase() { 35 | Map params = Map.of("ADD_HEADER", "authorization: bearer " + HS256_TOKEN); 36 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 37 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 38 | 39 | assertThat(abh.positionFound()).isTrue(); 40 | assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); 41 | } 42 | 43 | @Test 44 | void testAuthValidNonAuthHeader() { 45 | Map params = Map.of("ADD_HEADER", "X-AUTH: bearer " + HS256_TOKEN); 46 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 47 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 48 | 49 | assertThat(abh.positionFound()).isTrue(); 50 | assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); 51 | } 52 | 53 | @Test 54 | void testRandomHeaderWithoutBearer() { 55 | Map params = Map.of("ADD_HEADER", "Token: " + HS256_TOKEN); 56 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 57 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 58 | 59 | assertThat(abh.positionFound()).isTrue(); 60 | assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); 61 | } 62 | 63 | @Test 64 | void testRandomHeaderWithoutBearerAndSpaces() { 65 | Map params = Map.of("ADD_HEADER", "Foo: " + HS256_TOKEN+" "); 66 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 67 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 68 | 69 | assertThat(abh.positionFound()).isTrue(); 70 | assertThat(abh.getToken()).isEqualTo(HS256_TOKEN); 71 | } 72 | 73 | @Test 74 | void testRandomHeaderWithInvalid() { 75 | Map params = Map.of("ADD_HEADER", "Token: " + INVALID_HEADER_TOKEN); 76 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 77 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 78 | 79 | assertThat(abh.positionFound()).isFalse(); 80 | } 81 | 82 | @Test 83 | void testAuthInvalid() { 84 | Map params = Map.of("ADD_HEADER", "Authorization: Bearer " + INVALID_HEADER_TOKEN); 85 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 86 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 87 | 88 | assertThat(abh.positionFound()).isFalse(); 89 | } 90 | 91 | @Test 92 | void testAuthInvalid2() { 93 | Map params = Map.of("ADD_HEADER", "Authorization: Bearer topsecret123456789!"); 94 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 95 | AuthorizationBearerHeader abh = new AuthorizationBearerHeader(httpRequest, true); 96 | 97 | assertThat(abh.positionFound()).isFalse(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/app/tokenposition/Body.java: -------------------------------------------------------------------------------- 1 | package app.tokenposition; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | import burp.api.montoya.http.message.HttpMessage; 6 | import burp.api.montoya.http.message.requests.HttpRequest; 7 | import burp.api.montoya.http.message.responses.HttpResponse; 8 | import org.apache.commons.lang.StringUtils; 9 | 10 | import com.eclipsesource.json.Json; 11 | import com.eclipsesource.json.JsonObject; 12 | 13 | import app.helpers.KeyValuePair; 14 | import app.helpers.TokenChecker; 15 | 16 | //finds and replaces JWT's in HTTP bodies 17 | public class Body extends ITokenPosition { 18 | 19 | public Body(HttpMessage httpMessage, boolean isRequest) { 20 | super(httpMessage, isRequest); 21 | } 22 | 23 | @Override 24 | public boolean positionFound() { 25 | KeyValuePair postJWT = getJWTFromBody(); 26 | if (postJWT != null) { 27 | token = postJWT.getValue(); 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | private KeyValuePair getJWTFromBody() { 34 | KeyValuePair ret; 35 | if ((ret = getJWTFromBodyWithParameters()) != null) { 36 | return ret; 37 | } else if ((ret = getJWTFromBodyWithJson()) != null) { 38 | return ret; 39 | } else { 40 | return getJWTFromBodyWithoutParametersOrJSON(); 41 | } 42 | } 43 | 44 | private KeyValuePair getJWTFromBodyWithoutParametersOrJSON() { 45 | String body = this.httpMessage.bodyToString(); 46 | 47 | String[] split = StringUtils.split(body); 48 | for (String strg : split) { 49 | if (TokenChecker.isValidJWT(strg)) { 50 | return new KeyValuePair("", strg); 51 | } 52 | } 53 | 54 | return null; 55 | } 56 | 57 | private KeyValuePair getJWTFromBodyWithJson() { 58 | String body = this.httpMessage.bodyToString(); 59 | 60 | JsonObject obj; 61 | try { 62 | if (body.length() < 2) { 63 | return null; 64 | } 65 | obj = Json.parse(body).asObject(); 66 | } catch (Exception e) { 67 | return null; 68 | } 69 | return lookForJwtInJsonObject(obj); 70 | } 71 | 72 | private KeyValuePair lookForJwtInJsonObject(JsonObject object) { 73 | KeyValuePair rec; 74 | for (String name : object.names()) { 75 | if (object.get(name).isString()) { 76 | if (TokenChecker.isValidJWT(object.get(name).asString())) { 77 | return new KeyValuePair(name, object.get(name).asString().trim()); 78 | } 79 | } else if (object.get(name).isObject()) { 80 | if ((rec = lookForJwtInJsonObject(object.get(name).asObject())) != null) { 81 | return rec; 82 | } 83 | } 84 | } 85 | return null; 86 | } 87 | 88 | private KeyValuePair getJWTFromBodyWithParameters() { 89 | String body = this.httpMessage.bodyToString(); 90 | 91 | int from = 0; 92 | int index = body.contains("&") ? body.indexOf("&") : body.length(); 93 | int parameterCount = StringUtils.countMatches(body, "&") + 1; 94 | 95 | for (int i = 0; i < parameterCount; i++) { 96 | String parameter = body.substring(from, index); 97 | parameter = parameter.replace("&", ""); 98 | 99 | String[] parameterSplit = parameter.split(Pattern.quote("=")); 100 | if (parameterSplit.length > 1) { 101 | String name = parameterSplit[0]; 102 | String value = parameterSplit[1]; 103 | if (TokenChecker.isValidJWT(value)) { 104 | return new KeyValuePair(name, value); 105 | } 106 | 107 | from = index; 108 | index = body.indexOf("&", index + 1); 109 | if (index == -1) { 110 | index = body.length(); 111 | } 112 | } 113 | } 114 | 115 | return null; 116 | } 117 | 118 | @Override 119 | public HttpRequest getRequest() { 120 | return HttpRequest.httpRequest(replaceTokenImpl(this.token, httpMessage.toString())); 121 | } 122 | 123 | @Override 124 | public HttpResponse getResponse() { 125 | return HttpResponse.httpResponse(replaceTokenImpl(this.token, httpMessage.toString())); 126 | } 127 | 128 | private String replaceTokenImpl(String newToken, String httpMessage) { 129 | String newMessage = httpMessage; 130 | 131 | KeyValuePair postJWT = getJWTFromBody(); 132 | if (postJWT != null) { 133 | newMessage = httpMessage.replace(postJWT.getValue(), newToken); 134 | } 135 | 136 | return newMessage; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/app/tokenposition/Cookie.java: -------------------------------------------------------------------------------- 1 | package app.tokenposition; 2 | 3 | import java.util.List; 4 | 5 | import app.helpers.CookieFlagWrapper; 6 | import app.helpers.KeyValuePair; 7 | import app.helpers.TokenChecker; 8 | import burp.api.montoya.http.message.HttpHeader; 9 | import burp.api.montoya.http.message.HttpMessage; 10 | import burp.api.montoya.http.message.params.HttpParameter; 11 | import burp.api.montoya.http.message.requests.HttpRequest; 12 | import burp.api.montoya.http.message.responses.HttpResponse; 13 | 14 | //finds and replaces JWT's in cookies 15 | public class Cookie extends ITokenPosition { 16 | 17 | private static final String SET_COOKIE_HEADER = "Set-Cookie"; 18 | private static final String COOKIE_HEADER = "Cookie"; 19 | 20 | private CookieFlagWrapper cFW; 21 | private KeyValuePair cookieHeader; 22 | 23 | public Cookie(HttpMessage httpMessage, boolean isRequest) { 24 | super(httpMessage, isRequest); 25 | } 26 | 27 | @Override 28 | public boolean positionFound() { 29 | try { 30 | List headers = this.httpMessage.headers(); 31 | 32 | cookieHeader = getJWTInCookieHeader(headers); 33 | if (cookieHeader != null) { 34 | token = cookieHeader.getValue(); 35 | return true; 36 | } 37 | } catch (Exception ignored) { 38 | // 39 | } 40 | 41 | return false; 42 | } 43 | 44 | @Override 45 | public HttpRequest getRequest() { 46 | HttpRequest httpRequest = (HttpRequest) httpMessage; 47 | return httpRequest.withParameter(HttpParameter.cookieParameter(cookieHeader.getName(), token)); 48 | } 49 | 50 | @Override 51 | public HttpResponse getResponse() { 52 | return HttpResponse.httpResponse(replaceTokenImpl(this.token, httpMessage.toString())); 53 | } 54 | 55 | private String replaceTokenImpl(String newToken, String httpMessageAsString) { 56 | String newMessage = httpMessageAsString; 57 | List headers = this.httpMessage.headers(); 58 | 59 | KeyValuePair cookieJWT = getJWTInCookieHeader(headers); 60 | if (cookieJWT != null) { 61 | newMessage = httpMessageAsString.replace(cookieJWT.getValue(), newToken); 62 | } 63 | 64 | return newMessage; 65 | } 66 | 67 | // finds the first jwt in the set-cookie or cookie header(s) 68 | public KeyValuePair getJWTInCookieHeader(List headers) { 69 | cFW = new CookieFlagWrapper(false, false, false); 70 | 71 | for (HttpHeader httpHeader : headers) { 72 | if (httpHeader.name().regionMatches(true, 0, SET_COOKIE_HEADER, 0, SET_COOKIE_HEADER.length())) { 73 | String setCookieValue = httpHeader.value(); 74 | if (setCookieValue.length() > 1 && setCookieValue.contains("=")) { // sanity check 75 | int nameMarkerPos = setCookieValue.indexOf("="); 76 | String name = setCookieValue.substring(0,nameMarkerPos); 77 | String value = setCookieValue.substring(nameMarkerPos+1); 78 | int flagMarker = value.indexOf(";"); 79 | if (flagMarker != -1) { 80 | value = value.substring(0, flagMarker); 81 | cFW = new CookieFlagWrapper(true, setCookieValue.toLowerCase().contains("; secure"), setCookieValue.toLowerCase().contains("; httponly")); 82 | } else { 83 | cFW = new CookieFlagWrapper(true, false, false); 84 | } 85 | if (TokenChecker.isValidJWT(value)) { 86 | return new KeyValuePair(name, value); 87 | } 88 | } 89 | } 90 | 91 | if (httpHeader.name().regionMatches(true, 0, COOKIE_HEADER, 0, COOKIE_HEADER.length())) { 92 | String cookieHeaderValue = httpHeader.value(); 93 | if (cookieHeaderValue != null && !cookieHeaderValue.isEmpty()) { 94 | String[] pairs = cookieHeaderValue.split(";\\s*"); 95 | for (String pair : pairs) { 96 | String[] parts = pair.split("=", 2); 97 | if (parts.length == 2) { 98 | String name = parts[0].trim(); 99 | String value = parts[1].trim(); 100 | if (TokenChecker.isValidJWT(value)) { 101 | return new KeyValuePair(name, value); 102 | } 103 | } 104 | } 105 | } 106 | } 107 | } 108 | return null; 109 | } 110 | 111 | 112 | @Override 113 | public CookieFlagWrapper getcFW() { 114 | return this.cFW; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![build status](https://api.travis-ci.com/ozzi-/JWT4B.svg?branch=master) 2 | ![licence](https://img.shields.io/github/license/mvetsch/JWT4B.svg) 3 | ![open issues](https://img.shields.io/github/issues/mvetsch/JWT4B.svg) 4 | 5 | 6 | # JWT4B 7 | JSON Web Tokens (JWT) support for the Burp Interception Proxy. JWT4B will let you manipulate a JWT on the fly, automate common attacks against JWT and decode it for you in the proxy history. JWT4B automagically detects JWTs in the form of 'Authorization Bearer' headers as well as customizable post body parameters and body content. 8 | 9 | ![Logo](https://i.imgur.com/SnrC5To.png) 10 | 11 | # Screenshots 12 | ![Screenshot - Intercept View](https://i.imgur.com/VsbqoNL.png) 13 | 14 | ![Screenshot - Decode View](https://i.imgur.com/hsBNeE2.png) 15 | 16 | ![Screenshot - Suite Tab View](https://i.imgur.com/2HGOI27.png) 17 | 18 | # Testing 19 | The following url contains links to four pages which simulate a JWT being sent via XHR or as cookie. 20 | [https://oz-web.com/jwt/](https://oz-web.com/jwt/) 21 | 22 | # Configuration 23 | A config file will be created under "%user.home%\.JWT4B\config.json" with the following content: 24 | ``` 25 | { 26 | "resetEditor": true, 27 | "highlightColor": "blue", 28 | "interceptComment": "Contains a JWT", 29 | "tokenKeywords": [ 30 | "id_token", 31 | "ID_TOKEN", 32 | "access_token", 33 | "token" 34 | ], 35 | "cveAttackModePublicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuNCJ/1Tawe8DUIbQDxjRr+bVSoIdcOjJm5wskbMUjHopTWERzLo65yLPjCVcRudQ8DNJIs3yb+hzxi0b8uyKXK6nYTaxdwtRN61NMgI/ecNYw1A3nMLRJ4KetLCUqCehVV+OavJqwGXb0k4OhJu7VefLD9PxOQxLd/MxJLMTChqYYQWY069oNTB9uRaBRLwcEv3i8uiM3HAdx4di0FZLHN5yAt6Zq7TR53CUDSI74q/AH4zeuo+D/UscVTq2bInfJmN3NdA6XqPdjnu6DtT7VQZif+06sFXgnoieuUaeRE0Jn8ZY72hljToFZmsLUPPhTSzmFTgko4+MGnS29w1rbQIDAQAB", 36 | "cveAttackModePrivateKey": "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC40In/VNrB7wNQhtAPGNGv5tVKgh1w6MmbnCyRsxSMeilNYRHMujrnIs+MJVxG51DwM0kizfJv6HPGLRvy7IpcrqdhNrF3C1E3rU0yAj95w1jDUDecwtEngp60sJSoJ6FVX45q8mrAZdvSTg6Em7tV58sP0/E5DEt38zEksxMKGphhBZjTr2g1MH25FoFEvBwS/eLy6IzccB3Hh2LQVksc3nIC3pmrtNHncJQNIjvir8AfjN66j4P9SxxVOrZsid8mY3c10Dpeo92Oe7oO1PtVBmJ/7TqwVeCeiJ65Rp5ETQmfxljvaGWNOgVmawtQ8+FNLOYVOCSjj4wadLb3DWttAgMBAAECggEBALF/J2ngNxEW2loWf/Bf59NGoQakHF56VFZtEakFEvEvykcUuSGkojmmhyqUHyHBu0xWFSGmJfcwizCD1lnir6f/3aVR//LTHbeZa5Bh9FCfOrqqah7WREXr/zyOctdk6F+0HHW+SKRrr0k1yl+1qaABtFaJOR2PH1Qebs5OZjTGXvKtm5H7G4FeNPDjprCKB5vRiWPY5F3sRJOFp8TwkH5qbirgZh0KJiYuJMq9QtzjRHYjzALOSWldpqb8Xzcx7lHZbF8gNv3zeRJRJWTYATq8KVaZ3fs0mv9z37MPRC1AS9v4ylrwXsAviWvn21Q6E1jrxOxZfAhkoA2aLtFMr4kCgYEA68yc8mupFsRCwcfChauAExibU2lCmW1ImcWxGLQR0dVPyaEPlecwKxvdetWs7BPaxqogKppB71gsxXYASUntgwj1f7zXxo4rdSZv20B09eASo+I8qZpfDZWR1oM7HjXR40lWELtQhzD0QDQCUmQtCpVGgyheqPsrQntCeM5LEisCgYEAyKXD93Onevtg6K2GWmnIgCP8+PRvu9kYW+3yhN0BGzmJrVSlD6uw0SAsA7awd54Qs00gGcWoztDm7V+YHDcYy3oOzwip4Yw2S3kUPewupySLm1VrDBMdXVp1sQH/I5DE3B4c5OxgdCmiX+7hLkXBBjpOqbHS+2bsPs9qnO2M5McCgYAhj84G8yvuAaE+05/sRqzECwyQorrH+7YJrQm36mle5G2m1TXSsEU63Yx4n1EtiOXqwOwzJCGeX35/3HvN8qfLrsrCk65ipHmrAv2Ix3PeSzZb/SeFPGOrG07WqXcQpbhqEVYeq4qas20QdlaeQ4PlrbmLkYNnqdhObhzX9QTaYQKBgQDDa9/fpL8cIrWSKV/Ps3PaijKa7sfcd2coMiqgiPfI4lNbhDN3fcsrA2CbBVX+Su8NEzMOptrxA7nGu/JUmL0HgQvnTRLYYE2JWJYEcYJGvGtUkO8/xWY2RCKYkc9Dfn6dvJ57wFV5Dgvdz7V18e47+JIg6NcKkIXL7wxxZ1RwhQKBgQDd4nlMdJue4zA7hO2YGxUqX+ALVY6ikZ/SBOQIDrnI9aixwXYQ3t3Nwjim73/0uiLXLOpO92dBSym7GeSPYqWZhkyQ8C05tDyGvDI5b7bVmD1pxmnhG9sOktrkDVkOsYUnAhRwCgmuExkoeGWPvUt+85cmMpJfHHqbrb5FLqTeXQ==" 37 | } 38 | ``` 39 | Changing the config requires a reload of the extension or BURP. 40 | If you messed something up, just delete the file, it will be created again with the default values. 41 | Note: If resetEditor is set to false, all options such as the re-singing and alg attack won't be reset for every new request. This might be useful when working in the repeater. 42 | 43 | ## Building your own version (with Eclipse) 44 | 1. Clone repository and create new Eclipse Java Project 45 | 2. Rightclick -> Configure -> Convert to Gradle Project (downloading all required libraries) 46 | 3. Open Burp -> Extensions -> APIs -> Save interface files -> Copy all files to JWT4B\src\burp 47 | 4. Gradle -> build jar 48 | 5. Load the JAR in Burp through the Extender Tab -> Extensions -> Add (Good to know: CTRL+Click on a extension to reload it) 49 | 50 | # Installation from BApp Store 51 | This extension is available in the [BApp Store](https://portswigger.net/bappstore/f923cbf91698420890354c1d8958fee6). 52 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeHttpMessage.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Scanner; 6 | import java.util.regex.Pattern; 7 | 8 | import burp.api.montoya.http.message.HttpHeader; 9 | import burp.api.montoya.http.message.HttpMessage; 10 | 11 | public class FakeHttpMessage implements HttpMessage { 12 | 13 | String rawContent; 14 | 15 | String header; 16 | String body; 17 | 18 | List headerList = new ArrayList<>(); 19 | 20 | public FakeHttpMessage(String message) { 21 | super(); 22 | rawContent = message; 23 | 24 | splitHeaderAndBody(); 25 | processHeader(); 26 | } 27 | 28 | private void processHeader() { 29 | List lines = List.of(header.split("\r\n")); 30 | for (String line : lines) { 31 | // body reached 32 | if (line.isBlank()) { 33 | return; 34 | } 35 | 36 | int colon = line.indexOf(':'); 37 | 38 | if (colon > -1) { 39 | headerList.add(HttpHeader.httpHeader(line.substring(0, colon), line.substring(colon + 1).trim())); 40 | } 41 | } 42 | } 43 | 44 | public String toString() { 45 | return rawContent; 46 | } 47 | 48 | private void splitHeaderAndBody() { 49 | Scanner scanner = new Scanner(this.rawContent); 50 | String delimiter = "\r\n"; 51 | scanner.useDelimiter(delimiter); 52 | StringBuilder sb=new StringBuilder(); 53 | List result=new ArrayList<>(); 54 | while (scanner.hasNextLine()){ 55 | String line = scanner.nextLine(); 56 | if(!(line.trim().isEmpty())){ 57 | sb.append(line).append(delimiter); 58 | }else if(!sb.toString().isEmpty()) { 59 | result.add(sb.toString()); 60 | sb.setLength(0); 61 | } 62 | } 63 | if(!sb.toString().isEmpty()) { 64 | result.add(sb.toString()); 65 | } 66 | 67 | header = result.get(0); 68 | 69 | if (result.size() > 1) { 70 | body = result.get(1).trim(); // TODO 71 | } else { 72 | body = ""; 73 | } 74 | } 75 | 76 | @Override 77 | public boolean hasHeader(HttpHeader header) { 78 | return headerList.stream() 79 | .anyMatch(o -> o.equals(header)); 80 | } 81 | 82 | @Override 83 | public boolean hasHeader(String name) { 84 | return headerList.stream() 85 | .anyMatch(o -> o.name().equals(name)); 86 | } 87 | 88 | @Override 89 | public boolean hasHeader(String name, String value) { 90 | return headerList.stream() 91 | .anyMatch(o -> o.name().equals(name) && o.value().equals(value)); 92 | } 93 | 94 | @Override 95 | public HttpHeader header(String name) { 96 | return headerList.stream() 97 | .filter(o -> o.name().equals(name)) 98 | .findFirst() 99 | .orElse(null); 100 | } 101 | 102 | @Override 103 | public String headerValue(String name) { 104 | return headerList.stream() 105 | .filter(o -> o.name().equals(name)) 106 | .map(HttpHeader::value) 107 | .findFirst() 108 | .orElse(null); 109 | } 110 | 111 | @Override 112 | public List headers() { 113 | return headerList; 114 | } 115 | 116 | @Override 117 | public String httpVersion() { 118 | System.err.println("Not implemented"); 119 | return ""; 120 | } 121 | 122 | @Override 123 | public int bodyOffset() { 124 | System.err.println("Not implemented"); 125 | return 0; 126 | } 127 | 128 | @Override 129 | public ByteArray body() { 130 | System.err.println("Not implemented"); 131 | return null; 132 | } 133 | 134 | @Override 135 | public String bodyToString() { 136 | return body; 137 | } 138 | 139 | @Override 140 | public List markers() { 141 | System.err.println("Not implemented"); 142 | return List.of(); 143 | } 144 | 145 | @Override 146 | public boolean contains(String searchTerm, boolean caseSensitive) { 147 | System.err.println("Not implemented"); 148 | return false; 149 | } 150 | 151 | @Override 152 | public boolean contains(Pattern pattern) { 153 | System.err.println("Not implemented"); 154 | return false; 155 | } 156 | 157 | @Override 158 | public ByteArray toByteArray() { 159 | System.err.println("Not implemented"); 160 | return null; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/SecretFinder.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import static org.apache.commons.lang.StringUtils.isNotEmpty; 4 | 5 | import java.net.URI; 6 | import java.net.URL; 7 | import java.security.KeyFactory; 8 | import java.security.spec.X509EncodedKeySpec; 9 | import java.util.ArrayList; 10 | import java.util.Iterator; 11 | import java.util.LinkedHashSet; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.Optional; 16 | import java.util.Set; 17 | import java.util.regex.Matcher; 18 | import java.util.regex.Pattern; 19 | import java.util.stream.Collectors; 20 | 21 | import com.auth0.jwt.JWT; 22 | import com.auth0.jwt.JWTVerifier; 23 | import com.fasterxml.jackson.databind.JsonNode; 24 | import com.fasterxml.jackson.databind.ObjectMapper; 25 | 26 | import app.algorithm.AlgorithmWrapper; 27 | import app.tokenposition.ITokenPosition; 28 | import burp.api.montoya.http.handler.HttpRequestToBeSent; 29 | import lombok.Getter; 30 | import model.CustomJWToken; 31 | 32 | public class SecretFinder { 33 | private static final String RE_TOP = "[\\w-]+\\.(com.cn|net.cn|gov.cn|org\\.nz|org.cn|com|net|org|gov|cc|biz|info|cn|co)\\b"; 34 | private static final Pattern DOMAIN_PATTERN = Pattern.compile(RE_TOP, Pattern.CASE_INSENSITIVE); 35 | 36 | private final String jwt; 37 | private final String jwtPayload; 38 | private final String algorithm; 39 | 40 | @Getter 41 | private final List secrets; 42 | private final HttpRequestToBeSent httpRequestToBeSent; 43 | 44 | public SecretFinder(ITokenPosition tokenPosition, HttpRequestToBeSent requestToBeSent) { 45 | CustomJWToken cjwt = new CustomJWToken(Objects.requireNonNull(tokenPosition).getToken()); 46 | this.jwt = Objects.requireNonNull(tokenPosition).getToken(); 47 | this.jwtPayload = cjwt.getPayloadJson(); 48 | this.algorithm = cjwt.getAlgorithm(); 49 | this.httpRequestToBeSent = requestToBeSent; 50 | this.secrets = collectSecrets(); 51 | } 52 | 53 | public List collectSecrets() { 54 | Set secretSet = new LinkedHashSet<>(); 55 | 56 | String host = getHost(); 57 | secretSet.add(host); 58 | 59 | String domainName = getDomainName(host); 60 | secretSet.add(domainName); 61 | 62 | String domain = getDomain(host); 63 | secretSet.add(domain); 64 | 65 | List values = getAllValues(this.jwtPayload); 66 | secretSet.addAll(values); 67 | 68 | ArrayList upperSecrets = secretSet.stream().map(String::toUpperCase).collect(Collectors.toCollection(ArrayList::new)); 69 | secretSet.addAll(upperSecrets); 70 | 71 | return new ArrayList<>(secretSet); 72 | 73 | } 74 | 75 | public String getDomainName(String host) { 76 | try { 77 | Matcher matcher = DOMAIN_PATTERN.matcher(host); 78 | if (matcher.find()) { 79 | return matcher.group(); 80 | } 81 | } catch (IllegalStateException | IndexOutOfBoundsException e) { 82 | Output.outputError("Failed to extract domain: " + e.getMessage()); 83 | } 84 | return ""; 85 | } 86 | 87 | public String getDomain(String host) { 88 | return Optional.ofNullable(getDomainName(host)).map(domainName -> domainName.split("\\.")[0]).orElse(""); 89 | } 90 | 91 | public String getHost() { 92 | String urlString = this.httpRequestToBeSent.url(); 93 | String host = ""; 94 | try { 95 | URI uri = new URI(urlString); 96 | URL url = uri.toURL(); 97 | host = url.getHost(); 98 | } catch (Exception e) { 99 | Output.outputError("URL Parse Error: " + e.getMessage()); 100 | } 101 | 102 | return host; 103 | } 104 | 105 | public static List getAllValues(String jsonString) { 106 | ArrayList values = new ArrayList<>(); 107 | try { 108 | ObjectMapper objectMapper = new ObjectMapper(); 109 | JsonNode rootNode = objectMapper.readTree(jsonString); 110 | extractValues(rootNode, values); 111 | } catch (Exception e) { 112 | Output.outputError(e.getMessage()); 113 | } 114 | return values; 115 | } 116 | 117 | private static void extractValues(JsonNode node, ArrayList values) { 118 | if (node.isObject()) { 119 | Iterator> fields = node.fields(); 120 | while (fields.hasNext()) { 121 | extractValues(fields.next().getValue(), values); 122 | } 123 | } else if (node.isArray()) { 124 | for (JsonNode arrayElement : node) { 125 | extractValues(arrayElement, values); 126 | } 127 | } else { 128 | values.add(node.asText()); 129 | } 130 | } 131 | 132 | public Boolean checkSecret(String secret) { 133 | try { 134 | if(!checkIfIsX509Key(secret,this.algorithm)) { 135 | return false; 136 | } 137 | JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(this.algorithm, secret)).build(); 138 | verifier.verify(this.jwt); 139 | return true; 140 | } catch (Exception ignored) { 141 | return false; 142 | } 143 | } 144 | 145 | 146 | private static boolean checkIfIsX509Key(String key, String algorithm) { 147 | if (isNotEmpty(key)) { 148 | byte[] keyByteArray = java.util.Base64.getDecoder().decode(key); 149 | try { 150 | KeyFactory.getInstance(algorithm); 151 | new X509EncodedKeySpec(keyByteArray); 152 | return true; 153 | } catch (Exception e) { 154 | return false; 155 | } 156 | } 157 | return false; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/KeyHelper.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import static org.apache.commons.lang.StringUtils.isNotEmpty; 4 | 5 | import java.security.Key; 6 | import java.security.KeyFactory; 7 | import java.security.KeyPair; 8 | import java.security.KeyPairGenerator; 9 | import java.security.NoSuchAlgorithmException; 10 | import java.security.PrivateKey; 11 | import java.security.PublicKey; 12 | import java.security.interfaces.RSAPublicKey; 13 | import java.security.spec.EncodedKeySpec; 14 | import java.security.spec.PKCS8EncodedKeySpec; 15 | import java.security.spec.X509EncodedKeySpec; 16 | 17 | import javax.crypto.Mac; 18 | import javax.crypto.spec.SecretKeySpec; 19 | 20 | import org.apache.commons.codec.binary.Base64; 21 | import org.apache.commons.lang.RandomStringUtils; 22 | 23 | import app.algorithm.AlgorithmType; 24 | import app.algorithm.AlgorithmWrapper; 25 | 26 | public class KeyHelper { 27 | 28 | KeyHelper() { 29 | 30 | } 31 | 32 | private static final String[] KEY_BEGIN_MARKERS = new String[] { "-----BEGIN PUBLIC KEY-----", "-----BEGIN CERTIFICATE-----" }; 33 | private static final String[] KEY_END_MARKERS = new String[] { "-----END PUBLIC KEY-----", "-----END CERTIFICATE-----" }; 34 | public static final String HMAC_SHA_256 = "HmacSHA256"; 35 | 36 | public static String getRandomKey(String algorithm) { 37 | AlgorithmType algorithmType = AlgorithmWrapper.getTypeOf(algorithm); 38 | 39 | if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { 40 | return RandomStringUtils.randomAlphanumeric(6); 41 | } 42 | if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("RS")) { 43 | try { 44 | KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); 45 | 46 | PublicKeyBroker.publicKey = Base64.encodeBase64String(keyPair.getPublic().getEncoded()); 47 | return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); 48 | } catch (NoSuchAlgorithmException e) { 49 | Output.outputError(e.getMessage()); 50 | } 51 | } 52 | if (algorithmType.equals(AlgorithmType.ASYMMETRIC) && algorithm.startsWith("ES")) { 53 | try { 54 | KeyPair keyPair = KeyPairGenerator.getInstance("EC").generateKeyPair(); 55 | return Base64.encodeBase64String(keyPair.getPrivate().getEncoded()); 56 | } catch (NoSuchAlgorithmException e) { 57 | Output.outputError(e.getMessage()); 58 | } 59 | } 60 | throw new IllegalArgumentException("Cannot get random key of provided algorithm as it does not seem valid HS, RS or ES"); 61 | } 62 | 63 | public static PrivateKey generatePrivateKeyFromString(String key, String algorithm) { 64 | PrivateKey privateKey = null; 65 | if (isNotEmpty(key)) { 66 | key = cleanKey(key); 67 | try { 68 | byte[] keyByteArray = Base64.decodeBase64(key); 69 | KeyFactory kf = KeyFactory.getInstance(algorithm); 70 | EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyByteArray); 71 | privateKey = kf.generatePrivate(keySpec); 72 | } catch (Exception e) { 73 | Output.outputError("Error generating private key with input string '" + key + "' and algorithm '" + algorithm + "' - " + e.getMessage() + " - "); 74 | } 75 | } 76 | return privateKey; 77 | } 78 | 79 | public static String cleanKey(String key) { 80 | for (String keyBeginMarker : KEY_BEGIN_MARKERS) { 81 | key = key.replace(keyBeginMarker, ""); 82 | } 83 | for (String keyEndMarker : KEY_END_MARKERS) { 84 | key = key.replace(keyEndMarker, ""); 85 | } 86 | key = key.replaceAll("\\s+", "").replaceAll("\\r+", "").replaceAll("\\n+", ""); 87 | 88 | return key; 89 | } 90 | 91 | public static RSAPublicKey loadCVEAttackPublicKey() { 92 | String publicPEM = KeyHelper.cleanKey(Config.cveAttackModePublicKey); 93 | KeyFactory kf; 94 | try { 95 | kf = KeyFactory.getInstance("RSA"); 96 | X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(java.util.Base64.getDecoder().decode(publicPEM)); 97 | return (RSAPublicKey) kf.generatePublic(keySpecX509); 98 | } catch (Exception e) { 99 | Output.outputError("Could not load public key - " + e.getMessage()); 100 | e.printStackTrace(); 101 | } 102 | return null; 103 | } 104 | 105 | private static PublicKey generatePublicKeyFromString(String key, String algorithm) { 106 | PublicKey publicKey = null; 107 | if (isNotEmpty(key)) { 108 | key = cleanKey(key); 109 | byte[] keyByteArray = java.util.Base64.getDecoder().decode(key); 110 | try { 111 | KeyFactory kf = KeyFactory.getInstance(algorithm); 112 | EncodedKeySpec keySpec = new X509EncodedKeySpec(keyByteArray); 113 | publicKey = kf.generatePublic(keySpec); 114 | } catch (Exception e) { 115 | Output.outputError(e.getMessage()); 116 | } 117 | } 118 | return publicKey; 119 | } 120 | 121 | public static Key getKeyInstance(String key, String algorithm, boolean isPrivate) { 122 | return isPrivate ? generatePrivateKeyFromString(key, algorithm) : generatePublicKeyFromString(key, algorithm); 123 | } 124 | 125 | public static byte[] calcHmacSha256(byte[] secretKey, byte[] message) { 126 | byte[] hmacSha256 = null; 127 | try { 128 | Mac mac = Mac.getInstance(HMAC_SHA_256); 129 | SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, HMAC_SHA_256); 130 | mac.init(secretKeySpec); 131 | hmacSha256 = mac.doFinal(message); 132 | } catch (Exception e) { 133 | Output.outputError("Exception during " + HMAC_SHA_256 + ": " + e.getMessage()); 134 | } 135 | return hmacSha256; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/test/java/app/TestBodyDetection.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.tokenposition.Body; 4 | import burp.api.montoya.MontoyaExtension; 5 | import burp.api.montoya.http.message.requests.HttpRequest; 6 | import org.apache.commons.text.StringSubstitutor; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.junit.jupiter.params.ParameterizedTest; 10 | import org.junit.jupiter.params.provider.Arguments; 11 | import org.junit.jupiter.params.provider.MethodSource; 12 | 13 | import java.util.Map; 14 | import java.util.stream.Stream; 15 | 16 | import static app.TestConstants.*; 17 | import static burp.api.montoya.http.message.requests.HttpRequest.httpRequest; 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | import static org.junit.jupiter.params.provider.Arguments.arguments; 20 | 21 | @ExtendWith(MontoyaExtension.class) 22 | class TestBodyDetection { 23 | 24 | static Stream bodyDataAndDetectedTokens() { 25 | return Stream.of( 26 | arguments("test=best&abc=" + HS256_TOKEN, HS256_TOKEN), 27 | arguments("a " + HS256_TOKEN + " b", HS256_TOKEN), 28 | arguments("{\"aaaa\":\"" + HS256_TOKEN + "\"}", HS256_TOKEN), 29 | arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":\"" + HS256_TOKEN + " \"}}}", HS256_TOKEN), 30 | arguments("token=" + HS256_TOKEN, HS256_TOKEN), 31 | arguments(HS256_TOKEN, HS256_TOKEN), 32 | arguments("egg=" + HS256_TOKEN + "&test=best", HS256_TOKEN), 33 | arguments("abc def " + HS256_TOKEN + " ghi jkl", HS256_TOKEN) 34 | ); 35 | } 36 | 37 | @MethodSource("bodyDataAndDetectedTokens") 38 | @ParameterizedTest 39 | void testBodyDetection(String body, String bodyToken) { 40 | Map params = Map.of( 41 | "METHOD", METHOD_POST, 42 | "BODY", body); 43 | 44 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 45 | 46 | Body pb = new Body(httpRequest, true); 47 | 48 | assertThat(pb.positionFound()).isTrue(); 49 | assertThat(pb.getToken()).isEqualTo(bodyToken); 50 | } 51 | 52 | static Stream bodyDataWhereNoTokenDetected() { 53 | return Stream.of( 54 | arguments("{ \"bbbb\" : { \" cccc \": { \" dddd\":" + HS256_TOKEN + " \"}}}"), 55 | arguments("{}"), 56 | arguments("{ \"abc\": \"def\"}"), 57 | arguments("{ \"abc\": {\"def\" : \"ghi\"} }"), 58 | arguments("yo=" + INVALID_HEADER_TOKEN + "&test=best"), 59 | arguments("ab " + INVALID_HEADER_TOKEN + " de"), 60 | arguments(HS256_TOKEN + "&"), 61 | arguments("abc def ghi jkl"), 62 | arguments("") 63 | ); 64 | } 65 | 66 | @MethodSource("bodyDataWhereNoTokenDetected") 67 | @ParameterizedTest 68 | void testBodyDetection(String body) { 69 | Map params = Map.of( 70 | "METHOD", METHOD_POST, 71 | "BODY", body); 72 | 73 | HttpRequest httpRequest = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params)); 74 | 75 | Body pb = new Body(httpRequest, true); 76 | 77 | assertThat(pb.positionFound()).isFalse(); 78 | } 79 | 80 | 81 | @Test 82 | void testPostBodyReplaceWithParam() { 83 | String body1 = "test=best&token=" + HS256_TOKEN; 84 | 85 | Map params1 = Map.of( 86 | "METHOD", METHOD_POST, 87 | "BODY", body1); 88 | 89 | HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); 90 | 91 | Body pb1 = new Body(httpRequest1, true); 92 | pb1.positionFound(); 93 | pb1.replaceToken(HS256_TOKEN_2); 94 | 95 | // 96 | String body2 = "test=best&token=" + HS256_TOKEN_2; 97 | 98 | Map params2 = Map.of( 99 | "METHOD", METHOD_POST, 100 | "BODY", body2); 101 | 102 | HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); 103 | 104 | Body pb2 = new Body(httpRequest2, true); 105 | pb2.positionFound(); 106 | 107 | // 108 | assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); 109 | } 110 | 111 | 112 | @Test 113 | void testPostBodyReplaceWithoutParam() { 114 | String body1 = "ab\n cd " + HS256_TOKEN + " cd"; 115 | 116 | Map params1 = Map.of( 117 | "METHOD", METHOD_POST, 118 | "BODY", body1); 119 | 120 | HttpRequest httpRequest1 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params1)); 121 | 122 | Body pb1 = new Body(httpRequest1, true); 123 | pb1.positionFound(); 124 | pb1.replaceToken(HS256_TOKEN_2); 125 | 126 | // 127 | String body2 = "ab\n cd " + HS256_TOKEN_2 + " cd"; 128 | 129 | Map params2 = Map.of( 130 | "METHOD", METHOD_POST, 131 | "BODY", body2); 132 | 133 | HttpRequest httpRequest2 = httpRequest(StringSubstitutor.replace(REQUEST_TEMPLATE, params2)); 134 | 135 | Body pb2 = new Body(httpRequest2, true); 136 | pb2.positionFound(); 137 | 138 | // 139 | assertThat(pb1.getRequest().toString()).isEqualTo(pb2.getRequest().toString()); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/test/java/app/TestSetCookieDetection.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | import app.tokenposition.Cookie; 4 | import burp.api.montoya.MontoyaExtension; 5 | import burp.api.montoya.http.message.responses.HttpResponse; 6 | import org.apache.commons.text.StringSubstitutor; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | 10 | import java.util.Map; 11 | 12 | import static app.TestConstants.*; 13 | import static burp.api.montoya.http.message.responses.HttpResponse.httpResponse; 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | @ExtendWith(MontoyaExtension.class) 17 | class TestSetCookieDetection { 18 | 19 | @Test 20 | void testCookieReversedOrder() { 21 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "\r\n" + "Set-Cookie: test=best"; 22 | 23 | Map params = Map.of("ADD_HEADER", cookieHeader); 24 | 25 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 26 | 27 | Cookie cookie = new Cookie(httpResponse, false); 28 | 29 | assertThat(cookie.positionFound()).isTrue(); 30 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 31 | } 32 | 33 | @Test 34 | void testCookieInvalidJWT() { 35 | String cookieHeader = "Set-Cookie: token=" + INVALID_HEADER_TOKEN + "\r\n" + "Set-Cookie: test=best"; 36 | 37 | Map params = Map.of("ADD_HEADER", cookieHeader); 38 | 39 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 40 | 41 | Cookie cookie = new Cookie(httpResponse, false); 42 | 43 | assertThat(cookie.positionFound()).isFalse(); 44 | assertThat(cookie.getToken()).isEmpty(); 45 | } 46 | 47 | @Test 48 | void testSetCookieSemicolon() { 49 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + ";"; 50 | 51 | Map params = Map.of("ADD_HEADER", cookieHeader); 52 | 53 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 54 | 55 | Cookie cookie = new Cookie(httpResponse, false); 56 | 57 | assertThat(cookie.positionFound()).isTrue(); 58 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 59 | } 60 | 61 | @Test 62 | void testSetCookieReplace() { 63 | String cookieHeader1 = "Set-Cookie: token=" + HS256_TOKEN; 64 | 65 | Map params1 = Map.of("ADD_HEADER", cookieHeader1); 66 | 67 | HttpResponse httpResponse1 = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params1)); 68 | 69 | Cookie cookie1 = new Cookie(httpResponse1, false); 70 | 71 | assertThat(cookie1.positionFound()).isTrue(); 72 | assertThat(cookie1.getToken()).isEqualTo(HS256_TOKEN); 73 | 74 | cookie1.replaceToken(HS256_TOKEN_2); 75 | 76 | // 77 | String cookieHeader2 = "Set-Cookie: token=" + HS256_TOKEN_2; 78 | 79 | Map params2 = Map.of("ADD_HEADER", cookieHeader2); 80 | 81 | HttpResponse httpResponse2 = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params2)); 82 | 83 | Cookie cookie2 = new Cookie(httpResponse2, false); 84 | 85 | assertThat(cookie2.positionFound()).isTrue(); 86 | assertThat(cookie2.getToken()).isEqualTo(HS256_TOKEN_2); 87 | 88 | // 89 | assertThat(cookie1.getResponse().toString()).isEqualTo(cookie2.getResponse().toString()); 90 | } 91 | 92 | @Test 93 | void testSetCookieSecureFlag() { 94 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; Secure;"; 95 | 96 | Map params = Map.of("ADD_HEADER", cookieHeader); 97 | 98 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 99 | 100 | Cookie cookie = new Cookie(httpResponse, false); 101 | 102 | assertThat(cookie.positionFound()).isTrue(); 103 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 104 | 105 | assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); 106 | } 107 | 108 | @Test 109 | void testSetCookieHTTPOnlyFlag() { 110 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; path=/;"; 111 | 112 | Map params = Map.of("ADD_HEADER", cookieHeader); 113 | 114 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 115 | 116 | Cookie cookie = new Cookie(httpResponse, false); 117 | 118 | assertThat(cookie.positionFound()).isTrue(); 119 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 120 | assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); 121 | } 122 | 123 | @Test 124 | void testSetCookieBothFlags() { 125 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; HttpOnly; Max-Age=0; secure; path=/;"; 126 | 127 | Map params = Map.of("ADD_HEADER", cookieHeader); 128 | 129 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 130 | 131 | Cookie cookie = new Cookie(httpResponse, false); 132 | 133 | assertThat(cookie.positionFound()).isTrue(); 134 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 135 | assertThat(cookie.getcFW().hasHttpOnlyFlag()).isTrue(); 136 | assertThat(cookie.getcFW().hasSecureFlag()).isTrue(); 137 | } 138 | 139 | @Test 140 | void testSetCookieNoFlags() { 141 | String cookieHeader = "Set-Cookie: token=" + HS256_TOKEN + "; expires=Thu, 01-Jan-1970 01:40:00 GMT; Max-Age=0; path=/;"; 142 | 143 | Map params = Map.of("ADD_HEADER", cookieHeader); 144 | 145 | HttpResponse httpResponse = httpResponse(StringSubstitutor.replace(RESPONSE_TEMPLATE, params)); 146 | 147 | Cookie cookie = new Cookie(httpResponse, false); 148 | 149 | assertThat(cookie.positionFound()).isTrue(); 150 | assertThat(cookie.getToken()).isEqualTo(HS256_TOKEN); 151 | assertThat(cookie.getcFW().hasHttpOnlyFlag()).isFalse(); 152 | assertThat(cookie.getcFW().hasSecureFlag()).isFalse(); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeByteArray.java: -------------------------------------------------------------------------------- 1 | // copied from https://github.com/Hannah-PortSwigger/WebSocketTurboIntruder/blob/main/src/test/java/burp/api/montoya/core/FakeByteArray.java 2 | 3 | package burp.api.montoya.core; 4 | 5 | import java.util.Iterator; 6 | import java.util.regex.Pattern; 7 | 8 | import static java.nio.charset.StandardCharsets.UTF_8; 9 | 10 | public class FakeByteArray implements ByteArray { 11 | private final byte[] data; 12 | 13 | public FakeByteArray(String data) { 14 | this(data.getBytes(UTF_8)); 15 | } 16 | 17 | public FakeByteArray(byte[] data) { 18 | this.data = data; 19 | } 20 | 21 | @Override 22 | public byte getByte(int index) { 23 | return data[index]; 24 | } 25 | 26 | @Override 27 | public void setByte(int index, byte value) { 28 | data[index] = value; 29 | } 30 | 31 | @Override 32 | public void setByte(int index, int value) { 33 | setByte(index, (byte) (0xFF & value)); 34 | } 35 | 36 | @Override 37 | public void setBytes(int index, byte... data) { 38 | throw new UnsupportedOperationException(); 39 | } 40 | 41 | @Override 42 | public void setBytes(int index, int... data) { 43 | throw new UnsupportedOperationException(); 44 | } 45 | 46 | @Override 47 | public void setBytes(int index, ByteArray byteArray) { 48 | throw new UnsupportedOperationException(); 49 | } 50 | 51 | @Override 52 | public int length() { 53 | return data.length; 54 | } 55 | 56 | @Override 57 | public byte[] getBytes() { 58 | return data; 59 | } 60 | 61 | @Override 62 | public ByteArray subArray(int startIndexInclusive, int endIndexExclusive) { 63 | if (startIndexInclusive < 0 || endIndexExclusive <= startIndexInclusive || endIndexExclusive > data.length) { 64 | throw new IllegalArgumentException("Invalid indices (%d, %d) for array of length %d".formatted(startIndexInclusive, endIndexExclusive, data.length)); 65 | } 66 | 67 | int length = endIndexExclusive - startIndexInclusive; 68 | byte[] subArray = new byte[length]; 69 | System.arraycopy(data, startIndexInclusive, subArray, 0, length); 70 | 71 | return new FakeByteArray(subArray); 72 | } 73 | 74 | @Override 75 | public ByteArray subArray(Range range) { 76 | throw new UnsupportedOperationException(); 77 | } 78 | 79 | @Override 80 | public ByteArray copy() { 81 | throw new UnsupportedOperationException(); 82 | } 83 | 84 | @Override 85 | public ByteArray copyToTempFile() { 86 | throw new UnsupportedOperationException(); 87 | } 88 | 89 | @Override 90 | public int indexOf(ByteArray searchTerm) { 91 | throw new UnsupportedOperationException(); 92 | } 93 | 94 | @Override 95 | public int indexOf(String searchTerm) { 96 | throw new UnsupportedOperationException(); 97 | } 98 | 99 | @Override 100 | public int indexOf(ByteArray searchTerm, boolean caseSensitive) { 101 | throw new UnsupportedOperationException(); 102 | } 103 | 104 | @Override 105 | public int indexOf(String searchTerm, boolean caseSensitive) { 106 | throw new UnsupportedOperationException(); 107 | } 108 | 109 | @Override 110 | public int indexOf(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { 111 | throw new UnsupportedOperationException(); 112 | } 113 | 114 | @Override 115 | public int indexOf(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { 116 | throw new UnsupportedOperationException(); 117 | } 118 | 119 | @Override 120 | public int indexOf(Pattern pattern) { 121 | throw new UnsupportedOperationException(); 122 | } 123 | 124 | @Override 125 | public int indexOf(Pattern pattern, int i, int i1) { 126 | throw new UnsupportedOperationException(); 127 | } 128 | 129 | @Override 130 | public int countMatches(ByteArray searchTerm) { 131 | throw new UnsupportedOperationException(); 132 | } 133 | 134 | @Override 135 | public int countMatches(String searchTerm) { 136 | throw new UnsupportedOperationException(); 137 | } 138 | 139 | @Override 140 | public int countMatches(ByteArray searchTerm, boolean caseSensitive) { 141 | throw new UnsupportedOperationException(); 142 | } 143 | 144 | @Override 145 | public int countMatches(String searchTerm, boolean caseSensitive) { 146 | throw new UnsupportedOperationException(); 147 | } 148 | 149 | @Override 150 | public int countMatches(ByteArray searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { 151 | throw new UnsupportedOperationException(); 152 | } 153 | 154 | @Override 155 | public int countMatches(String searchTerm, boolean caseSensitive, int startIndexInclusive, int endIndexExclusive) { 156 | throw new UnsupportedOperationException(); 157 | } 158 | 159 | @Override 160 | public int countMatches(Pattern pattern) { 161 | throw new UnsupportedOperationException(); 162 | } 163 | 164 | @Override 165 | public int countMatches(Pattern pattern, int i, int i1) { 166 | throw new UnsupportedOperationException(); 167 | } 168 | 169 | @Override 170 | public ByteArray withAppended(byte... data) { 171 | throw new UnsupportedOperationException(); 172 | } 173 | 174 | @Override 175 | public ByteArray withAppended(int... data) { 176 | throw new UnsupportedOperationException(); 177 | } 178 | 179 | @Override 180 | public ByteArray withAppended(String text) { 181 | return withAppended(new FakeByteArray(text)); 182 | } 183 | 184 | @Override 185 | public ByteArray withAppended(ByteArray byteArray) { 186 | byte[] temp = new byte[data.length + byteArray.length()]; 187 | byte[] bytesToAppend = byteArray.getBytes(); 188 | 189 | System.arraycopy(data, 0, temp, 0, data.length); 190 | System.arraycopy(bytesToAppend, 0, temp, data.length, bytesToAppend.length); 191 | 192 | return new FakeByteArray(temp); 193 | } 194 | 195 | @Override 196 | public Iterator iterator() { 197 | throw new UnsupportedOperationException(); 198 | } 199 | 200 | @Override 201 | public String toString() { 202 | return new String(data, UTF_8); 203 | } 204 | } -------------------------------------------------------------------------------- /src/main/java/app/controllers/JWTSuiteTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import java.awt.Component; 4 | import java.util.List; 5 | 6 | import javax.swing.JTabbedPane; 7 | import javax.swing.event.DocumentEvent; 8 | import javax.swing.event.DocumentListener; 9 | 10 | import app.algorithm.AlgorithmWrapper; 11 | import com.auth0.jwt.JWT; 12 | import com.auth0.jwt.JWTVerifier; 13 | import com.auth0.jwt.exceptions.InvalidClaimException; 14 | import com.auth0.jwt.exceptions.JWTVerificationException; 15 | import com.auth0.jwt.exceptions.SignatureVerificationException; 16 | import com.auth0.jwt.interfaces.DecodedJWT; 17 | 18 | import app.helpers.Output; 19 | import gui.JWTSuiteTab; 20 | import model.CustomJWToken; 21 | import model.JWTSuiteTabModel; 22 | import model.Settings; 23 | import model.Strings; 24 | import model.TimeClaim; 25 | 26 | // used to provide the standalone suite tab after "User Options" 27 | public class JWTSuiteTabController { 28 | 29 | private final JWTSuiteTabModel jwtSTM; 30 | private final JWTSuiteTab jwtST; 31 | 32 | public JWTSuiteTabController(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { 33 | this.jwtSTM = jwtSTM; 34 | this.jwtST = jwtST; 35 | 36 | createAndRegisterActionListeners(jwtSTM, jwtST); 37 | } 38 | 39 | // This method was copied from 40 | // https://support.portswigger.net/customer/portal/questions/16743551-burp-extension-get-focus-on-tab-after-custom-menu-action 41 | public void selectJWTSuiteTab() { 42 | Component current = jwtST; 43 | do { 44 | current = current.getParent(); 45 | } while (!(current instanceof JTabbedPane)); 46 | 47 | JTabbedPane tabPane = (JTabbedPane) current; 48 | for (int i = 0; i < tabPane.getTabCount(); i++) { 49 | if (tabPane.getTitleAt(i).equals(Settings.TAB_NAME)) 50 | tabPane.setSelectedIndex(i); 51 | } 52 | } 53 | 54 | public void contextActionSendJWTtoSuiteTab(String jwts, boolean fromContextMenu) { 55 | jwts = jwts.replace("Authorization:", ""); 56 | jwts = jwts.replace("Bearer", ""); 57 | jwts = jwts.replace("Set-Cookie: ", ""); 58 | jwts = jwts.replace("Cookie: ", ""); 59 | jwts = jwts.replaceAll("\\s", ""); 60 | jwtSTM.setJwtInput(jwts); 61 | try { 62 | CustomJWToken jwt = new CustomJWToken(jwts,true); 63 | List tcl = jwt.getTimeClaimList(); 64 | jwtSTM.setTimeClaims(tcl); 65 | jwtSTM.setJwtJSON(ReadableTokenFormat.getReadableFormat(jwt)); 66 | } catch (Exception e) { 67 | Output.outputError("JWT Context Action: " + e.getMessage()); 68 | } 69 | if (fromContextMenu) { 70 | // Reset View and Select 71 | jwtSTM.setJwtKey(""); 72 | selectJWTSuiteTab(); 73 | } else { 74 | // Since we changed the JWT, we need to check the Key/Signature too 75 | contextActionKey(jwtSTM.getJwtKey()); 76 | } 77 | jwtST.updateSetView(); 78 | } 79 | 80 | public void contextActionKey(String key) { 81 | jwtSTM.setJwtKey(key); 82 | jwtSTM.setVerificationResult(""); 83 | try { 84 | CustomJWToken token = new CustomJWToken(jwtSTM.getJwtInput(),true); 85 | String curAlgo = token.getAlgorithm(); 86 | JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); 87 | DecodedJWT test = verifier.verify(token.getToken()); 88 | jwtSTM.setJwtSignatureColor(Settings.getValidColor()); 89 | jwtSTM.setVerificationLabel(Strings.VALID_VERFICIATION); 90 | test.getAlgorithm(); 91 | } catch (JWTVerificationException e) { 92 | Output.output("Verification failed (" + e.getMessage() + ")"); 93 | jwtSTM.setVerificationResult(e.getMessage()); 94 | 95 | if (e instanceof SignatureVerificationException) { 96 | jwtSTM.setJwtSignatureColor(Settings.getInvalidColor()); 97 | jwtSTM.setVerificationLabel(Strings.INVALID_SIGNATURE_VERIFICATION); 98 | } else if (e instanceof InvalidClaimException) { 99 | jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); 100 | jwtSTM.setVerificationLabel(Strings.INVALID_CLAIM_VERIFICATION); 101 | } else { 102 | jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); 103 | jwtSTM.setVerificationLabel(Strings.GENERIC_ERROR_VERIFICATION); 104 | } 105 | 106 | } catch (IllegalArgumentException e) { 107 | Output.output("Verification failed (" + e.getMessage() + ")"); 108 | jwtSTM.setJwtSignatureColor(Settings.getProblemColor()); 109 | jwtSTM.setVerificationResult(e.getMessage()); 110 | jwtSTM.setVerificationLabel(Strings.INVALID_KEY_VERIFICATION); 111 | } 112 | jwtST.updateSetView(); 113 | } 114 | 115 | private void createAndRegisterActionListeners(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { 116 | DocumentListener jwtDocInputListener = new DocumentListener() { 117 | 118 | @Override 119 | public void removeUpdate(DocumentEvent e) { 120 | propagateUpdate(jwtSTM, jwtST); 121 | } 122 | 123 | @Override 124 | public void insertUpdate(DocumentEvent e) { 125 | propagateUpdate(jwtSTM, jwtST); 126 | } 127 | 128 | private void propagateUpdate(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { 129 | jwtSTM.setJwtInput(jwtST.getJWTInput()); 130 | contextActionSendJWTtoSuiteTab(jwtSTM.getJwtInput(), false); 131 | } 132 | 133 | @Override 134 | public void changedUpdate(DocumentEvent ignored) { 135 | // not required 136 | } 137 | }; 138 | DocumentListener jwtDocKeyListener = new DocumentListener() { 139 | 140 | @Override 141 | public void removeUpdate(DocumentEvent e) { 142 | propagateUpdate(jwtSTM, jwtST); 143 | } 144 | 145 | @Override 146 | public void insertUpdate(DocumentEvent e) { 147 | propagateUpdate(jwtSTM, jwtST); 148 | } 149 | 150 | private void propagateUpdate(final JWTSuiteTabModel jwtSTM, final JWTSuiteTab jwtST) { 151 | jwtSTM.setJwtKey(jwtST.getKeyInput()); 152 | contextActionKey(jwtSTM.getJwtKey()); 153 | } 154 | 155 | @Override 156 | public void changedUpdate(DocumentEvent ignored) { 157 | // not required 158 | } 159 | }; 160 | 161 | jwtST.registerDocumentListener(jwtDocInputListener, jwtDocKeyListener); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/app/TestConstants.java: -------------------------------------------------------------------------------- 1 | package app; 2 | 3 | public class TestConstants { 4 | private TestConstants() {} 5 | 6 | public static final String METHOD_POST = "POST"; 7 | public static final String METHOD_GET = "GET"; 8 | 9 | public static final String REQUEST_TEMPLATE = """ 10 | ${METHOD:-GET} / HTTP/1.1\r 11 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r 12 | Accept-Language: en-US,en;q=0.5\r${ADD_HEADER:-} 13 | Connection: close\r 14 | Upgrade-Insecure-Requests: 1\r 15 | \r 16 | ${BODY:-}"""; 17 | 18 | public static final String RESPONSE_TEMPLATE = """ 19 | HTTP/1.1 200 OK\r 20 | Connection: Keep-Alive\r 21 | Content-Type: text/html; charset=utf-8\r 22 | Date: Wed, 10 Aug 2026 13:17:18 GMT\r${ADD_HEADER:-} 23 | Server: Apache\r 24 | \r 25 | ${BODY:-}"""; 26 | 27 | public static final String HS256_HEADER = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"; 28 | public static final String RS256_HEADER = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"; 29 | public static final String ES256_HEADER = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"; 30 | 31 | public static final String HS256_TOKEN = HS256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; 32 | public static final String HS256_BEAUTIFIED_TOKEN = HS256_HEADER + ".ewogICAic3ViIjoiMTIzNDU2Nzg5MCIsCiAgICJuYW1lIjoiSm9obiBEb2UiLAogICAiYWRtaW4iOnRydWUKfQ==.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"; 33 | public static final String HS256_TOKEN_2 = HS256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ik1heCBNdXN0ZXJsaSIsImFkbWluIjp0cnVlfQ.9o7iXB3CEm8ciIJjc_yZPI49p7gSKX6zDddr3Gp5_hU"; 34 | 35 | public static final String INVALID_HEADER_TOKEN = "eyJhbFbiOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; 36 | public static final String INVALID_HEADER_TOKEN_2 = "eyJhbFb___RANDOM_GARBAGE___ZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjYZgeFONFh7HgQ"; 37 | public static final String INVALID_JSON_TOKEN = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDdIyfQ.GuoUe6tw79bJlbU1HU0ADX0pr0u2kf3r_4OdrDufSfQ"; 38 | 39 | public static final String ES256_TOKEN = ES256_HEADER + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.tyh-VfuzIxCyGYDlkBA7DfyjrqmSHu6pQ2hoZuFqUSLPNY2N0mpHb3nk5K17HWP_3cYHBw7AhHale5wky6-sVA"; 40 | public static final String ES256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEVs/o5+uQbTjL3chynL4wXgUg2R9q9UU8I5mEovUf86QZ7kOBIjJwqnzD1omageEHWwHdBO6B+dFabmdT9POxg==-----END PUBLIC KEY-----"; 41 | 42 | // How To - CLI 43 | // echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | sed s/\+/-/ | sed -E s/=+$// 44 | // echo -n '{"sub":"RS256inOTA","name":"John Doe"}' | base64 | sed s/\+/-/ | sed -E s/=+$// 45 | // openssl genrsa 2048 > jwtRSA256-private.pem 46 | // openssl rsa -in jwtRSA256-private.pem -pubout -outform PEM -out jwtRSA256-public.pem 47 | // echo -n "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJFUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0" | openssl dgst -sha256 -binary -sign jwtRSA256-private.pem | openssl enc -base64 | tr -d '\n=' | tr -- '+/' '-_' 48 | public static final String RS256_TOKEN = RS256_HEADER + ".eyJzdWIiOiJSUzI1NmluT1RBIiwibmFtZSI6IkpvaG4gRG9lIn0.VnF6UI5CHgOcg4T-k04xWLy5DW_-BiH75ccS9EpF1KP-5QAPKSqhls558cSa2DBPj5yeoFql9DFZ9H_mthbtz_HSfQ1DEDviP5mVfx9c5scEE9ebCaz9a5fQ_2uS2urh6HFTV7kGzjRqKJOCmB6gqtgGsPioDtrWU4o9mlqCh7k3meKTk5AJjeULgts96H2or4P9SUPXmI4Bv97bfSoj8LD3aHgI5FeKBU1KBEDFgDwy3WSI-SBlkf-43EQZwMgIvSVgqY9VXkJnS2aeu76oRn1MzpJBxWVRQaBrTZRnB0CCt3JjtK1QtIGHkl-M9-bVviQ-XtVqp52-DPG2GZFpqQ"; 49 | public static final String RS256_TOKEN_PUB = "-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQAB-----END PUBLIC KEY-----"; 50 | public static final String RS256_TOKEN_PRIV = "-----BEGIN RSA PRIVATE KEY-----MIIEpAIBAAKCAQEAss7FTpt5OpOsNbb5bfmLZnn0D7NzkxqWn4s2r3ZkPcDFMLF4/31sJHCdNkiavFaM7w+DfuSXb0rSQ1Eh/WX9UPR/BN0a8BRzogfzcXOekt4DdnLZibkYtcBfg519tbNVu6geuYi4QbwXrtJUfEAGSbvC3F11aO/qtPHJiwC5XHLgA8kteVXNgto6IBmq2bio9kKMVtceNjxGm6PnH9jBWB3cnlHYipg6hZlqfkiw8sF7UosfTqGn4ibTNUxNVNQw3K5w9S9YylaNq5HOVeHX1egz0aokkXoNwjV/31kG+SQq7MKiJ/PlCPbzY5e3++chEAg6dMKI/FOmIJIwbw1rHwIDAQABAoIBAGBQ7QtoyCZ7gVn10+ofb62lp4gFnA3zVoteS/i8B0cUXaPbFVhaUTRXzPd+qIsm/AeSDbz+mWwDm7tTKsH6fDdtXDZce7Qy8A6pxcKpCxQFr0vQlcmQAPV2SHz3Cs4jad0JtHMwaEBQd1leRtAfFMQG9fIKDcKW6ZDKZUwQ+cgH2XRFbEYtEgrw/G5+ZCwk7lENJCRVIqOGZH5ZmSrIgeEJP1sZgt1+qMDzDSiVV0EHdH07n/5SNuPawazSCa8/NOcpSyADdD3mJ6NN1icS6NAYeFS2mEecx5Wh3Dsx01E1YpdcF2m+nvuWqIlIl8DX1P9cs8fg1qJbedUXE21hg1ECgYEA7AJug9W1Xqt/fFhHCNGaILznsGCKS5FHzm6zOOHX1cH9zjXLl0Ywvv2ioX0utlOk5KxPUwxKTUvg1cvZ0WsJWckgcfkvKxZ/mfT3E56Ocf8i/Ra1R7dF0V+quXE0uNHvfQGyGqU8d6BkDXE3TFFkxzdcoBx1uvk1K8HOxsNtSBsCgYEAwfP9eWOguLFN9C/KuUbeGsRNO1rfQcPL2F8T7W5NaBKZl5f85KuG7JaSA7s4aIpSuACTGYE6AS/4AiShadjf+HXqXcTxBUWJyFoa+Py7Qfb1a4sPOQUnz/CXukiBSHXWmNqVbuKODu6ARmFoHUd0KvK4fPilOnmCfgVbjMtd4U0CgYEA6ltHztYKMiXuhFVMxG8Os++hyj0zVvK+8Thv884f+32VQI2ey2rBwQYv1lhuaFMK7KBGbNtJdRQiAWtZsmCtemEEPOkKc6j1sLXWG79ZB84oulUwUjSludFbwKWvis+9Fs72QwtNziSQ9eA03y37+u74pW1dYvtQV1EuuaUaAX0CgYEAwBFjPkbO7peG3v5E/12SrWcgJFtFI9dFkqv1C/djaGCjAWBd7AWAw+IIDvHkVoJEkDrhcSxryKk8LMMhpbRDd8UtplZVaCcI3wN8Gn4M4rIxL6KyHIFif6V+W9dZT+yB6zTrLrfkfhzposjrVbNg8vcSg4+n8FRMSYf8tVzfRzECgYAcsA/jdZthHEN6P0FycbAL4ALcCK1AcBVwdyrPjOm3sJ+7j0AoIRT9UlIyZ8xhtC3EX2iURJKlENdAnPQYThR+kCWGJq4CHhod9RhJgXzDyYYxyGcLBKTBcXjzZpx0jSguk3UobMdXgL2kG70tfwt1Y1b201OJmOLTg8sFmTYJRA==-----END RSA PRIVATE KEY-----"; 51 | } -------------------------------------------------------------------------------- /src/main/java/app/algorithm/AlgorithmWrapper.java: -------------------------------------------------------------------------------- 1 | package app.algorithm; 2 | 3 | import com.auth0.jwt.algorithms.Algorithm; 4 | import com.auth0.jwt.exceptions.AlgorithmMismatchException; 5 | import com.auth0.jwt.interfaces.ECDSAKeyProvider; 6 | import com.auth0.jwt.interfaces.RSAKeyProvider; 7 | 8 | import java.security.interfaces.ECPrivateKey; 9 | import java.security.interfaces.ECPublicKey; 10 | import java.security.interfaces.RSAPrivateKey; 11 | import java.security.interfaces.RSAPublicKey; 12 | import java.util.stream.Stream; 13 | 14 | import static app.helpers.KeyHelper.getKeyInstance; 15 | import static app.algorithm.AlgorithmType.ASYMMETRIC; 16 | import static app.algorithm.AlgorithmType.SYMMETRIC; 17 | 18 | public enum AlgorithmWrapper { 19 | NONE("none", AlgorithmType.NONE), 20 | HS256("HS256", SYMMETRIC), 21 | HS384("HS384", SYMMETRIC), 22 | HS512("HS512", SYMMETRIC), 23 | RS256("RS256", ASYMMETRIC), 24 | RS384("RS384", ASYMMETRIC), 25 | RS512("RS512", ASYMMETRIC), 26 | ES256("ES256", ASYMMETRIC), 27 | ES256K("ES256K", ASYMMETRIC), 28 | ES384("ES384", ASYMMETRIC), 29 | ES512("ES512", ASYMMETRIC); 30 | 31 | private final String algorithmName; 32 | private final AlgorithmType type; 33 | 34 | AlgorithmWrapper(String algorithmName, AlgorithmType none) { 35 | this.algorithmName = algorithmName; 36 | this.type = none; 37 | } 38 | 39 | String algorithmName() { 40 | return algorithmName; 41 | } 42 | 43 | private Algorithm algorithm(byte[] key) { 44 | switch (this) { 45 | // HMAC with SHA-XXX 46 | case HS256: 47 | return Algorithm.HMAC256(key); 48 | case HS384: 49 | return Algorithm.HMAC384(key); 50 | case HS512: 51 | return Algorithm.HMAC512(key); 52 | 53 | // ECDSA with curve 54 | case ES256: 55 | return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(new String(key))); 56 | case ES256K: 57 | return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(new String(key))); 58 | case ES384: 59 | return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(new String(key))); 60 | case ES512: 61 | return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(new String(key))); 62 | 63 | // RSASSA-PKCS1-v1_5 with SHA-XXX 64 | case RS256: 65 | return Algorithm.RSA256(new RSAKeyProviderImpl(new String(key))); 66 | case RS384: 67 | return Algorithm.RSA384(new RSAKeyProviderImpl(new String(key))); 68 | case RS512: 69 | return Algorithm.RSA512(new RSAKeyProviderImpl(new String(key))); 70 | 71 | case NONE: 72 | default: 73 | throwUnsupportedAlgo(); 74 | return null; 75 | } 76 | } 77 | 78 | private Algorithm algorithm(String key) { 79 | switch (this) { 80 | // HMAC with SHA-XXX 81 | case HS256: 82 | return Algorithm.HMAC256(key); 83 | case HS384: 84 | return Algorithm.HMAC384(key); 85 | case HS512: 86 | return Algorithm.HMAC512(key); 87 | 88 | // ECDSA with curve 89 | case ES256: 90 | return Algorithm.ECDSA256(new ECDSAKeyProviderImpl(key)); 91 | case ES256K: 92 | return Algorithm.ECDSA256K(new ECDSAKeyProviderImpl(key)); 93 | case ES384: 94 | return Algorithm.ECDSA384(new ECDSAKeyProviderImpl(key)); 95 | case ES512: 96 | return Algorithm.ECDSA512(new ECDSAKeyProviderImpl(key)); 97 | 98 | // RSASSA-PKCS1-v1_5 with SHA-XXX 99 | case RS256: 100 | return Algorithm.RSA256(new RSAKeyProviderImpl(key)); 101 | case RS384: 102 | return Algorithm.RSA384(new RSAKeyProviderImpl(key)); 103 | case RS512: 104 | return Algorithm.RSA512(new RSAKeyProviderImpl(key)); 105 | 106 | case NONE: 107 | default: 108 | throwUnsupportedAlgo(); 109 | return null; 110 | } 111 | } 112 | public static Algorithm getVerifierAlgorithm(String algo, String key) { 113 | return withName(algo).algorithm(key); 114 | } 115 | 116 | public static Algorithm getSignerAlgorithm(String algo, String key) { 117 | return withName(algo).algorithm(key); 118 | } 119 | 120 | public static Algorithm getSignerAlgorithm(String algo, byte[] key) { 121 | return withName(algo).algorithm(key); 122 | } 123 | public static AlgorithmWrapper withName(String algorithm) { 124 | return Stream.of(AlgorithmWrapper.values()) 125 | .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) 126 | .findFirst() 127 | .orElseThrow(() -> new AlgorithmMismatchException("Unsupported algorithm '" + algorithm + "'")); 128 | } 129 | 130 | public static AlgorithmType getTypeOf(String algorithm) { 131 | return Stream.of(AlgorithmWrapper.values()) 132 | .filter(supportedAlgorithm -> algorithm.equals(supportedAlgorithm.algorithmName())) 133 | .map(supportedAlgorithm -> supportedAlgorithm.type) 134 | .findFirst() 135 | .orElse(AlgorithmType.NONE); 136 | } 137 | 138 | private static class RSAKeyProviderImpl implements RSAKeyProvider { 139 | private final String key; 140 | 141 | private RSAKeyProviderImpl(String key) { 142 | this.key = key; 143 | } 144 | 145 | @Override 146 | public RSAPublicKey getPublicKeyById(String kid) { 147 | return (RSAPublicKey) getKeyInstance(key, "RSA", false); 148 | } 149 | 150 | @Override 151 | public RSAPrivateKey getPrivateKey() { 152 | return (RSAPrivateKey) getKeyInstance(key, "RSA", true); 153 | } 154 | 155 | @Override 156 | public String getPrivateKeyId() { 157 | return "id"; 158 | } 159 | } 160 | 161 | private static class ECDSAKeyProviderImpl implements ECDSAKeyProvider { 162 | private final String key; 163 | 164 | private ECDSAKeyProviderImpl(String key) { 165 | this.key = key; 166 | } 167 | 168 | @Override 169 | public ECPublicKey getPublicKeyById(String kid) { 170 | return (ECPublicKey) getKeyInstance(key, "EC", false); 171 | } 172 | 173 | @Override 174 | public ECPrivateKey getPrivateKey() { 175 | return (ECPrivateKey) getKeyInstance(key, "EC", true); 176 | } 177 | 178 | @Override 179 | public String getPrivateKeyId() { 180 | return "id"; 181 | } 182 | } 183 | 184 | private void throwUnsupportedAlgo() { 185 | throw new AlgorithmMismatchException("Unsupported algorithm '" + algorithmName + "'"); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/app/controllers/JwtTabController.java: -------------------------------------------------------------------------------- 1 | package app.controllers; 2 | 3 | import java.awt.Component; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Objects; 7 | 8 | import javax.swing.event.DocumentEvent; 9 | import javax.swing.event.DocumentListener; 10 | 11 | import burp.api.montoya.core.ByteArray; 12 | import burp.api.montoya.http.message.HttpMessage; 13 | import burp.api.montoya.http.message.HttpRequestResponse; 14 | import burp.api.montoya.http.message.requests.HttpRequest; 15 | import burp.api.montoya.http.message.responses.HttpResponse; 16 | import burp.api.montoya.ui.Selection; 17 | import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; 18 | import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpResponseEditor; 19 | import com.auth0.jwt.JWT; 20 | import com.auth0.jwt.JWTVerifier; 21 | import com.auth0.jwt.exceptions.InvalidClaimException; 22 | import com.auth0.jwt.exceptions.JWTVerificationException; 23 | import com.auth0.jwt.exceptions.SignatureVerificationException; 24 | import com.auth0.jwt.interfaces.DecodedJWT; 25 | 26 | import app.algorithm.AlgorithmType; 27 | import app.algorithm.AlgorithmWrapper; 28 | import app.helpers.Output; 29 | import app.tokenposition.ITokenPosition; 30 | import gui.JWTViewTab; 31 | import model.CustomJWToken; 32 | import model.JWTTabModel; 33 | import model.Settings; 34 | import model.Strings; 35 | import model.TimeClaim; 36 | 37 | // view to check JWTs such as in the HTTP history 38 | public class JwtTabController implements ExtensionProvidedHttpRequestEditor, ExtensionProvidedHttpResponseEditor { 39 | 40 | private ITokenPosition tokenPosition; 41 | private final ArrayList modelStateList = new ArrayList(); 42 | private byte[] content; 43 | private final JWTTabModel jwtTM; 44 | private final JWTViewTab jwtVT; 45 | boolean isRequest; 46 | private HttpRequestResponse httpRequestResponse; 47 | 48 | public JwtTabController(final JWTTabModel jwtTM, final JWTViewTab jwtVT, boolean isRequest) { 49 | this.jwtTM = jwtTM; 50 | this.jwtVT = jwtVT; 51 | this.isRequest = isRequest; 52 | 53 | DocumentListener documentListener = new DocumentListener() { 54 | 55 | @Override 56 | public void removeUpdate(DocumentEvent arg0) { 57 | jwtTM.setKey(jwtVT.getKeyValue()); 58 | checkKey(jwtTM.getKey()); 59 | } 60 | 61 | @Override 62 | public void insertUpdate(DocumentEvent arg0) { 63 | jwtTM.setKey(jwtVT.getKeyValue()); 64 | checkKey(jwtTM.getKey()); 65 | } 66 | 67 | @Override 68 | public void changedUpdate(DocumentEvent arg0) { 69 | jwtTM.setKey(jwtVT.getKeyValue()); 70 | checkKey(jwtTM.getKey()); 71 | } 72 | }; 73 | 74 | jwtVT.registerDocumentListener(documentListener); 75 | } 76 | 77 | @Override 78 | public HttpRequest getRequest() { 79 | return httpRequestResponse.request(); 80 | } 81 | 82 | @Override 83 | public HttpResponse getResponse() { 84 | return httpRequestResponse.response(); 85 | } 86 | 87 | @Override 88 | public void setRequestResponse(HttpRequestResponse requestResponse) { 89 | httpRequestResponse = requestResponse; 90 | HttpMessage httpMessage; 91 | 92 | if (isRequest) { 93 | httpMessage = requestResponse.request(); 94 | } else { 95 | httpMessage = requestResponse.response(); 96 | } 97 | 98 | try { 99 | tokenPosition = ITokenPosition.findTokenPositionImplementation(httpMessage, this.isRequest); 100 | jwtTM.setJWT(Objects.requireNonNull(tokenPosition).getToken()); 101 | } catch (Exception e) { 102 | Output.outputError("Exception setting message: " + e.getMessage()); 103 | } 104 | CustomJWToken jwt = new CustomJWToken(jwtTM.getJWT()); 105 | jwtTM.setJWTJSON(ReadableTokenFormat.getReadableFormat(jwt)); 106 | List tcl = jwt.getTimeClaimList(); 107 | jwtTM.setTimeClaims(tcl); 108 | if (tokenPosition != null) { 109 | jwtTM.setcFW(tokenPosition.getcFW()); 110 | } 111 | 112 | JWTTabModel current = new JWTTabModel(jwtTM.getKey(), content); 113 | int containsIndex = modelStateList.indexOf(current); 114 | 115 | // we know this request, load it 116 | if (containsIndex != -1) { 117 | JWTTabModel knownModel = modelStateList.get(containsIndex); 118 | jwtTM.setKey(knownModel.getKey()); 119 | jwtTM.setVerificationColor(knownModel.getVerificationColor()); 120 | jwtTM.setVerificationLabel(knownModel.getVerificationLabel()); 121 | // we haven't seen this request yet, add it and set the view to 122 | // default 123 | } else { 124 | modelStateList.add(current); 125 | jwtTM.setVerificationColor(Settings.COLOR_UNDEFINED); 126 | jwtTM.setVerificationResult(""); 127 | jwtTM.setKey(""); 128 | } 129 | AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); 130 | jwtVT.updateSetView(algoType); 131 | } 132 | 133 | @Override 134 | public boolean isEnabledFor(HttpRequestResponse requestResponse) { 135 | HttpMessage message; 136 | 137 | if (this.isRequest) { 138 | this.content = requestResponse.request().toString().getBytes(); 139 | message = requestResponse.request(); 140 | } else { 141 | this.content = requestResponse.response().toString().getBytes(); 142 | message = requestResponse.response(); 143 | } 144 | 145 | return ITokenPosition.findTokenPositionImplementation(message, this.isRequest) != null; 146 | } 147 | 148 | @Override 149 | public String caption() { 150 | return Settings.TAB_NAME; 151 | } 152 | 153 | @Override 154 | public Component uiComponent() { 155 | return this.jwtVT; 156 | } 157 | 158 | @Override 159 | public Selection selectedData() { 160 | return Selection.selection(ByteArray.byteArray(jwtVT.getSelectedData().getBytes())); 161 | } 162 | 163 | @Override 164 | public boolean isModified() { 165 | return false; 166 | } 167 | 168 | public void checkKey(String key) { 169 | jwtTM.setVerificationResult(""); 170 | String curAlgo = getCurrentAlgorithm(); 171 | AlgorithmType algoType = AlgorithmWrapper.getTypeOf(getCurrentAlgorithm()); 172 | try { 173 | JWTVerifier verifier = JWT.require(AlgorithmWrapper.getVerifierAlgorithm(curAlgo, key)).build(); 174 | DecodedJWT test = verifier.verify(jwtTM.getJWT()); 175 | jwtTM.setVerificationLabel(Strings.VALID_VERFICIATION); 176 | jwtTM.setVerificationColor(Settings.getValidColor()); 177 | test.getAlgorithm(); 178 | jwtVT.updateSetView(algoType); 179 | } catch (JWTVerificationException e) { 180 | if (e instanceof SignatureVerificationException) { 181 | jwtTM.setVerificationColor(Settings.getInvalidColor()); 182 | jwtTM.setVerificationLabel(Strings.INVALID_SIGNATURE_VERIFICATION); 183 | } else if (e instanceof InvalidClaimException) { 184 | jwtTM.setVerificationColor(Settings.getProblemColor()); 185 | jwtTM.setVerificationLabel(Strings.INVALID_CLAIM_VERIFICATION); 186 | } else { 187 | jwtTM.setVerificationColor(Settings.getProblemColor()); 188 | jwtTM.setVerificationLabel(Strings.GENERIC_ERROR_VERIFICATION); 189 | } 190 | jwtTM.setVerificationResult(e.getMessage()); 191 | jwtVT.updateSetView(algoType); 192 | } catch (IllegalArgumentException e) { 193 | jwtTM.setVerificationResult(e.getMessage()); 194 | jwtTM.setVerificationLabel(Strings.INVALID_KEY_VERIFICATION); 195 | jwtTM.setVerificationColor(Settings.getProblemColor()); 196 | jwtVT.updateSetView(algoType); 197 | } 198 | JWTTabModel current = new JWTTabModel(key, content); 199 | int containsIndex = modelStateList.indexOf(current); 200 | if (containsIndex != -1) { // we know this request, update the viewstate 201 | JWTTabModel knownState = modelStateList.get(containsIndex); 202 | knownState.setKeyValueAndHash(key, current.getHashCode()); 203 | knownState.setVerificationResult(jwtTM.getVerificationLabel()); 204 | knownState.setVerificationColor(jwtTM.getVerificationColor()); 205 | } 206 | } 207 | 208 | public String getCurrentAlgorithm() { 209 | return new CustomJWToken(jwtTM.getJWT()).getAlgorithm(); 210 | } 211 | 212 | } 213 | -------------------------------------------------------------------------------- /src/main/java/app/helpers/Config.java: -------------------------------------------------------------------------------- 1 | package app.helpers; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Paths; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | 11 | import com.eclipsesource.json.Json; 12 | import com.eclipsesource.json.JsonArray; 13 | import com.eclipsesource.json.JsonObject; 14 | import com.eclipsesource.json.JsonValue; 15 | import com.eclipsesource.json.WriterConfig; 16 | 17 | public class Config { 18 | 19 | public static List tokenKeywords = Arrays.asList("id_token", "ID_TOKEN", "access_token", "token"); 20 | public static String highlightColor = "Blue"; 21 | public static String interceptComment = "Contains a JWT"; 22 | public static String SecretFoundHighlightColor = "Red"; 23 | public static String SecretFoundInterceptComment = "JWT secret found: "; 24 | public static boolean resetEditor = true; 25 | public static boolean o365Support = true; 26 | public static String configName = "config.json"; 27 | public static String configFolderName = ".JWT4B"; 28 | public static String configPath = 29 | System.getProperty("user.home") + File.separator + configFolderName + File.separator + configName; 30 | 31 | /* 32 | ssh-keygen -t rsa -b 2048 -m PEM -f jwtRS256.key 33 | # Don't add passphrase 34 | openssl rsa -in jwtRS256.key -pubout -outform PEM -out jwtRS256.key.pub 35 | cat jwtRS256.key 36 | cat jwtRS256.key.pub 37 | */ 38 | 39 | public static String cveAttackModePublicKey = 40 | "-----BEGIN PUBLIC KEY-----\n" + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuvBC2RJqGAbPg6HoJaOl\n" 41 | + "T6L4tMwMzGUI8TptoBlStWe+TfRcuPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBm\n" 42 | + "mdzuNthAdPDMXlrnIbOywG52iPtHAV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmE\n" 43 | + "CVX+EeNwZlOEDfbtnpOTMRr/24r1CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+Uq\n" 44 | + "zwfNJ9BfE53u9JHJP/nWZopqlNQ26fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCw\n" 45 | + "Ino/ZEq7oN9tphmAPBwdfQhb2xmD3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC\n" + "2wIDAQAB\n" 46 | + "-----END PUBLIC KEY-----"; 47 | 48 | public static String cveAttackModePrivateKey = 49 | "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEowIBAAKCAQEAuvBC2RJqGAbPg6HoJaOlT6L4tMwMzGUI8TptoBlStWe+TfRc\n" 50 | + "uPVfxI1U6g87/7B62768kuU55H8bd3Yd7nBmmdzuNthAdPDMXlrnIbOywG52iPtH\n" 51 | + "AV1U5Vk5QGuj39aSuLjpBSC4jUJPcdJENpmECVX+EeNwZlOEDfbtnpOTMRr/24r1\n" 52 | + "CLSMwp9gtaLnE6NJzh+ycTDgyrWK9OtNA+UqzwfNJ9BfE53u9JHJP/nWZopqlNQ2\n" 53 | + "6fgPASu8FULa8bmJ3kc0SZFCNvXyjZn7HVCwIno/ZEq7oN9tphmAPBwdfQhb2xmD\n" 54 | + "3gYeWrXNP/M+SKisaX1CVwaPPowjCQMbsmfC2wIDAQABAoIBAGtODOEzq8i86BMk\n" 55 | + "NfCdHgA3iVGmq1YMTPTDWDgFMS/GLDvtH+hfmShnBC4SrpsXv34x32bmw7OArtCE\n" 56 | + "8atzw8FgSzEaMu2tZ3Jl9bSnxNymy83XhyumWlwIOk/bOcb8EV6NbdyuqqETRi0M\n" 57 | + "yHEa7+q3/M5h4pwqJmwpqL5U8bHGVGXNEbiA/TneNyXjSn03uPYaKTw4R9EG951A\n" 58 | + "pCJf4Atba5VIfdZ59fx/6rxCuKjWlvZrklE3Cll/+A0dRN5vBSR+EBYgfedMPepM\n" 59 | + "6TYDOsQnsy1bFJjy+aE/kwYGgtjuHOlvCpwq90SY3WueXClDfioaJ/1S6QT3q8hf\n" 60 | + "UHodWxkCgYEA8X6+dybVvBgawxyYZEi1P/KNWC9tr2zdztnkDB4nn97UIJzxmjTh\n" 61 | + "s81EsX0Mt24DJg36HoX5x1lDHNrR2RvIEPy8vfzTdNVa6KP7E7CWUUcW39nmt/z7\n" 62 | + "ezlyZa8TVPBE/xvozdZuTAzd0rafUX3Ugqzn17MBshz07/K4Z0iy/C0CgYEAxiqm\n" 63 | + "J7ul9CmNVvCnQ19tvcO7kY8h9AYIEtrqf9ubiq9W7Ldf9mXIhlG3wr6U3dXuAVVa\n" 64 | + "4g9zkXr+N7BE4hlQcJpBn5ywtYfqzK1GRy+rfwPgC/JbWEnNDP8oYnZ8R6pkhyOC\n" 65 | + "zqDqCZPtnmD9Je/ifdmgIkkxQD25ktyCYMhPuCcCgYEAh/MQCkfEfxUay8gnSh1c\n" 66 | + "W9mSFJjuqJki7TXgmanIKMnqpUl1AZjPjsb56uk45XJ7N0sbCV/m04C+tVnCVPS8\n" 67 | + "1kNRhar054rMmLbnu5fnp23bxL0Ik39Jm38llXTP7zsrvGnbzzTt9sYvglXorpml\n" 68 | + "rsLj6ZwOUlTW1tXPVeWpTSkCgYBfAkGpWRlGx8lA/p5i+dTGn5pFPmeb9GxYheba\n" 69 | + "KDMZudkmIwD6RHBwnatJzk/XT+MNdpvdOGVDQcGyd2t/L33Wjs6ZtOkwD5suSIEi\n" 70 | + "TiOeAQChGbBb0v5hldAJ7R7GyVXrSMZFRPcQYoERZxTX5HwltHpHFepsD2vykpBb\n" 71 | + "0I4QDwKBgDRH3RjKJduH2WvHOmQmXqWwtkY7zkLwSysWTW5KvCEUI+4VHMggaQ9Z\n" 72 | + "YUXuHa8osFZ8ruJzSd0HTrDVuNTb8Q7XADOn4a5AGHu1Bhw996uNCP075dx8IOsl\n" 73 | + "B6zvMHB8rRW93GfFd08REpsgqSm+AL6iLlZHowC00FFPtLs9e7ci\n" + "-----END RSA PRIVATE KEY-----"; 74 | 75 | 76 | public static void loadConfig() { 77 | 78 | File configFile = new File(configPath); 79 | 80 | if (!configFile.getParentFile().exists()) { 81 | Output.output("Config file directory '" + configFolderName + "' does not exist - creating it"); 82 | boolean mkdir = configFile.getParentFile().mkdir(); 83 | if (!mkdir) { 84 | Output.outputError("Could not create directory '" + configFile.getParentFile().toString() + "'"); 85 | } 86 | } 87 | 88 | if (!configFile.exists()) { 89 | Output.output("Config file '" + configPath + "' does not exist - creating it"); 90 | try { 91 | boolean configFileCreated = configFile.createNewFile(); 92 | if (!configFileCreated) { 93 | throw new IOException("Create new file failed for config file"); 94 | } 95 | } catch (IOException e) { 96 | Output.outputError( 97 | "Error creating config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() 98 | .toString()); 99 | return; 100 | } 101 | String defaultConfigJSONRaw = generateDefaultConfigFile(); 102 | try { 103 | Files.write(Paths.get(configPath), defaultConfigJSONRaw.getBytes()); 104 | } catch (IOException e) { 105 | Output.outputError( 106 | "Error writing config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() 107 | .toString()); 108 | } 109 | } 110 | 111 | try { 112 | String configRaw = new String(Files.readAllBytes(Paths.get(configPath))); 113 | JsonObject configJO = Json.parse(configRaw).asObject(); 114 | 115 | JsonArray tokenKeywordsJA = configJO.get("tokenKeywords").asArray(); 116 | tokenKeywords = new ArrayList<>(); 117 | for (JsonValue tokenKeyword : tokenKeywordsJA) { 118 | tokenKeywords.add(tokenKeyword.asString()); 119 | } 120 | 121 | resetEditor = configJO.getBoolean("resetEditor", true); 122 | 123 | o365Support = configJO.getBoolean("o365Support", true); 124 | 125 | highlightColor = configJO.get("highlightColor").asString(); 126 | 127 | // support color names regardless of case 128 | highlightColor = highlightColor.substring(0, 1).toUpperCase() + highlightColor.substring(1).toLowerCase(); 129 | 130 | // red, orange, yellow, green, cyan, blue, pink, magenta, gray,or a null String to clear any existing highlight. 131 | ArrayList allowedColors = new ArrayList<>( 132 | Arrays.asList("Red", "Orange", "Yellow", "Green", "Cyan", "Blue", "Pink", "Magenta", "Gray", "None")); 133 | if (!allowedColors.contains(highlightColor)) { 134 | highlightColor = "None"; 135 | Output.output( 136 | "Unknown color, only 'Red, Orange, Yellow, Green, Cyan, Blue, Pink, Magenta, Gray, None' is possible - defaulting to None."); 137 | } 138 | 139 | interceptComment = configJO.get("interceptComment").asString(); 140 | cveAttackModePublicKey = configJO.get("cveAttackModePublicKey").asString(); 141 | cveAttackModePrivateKey = configJO.get("cveAttackModePrivateKey").asString(); 142 | 143 | } catch (IOException e) { 144 | Output.outputError( 145 | "Error loading config file '" + configPath + "' - message:" + e.getMessage() + " - cause:" + e.getCause() 146 | .toString()); 147 | } 148 | } 149 | 150 | private static String generateDefaultConfigFile() { 151 | JsonObject configJO = new JsonObject(); 152 | 153 | JsonArray tokenKeywordsJA = new JsonArray(); 154 | for (String tokenKeyword : tokenKeywords) { 155 | tokenKeywordsJA.add(tokenKeyword); 156 | } 157 | 158 | configJO.add("resetEditor", true); 159 | configJO.add("o365Support", true); 160 | configJO.add("highlightColor", highlightColor); 161 | configJO.add("interceptComment", interceptComment); 162 | configJO.add("tokenKeywords", tokenKeywordsJA); 163 | 164 | configJO.add("cveAttackModePublicKey", cveAttackModePublicKey); 165 | configJO.add("cveAttackModePrivateKey", cveAttackModePrivateKey); 166 | 167 | return configJO.toString(WriterConfig.PRETTY_PRINT); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/test/java/burp/api/montoya/core/FakeHttpRequest.java: -------------------------------------------------------------------------------- 1 | package burp.api.montoya.core; 2 | 3 | import burp.api.montoya.http.HttpService; 4 | import burp.api.montoya.http.message.ContentType; 5 | import burp.api.montoya.http.message.HttpHeader; 6 | import burp.api.montoya.http.message.params.HttpParameter; 7 | import burp.api.montoya.http.message.params.HttpParameterType; 8 | import burp.api.montoya.http.message.params.ParsedHttpParameter; 9 | import burp.api.montoya.http.message.requests.HttpRequest; 10 | import burp.api.montoya.http.message.requests.HttpTransformation; 11 | 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | import java.util.stream.Collectors; 15 | 16 | public class FakeHttpRequest extends FakeHttpMessage implements HttpRequest { 17 | 18 | List parameters = new ArrayList<>();; 19 | 20 | private FakeHttpRequest(FakeHttpRequest request, HttpParameter parameter) { 21 | this(request.rawContent); 22 | 23 | // update parameters 24 | parameters = parameters.stream() 25 | .map(o -> o.name().equals(parameter.name()) ? new FakeParsedHttpParameter(parameter.name(), parameter.value(), parameter.type(), new FakeRange(0, 0), new FakeRange(0, 0)) : o) 26 | .toList(); 27 | 28 | // update body 29 | this.body = parameters.stream().filter(o -> o.type() == HttpParameterType.BODY).map(o -> ((FakeParsedHttpParameter) o).toNameValueString()).collect(Collectors.joining("&")); 30 | 31 | // update raw content 32 | this.rawContent = this.header + "\r\n" + this.body; 33 | } 34 | 35 | public FakeHttpRequest(String request) { 36 | super(request); 37 | 38 | processBody(); 39 | } 40 | 41 | private void processBody() { 42 | List params = List.of(body.split("&")); 43 | for (String param : params) { 44 | String[] keyValue = param.split("="); 45 | if (keyValue.length == 2) { 46 | String name = keyValue[0].trim(); 47 | String value = keyValue[1].trim(); 48 | 49 | // range is not yet supported 50 | parameters.add(new FakeParsedHttpParameter(name, value, HttpParameterType.BODY, new FakeRange(0, 0), new FakeRange(0, 1))); 51 | } 52 | } 53 | } 54 | 55 | @Override 56 | public boolean isInScope() { 57 | System.err.println("Not implemented"); 58 | return false; 59 | } 60 | 61 | @Override 62 | public HttpService httpService() { 63 | System.err.println("Not implemented"); 64 | return null; 65 | } 66 | 67 | @Override 68 | public String url() { 69 | System.err.println("Not implemented"); 70 | return ""; 71 | } 72 | 73 | @Override 74 | public String method() { 75 | System.err.println("Not implemented"); 76 | return ""; 77 | } 78 | 79 | @Override 80 | public String path() { 81 | System.err.println("Not implemented"); 82 | return ""; 83 | } 84 | 85 | @Override 86 | public String query() { 87 | System.err.println("Not implemented"); 88 | return ""; 89 | } 90 | 91 | @Override 92 | public String pathWithoutQuery() { 93 | System.err.println("Not implemented"); 94 | return ""; 95 | } 96 | 97 | @Override 98 | public String fileExtension() { 99 | System.err.println("Not implemented"); 100 | return ""; 101 | } 102 | 103 | @Override 104 | public ContentType contentType() { 105 | System.err.println("Not implemented"); 106 | return null; 107 | } 108 | 109 | @Override 110 | public List parameters() { 111 | return parameters; 112 | } 113 | 114 | @Override 115 | public List parameters(HttpParameterType type) { 116 | return parameters.stream().filter(parameter -> (parameter.type() == type)).toList(); 117 | } 118 | 119 | @Override 120 | public boolean hasParameters() { 121 | return !parameters.isEmpty(); 122 | } 123 | 124 | @Override 125 | public boolean hasParameters(HttpParameterType type) { 126 | return !parameters.stream().filter(parameter -> (parameter.type() == type)).toList().isEmpty(); 127 | } 128 | 129 | @Override 130 | public ParsedHttpParameter parameter(String name, HttpParameterType type) { 131 | return parameters.stream().filter(parameter -> (parameter.name().equals(name) && (parameter.type() == type))).findAny().orElse(null); 132 | } 133 | 134 | @Override 135 | public String parameterValue(String name, HttpParameterType type) { 136 | return parameters.stream().filter(parameter -> (parameter.name().equals(name) && (parameter.type() == type))).findAny().map(ParsedHttpParameter::value).orElse(null); 137 | } 138 | 139 | @Override 140 | public boolean hasParameter(String name, HttpParameterType type) { 141 | return parameters.stream().anyMatch(parameter -> (parameter.name().equals(name) && parameter.type() == type)); 142 | } 143 | 144 | @Override 145 | public boolean hasParameter(HttpParameter parameter) { 146 | System.err.println("Not implemented"); 147 | return false; 148 | } 149 | 150 | @Override 151 | public HttpRequest copyToTempFile() { 152 | System.err.println("Not implemented"); 153 | return null; 154 | } 155 | 156 | @Override 157 | public HttpRequest withService(HttpService service) { 158 | System.err.println("Not implemented"); 159 | return null; 160 | } 161 | 162 | @Override 163 | public HttpRequest withPath(String path) { 164 | System.err.println("Not implemented"); 165 | return null; 166 | } 167 | 168 | @Override 169 | public HttpRequest withMethod(String method) { 170 | System.err.println("Not implemented"); 171 | return null; 172 | } 173 | 174 | @Override 175 | public HttpRequest withHeader(HttpHeader header) { 176 | System.err.println("Not implemented"); 177 | return null; 178 | } 179 | 180 | @Override 181 | public HttpRequest withHeader(String name, String value) { 182 | System.err.println("Not implemented"); 183 | return null; 184 | } 185 | 186 | @Override 187 | public HttpRequest withParameter(HttpParameter parameter) { 188 | return new FakeHttpRequest(this, parameter); 189 | } 190 | 191 | @Override 192 | public HttpRequest withAddedParameters(List parameters) { 193 | System.err.println("Not implemented"); 194 | return null; 195 | } 196 | 197 | @Override 198 | public HttpRequest withAddedParameters(HttpParameter... parameters) { 199 | System.err.println("Not implemented"); 200 | return null; 201 | } 202 | 203 | @Override 204 | public HttpRequest withRemovedParameters(List parameters) { 205 | System.err.println("Not implemented"); 206 | return null; 207 | } 208 | 209 | @Override 210 | public HttpRequest withRemovedParameters(HttpParameter... parameters) { 211 | System.err.println("Not implemented"); 212 | return null; 213 | } 214 | 215 | @Override 216 | public HttpRequest withUpdatedParameters(List parameters) { 217 | System.err.println("Not implemented"); 218 | return null; 219 | } 220 | 221 | @Override 222 | public HttpRequest withUpdatedParameters(HttpParameter... parameters) { 223 | System.err.println("Not implemented"); 224 | return null; 225 | } 226 | 227 | @Override 228 | public HttpRequest withTransformationApplied(HttpTransformation transformation) { 229 | System.err.println("Not implemented"); 230 | return null; 231 | } 232 | 233 | @Override 234 | public HttpRequest withBody(String body) { 235 | System.err.println("Not implemented"); 236 | return null; 237 | } 238 | 239 | @Override 240 | public HttpRequest withBody(ByteArray body) { 241 | System.err.println("Not implemented"); 242 | return null; 243 | } 244 | 245 | @Override 246 | public HttpRequest withAddedHeader(String name, String value) { 247 | System.err.println("Not implemented"); 248 | return null; 249 | } 250 | 251 | @Override 252 | public HttpRequest withAddedHeader(HttpHeader header) { 253 | System.err.println("Not implemented"); 254 | return null; 255 | } 256 | 257 | @Override 258 | public HttpRequest withUpdatedHeader(String name, String value) { 259 | System.err.println("Not implemented"); 260 | return null; 261 | } 262 | 263 | @Override 264 | public HttpRequest withUpdatedHeader(HttpHeader header) { 265 | System.err.println("Not implemented"); 266 | return null; 267 | } 268 | 269 | @Override 270 | public HttpRequest withRemovedHeader(String name) { 271 | System.err.println("Not implemented"); 272 | return null; 273 | } 274 | 275 | @Override 276 | public HttpRequest withRemovedHeader(HttpHeader header) { 277 | System.err.println("Not implemented"); 278 | return null; 279 | } 280 | 281 | @Override 282 | public HttpRequest withMarkers(List markers) { 283 | System.err.println("Not implemented"); 284 | return null; 285 | } 286 | 287 | @Override 288 | public HttpRequest withMarkers(Marker... markers) { 289 | System.err.println("Not implemented"); 290 | return null; 291 | } 292 | 293 | @Override 294 | public HttpRequest withDefaultHeaders() { 295 | System.err.println("Not implemented"); 296 | return null; 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/java/gui/JWTViewTab.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Dimension; 5 | import java.awt.Font; 6 | import java.awt.GridBagConstraints; 7 | import java.awt.GridBagLayout; 8 | import java.awt.Insets; 9 | import java.awt.SystemColor; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.ActionListener; 12 | 13 | import javax.swing.JButton; 14 | import javax.swing.JLabel; 15 | import javax.swing.JPanel; 16 | import javax.swing.JPopupMenu; 17 | import javax.swing.JTextArea; 18 | import javax.swing.SwingUtilities; 19 | import javax.swing.UIManager; 20 | import javax.swing.event.DocumentListener; 21 | import javax.swing.text.JTextComponent; 22 | 23 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 24 | import org.fife.ui.rsyntaxtextarea.Style; 25 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants; 26 | import org.fife.ui.rsyntaxtextarea.SyntaxScheme; 27 | import org.fife.ui.rsyntaxtextarea.Token; 28 | import org.fife.ui.rtextarea.RTextScrollPane; 29 | 30 | import app.algorithm.AlgorithmType; 31 | import burp.api.montoya.MontoyaApi; 32 | import model.JWTTabModel; 33 | import model.Strings; 34 | 35 | public class JWTViewTab extends JPanel { 36 | 37 | private static final long serialVersionUID = 1L; 38 | private RSyntaxTextArea outputField; 39 | private JTextArea jwtKeyArea; 40 | private JLabel keyLabel; 41 | private JButton verificationIndicator; 42 | private final JWTTabModel jwtTM; 43 | private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; 44 | private JLabel lblCookieFlags; 45 | private JLabel lbRegisteredClaims; 46 | private JLabel outputLabel; 47 | private MontoyaApi api; 48 | 49 | public JWTViewTab(JWTTabModel jwtTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory, MontoyaApi api) { 50 | this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; 51 | this.api = api; 52 | drawPanel(); 53 | this.jwtTM = jwtTM; 54 | } 55 | 56 | public void registerDocumentListener(DocumentListener inputFieldListener) { 57 | jwtKeyArea.getDocument().addDocumentListener(inputFieldListener); 58 | } 59 | 60 | private void drawPanel() { 61 | Font currentFont = api.userInterface().currentDisplayFont(); 62 | 63 | GridBagLayout gridBagLayout = new GridBagLayout(); 64 | gridBagLayout.columnWidths = new int[] { 10, 447, 0, 0 }; 65 | gridBagLayout.rowHeights = new int[] { 10, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 66 | gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0, Double.MIN_VALUE }; 67 | gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, Double.MIN_VALUE }; 68 | setLayout(gridBagLayout); 69 | 70 | keyLabel = new JLabel(" "); 71 | keyLabel.setFont(currentFont); 72 | GridBagConstraints gbc_inputLabel1 = new GridBagConstraints(); 73 | gbc_inputLabel1.fill = GridBagConstraints.VERTICAL; 74 | gbc_inputLabel1.insets = new Insets(0, 0, 5, 5); 75 | gbc_inputLabel1.anchor = GridBagConstraints.WEST; 76 | gbc_inputLabel1.gridx = 1; 77 | gbc_inputLabel1.gridy = 1; 78 | add(keyLabel, gbc_inputLabel1); 79 | 80 | jwtKeyArea = new JTextArea(); 81 | jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); 82 | GridBagConstraints gbc_inputField1 = new GridBagConstraints(); 83 | gbc_inputField1.insets = new Insets(0, 0, 5, 5); 84 | gbc_inputField1.fill = GridBagConstraints.HORIZONTAL; 85 | gbc_inputField1.gridx = 1; 86 | gbc_inputField1.gridy = 2; 87 | add(jwtKeyArea, gbc_inputField1); 88 | jwtKeyArea.setColumns(10); 89 | 90 | verificationIndicator = new JButton(""); 91 | verificationIndicator.setText(Strings.NO_SECRET_PROVIDED); 92 | verificationIndicator.addActionListener(new ActionListener() { 93 | 94 | public void actionPerformed(ActionEvent e) { 95 | } 96 | }); 97 | Dimension preferredSize = new Dimension(400, 30); 98 | verificationIndicator.setPreferredSize(preferredSize); 99 | GridBagConstraints gbc_validIndicator = new GridBagConstraints(); 100 | gbc_validIndicator.insets = new Insets(0, 0, 5, 5); 101 | gbc_validIndicator.gridx = 1; 102 | gbc_validIndicator.gridy = 4; 103 | add(verificationIndicator, gbc_validIndicator); 104 | 105 | JTextComponent.removeKeymap("RTextAreaKeymap"); 106 | outputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); 107 | UIManager.put("RSyntaxTextAreaUI.actionMap", null); 108 | UIManager.put("RSyntaxTextAreaUI.inputMap", null); 109 | UIManager.put("RTextAreaUI.actionMap", null); 110 | UIManager.put("RTextAreaUI.inputMap", null); 111 | outputField.setFont(currentFont); 112 | 113 | outputField.setWhitespaceVisible(true); 114 | SyntaxScheme scheme = outputField.getSyntaxScheme(); 115 | Style style = new Style(); 116 | style.foreground = new Color(222, 133, 10); 117 | scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); 118 | outputField.revalidate(); 119 | outputField.setHighlightCurrentLine(false); 120 | outputField.setCurrentLineHighlightColor(Color.WHITE); 121 | outputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); 122 | outputField.setEditable(false); 123 | outputField.setPopupMenu(new JPopupMenu()); // no context menu on right-click 124 | 125 | outputLabel = new JLabel("JWT"); 126 | outputLabel.setFont(currentFont); 127 | GridBagConstraints gbc_outputLabel = new GridBagConstraints(); 128 | gbc_outputLabel.anchor = GridBagConstraints.WEST; 129 | gbc_outputLabel.insets = new Insets(0, 0, 5, 5); 130 | gbc_outputLabel.gridx = 1; 131 | gbc_outputLabel.gridy = 5; 132 | add(outputLabel, gbc_outputLabel); 133 | 134 | lbRegisteredClaims = new JLabel(); 135 | lbRegisteredClaims.putClientProperty("html.disable", null); 136 | lbRegisteredClaims.setBackground(SystemColor.controlHighlight); 137 | GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); 138 | gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; 139 | gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); 140 | gbc_lbRegisteredClaims.gridx = 1; 141 | gbc_lbRegisteredClaims.gridy = 8; 142 | add(lbRegisteredClaims, gbc_lbRegisteredClaims); 143 | 144 | lblCookieFlags = new JLabel(" "); 145 | lblCookieFlags.putClientProperty("html.disable", null); 146 | lblCookieFlags.setFont(currentFont); 147 | GridBagConstraints gbc_lblCookieFlags = new GridBagConstraints(); 148 | gbc_lblCookieFlags.anchor = GridBagConstraints.SOUTHWEST; 149 | gbc_lblCookieFlags.insets = new Insets(0, 0, 5, 5); 150 | gbc_lblCookieFlags.gridx = 1; 151 | gbc_lblCookieFlags.gridy = 9; 152 | add(lblCookieFlags, gbc_lblCookieFlags); 153 | 154 | RTextScrollPane sp = new RTextScrollPane(outputField); 155 | sp.setLineNumbersEnabled(false); 156 | 157 | GridBagConstraints gbc_outputfield = new GridBagConstraints(); 158 | gbc_outputfield.insets = new Insets(0, 0, 5, 5); 159 | gbc_outputfield.fill = GridBagConstraints.BOTH; 160 | gbc_outputfield.gridx = 1; 161 | gbc_outputfield.gridy = 6; 162 | add(sp, gbc_outputfield); 163 | 164 | } 165 | 166 | public JTextArea getOutputfield() { 167 | return outputField; 168 | } 169 | 170 | public String getKeyValue() { 171 | return jwtKeyArea.getText(); 172 | } 173 | 174 | public void setKeyValue(String value) { 175 | jwtKeyArea.setText(value); 176 | } 177 | 178 | public void setVerificationResult(String value) { 179 | verificationIndicator.setText(value); 180 | } 181 | 182 | public void setVerificationResultColor(Color verificationResultColor) { 183 | verificationIndicator.setBackground(verificationResultColor); 184 | } 185 | 186 | public void setCaret() { 187 | outputField.setCaretPosition(0); 188 | } 189 | 190 | public String getSelectedData() { 191 | return getOutputfield().getSelectedText(); 192 | } 193 | 194 | public void updateSetView(AlgorithmType algorithmType) { 195 | SwingUtilities.invokeLater(new Runnable() { 196 | 197 | public void run() { 198 | if (!jwtTM.getJWTJSON().equals(outputField.getText())) { 199 | outputField.setText(jwtTM.getJWTJSON()); 200 | } 201 | if (!jwtTM.getKeyLabel().equals(keyLabel.getText())) { 202 | keyLabel.setText(jwtTM.getKeyLabel()); 203 | } 204 | if (!jwtTM.getKey().equals(jwtKeyArea.getText())) { 205 | jwtKeyArea.setText(jwtTM.getKey()); 206 | } 207 | if (!jwtTM.getVerificationColor().equals(verificationIndicator.getBackground())) { 208 | verificationIndicator.setBackground(jwtTM.getVerificationColor()); 209 | } 210 | if (!jwtTM.getVerificationLabel().equals(verificationIndicator.getText())) { 211 | if (jwtTM.getVerificationLabel().equals("")) { 212 | verificationIndicator.setText(Strings.NO_SECRET_PROVIDED); 213 | } else { 214 | verificationIndicator.setText(jwtTM.getVerificationLabel()); 215 | } 216 | 217 | } 218 | if (algorithmType.equals(AlgorithmType.SYMMETRIC)) { 219 | keyLabel.setText("Secret"); 220 | jwtKeyArea.setEnabled(true); 221 | } 222 | if (algorithmType.equals(AlgorithmType.ASYMMETRIC)) { 223 | keyLabel.setText("Public Key"); 224 | jwtKeyArea.setEnabled(true); 225 | } 226 | if (algorithmType.equals(AlgorithmType.NONE)) { 227 | keyLabel.setText(""); 228 | jwtKeyArea.setEnabled(false); 229 | jwtKeyArea.setEnabled(false); 230 | } 231 | 232 | if (jwtTM.getcFW().isCookie()) { 233 | lblCookieFlags.setText(jwtTM.getcFW().toHTMLString()); 234 | } else { 235 | lblCookieFlags.setText(""); 236 | } 237 | setCaret(); 238 | lbRegisteredClaims.setText(jwtTM.getTimeClaimsAsText()); 239 | } 240 | }); 241 | } 242 | 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/model/CustomJWToken.java: -------------------------------------------------------------------------------- 1 | package model; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.ByteArrayOutputStream; 5 | import java.io.IOException; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Objects; 10 | import java.util.TimeZone; 11 | import java.util.zip.GZIPInputStream; 12 | 13 | import org.apache.commons.codec.binary.Base64; 14 | import org.apache.commons.codec.binary.StringUtils; 15 | 16 | import com.auth0.jwt.JWT; 17 | import com.auth0.jwt.algorithms.Algorithm; 18 | import com.auth0.jwt.exceptions.JWTDecodeException; 19 | import com.eclipsesource.json.Json; 20 | import com.eclipsesource.json.JsonObject; 21 | import com.eclipsesource.json.JsonValue; 22 | import com.fasterxml.jackson.core.JsonProcessingException; 23 | import com.fasterxml.jackson.databind.JsonNode; 24 | import com.fasterxml.jackson.databind.ObjectMapper; 25 | 26 | import app.helpers.Minify; 27 | import app.helpers.Output; 28 | 29 | /* 30 | * This Class is implemented separately to get raw access to the content of the Tokens. 31 | * The JWTDecoder class cannot be extended because it is final 32 | */ 33 | 34 | public class CustomJWToken extends JWT { 35 | 36 | private boolean isMinified; 37 | private String headerJson; 38 | private String payloadJson; 39 | private byte[] signature; 40 | private final List timeClaimList = new ArrayList<>(); 41 | private boolean builtSuccessfully = true; 42 | 43 | public CustomJWToken(String token) { 44 | construct(token, false); 45 | } 46 | 47 | public CustomJWToken(String token, boolean log) { 48 | construct(token, log); 49 | } 50 | 51 | private void construct(String token, boolean log) { 52 | if (token != null) { 53 | final String[] parts = splitToken(token,log); 54 | try { 55 | headerJson = StringUtils.newStringUtf8(Base64.decodeBase64(parts[0])); 56 | byte[] payloadBase64 = Base64.decodeBase64(parts[1]); 57 | payloadJson = StringUtils.newStringUtf8(payloadBase64); 58 | isMinified = (isMinified(payloadJson) && isMinified(headerJson)); 59 | JsonObject headerObject; 60 | try { 61 | headerObject = Json.parse(headerJson).asObject(); 62 | if (headerObject.getString("zip", "").equalsIgnoreCase("GZIP")) { 63 | GZIPInputStream gis = new GZIPInputStream(new ByteArrayInputStream(payloadBase64)); 64 | ByteArrayOutputStream buf = new ByteArrayOutputStream(); 65 | for (int result = gis.read(); result != -1; result = gis.read()) { 66 | buf.write((byte) result); 67 | } 68 | payloadJson = buf.toString(StandardCharsets.UTF_8); 69 | } 70 | } catch (IOException e) { 71 | Output.outputError("Could not gunzip JSON - " + e.getMessage(),log); 72 | builtSuccessfully = false; 73 | } catch (Exception e) { 74 | Output.outputError("Could not parse header - " + e.getMessage(),log); 75 | builtSuccessfully = false; 76 | } 77 | checkRegisteredClaims(payloadJson,log); 78 | } catch (NullPointerException e) { 79 | Output.outputError("The UTF-8 Charset isn't initialized (" + e.getMessage() + ")",log); 80 | builtSuccessfully = false; 81 | } 82 | signature = Base64.decodeBase64(parts[2]); 83 | } 84 | } 85 | 86 | private boolean isMinified(String json) { 87 | ObjectMapper objectMapper = new ObjectMapper(); 88 | try { 89 | JsonNode jsonNode = objectMapper.readValue(json, JsonNode.class); 90 | return jsonNode.toString().equals(json); 91 | } catch (JsonProcessingException ignored) { 92 | // ignored 93 | } 94 | return false; 95 | } 96 | 97 | public List getTimeClaimList() { 98 | return timeClaimList; 99 | } 100 | 101 | private void checkRegisteredClaims(String payloadJson, boolean log) { 102 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 103 | JsonObject object; 104 | try { 105 | object = Json.parse(payloadJson).asObject(); 106 | } catch (Exception e) { 107 | Output.output("Could not parse claims - " + e.getMessage(),log); 108 | return; 109 | } 110 | 111 | JsonValue exp = object.get("exp"); 112 | long curUT = System.currentTimeMillis() / 1000L; 113 | if (exp != null) { 114 | try { 115 | long expUT = getDateJSONValue(exp); 116 | java.util.Date time = new java.util.Date(expUT * 1000); 117 | String expDate = time.toString(); 118 | boolean expValid = expUT > curUT; 119 | timeClaimList.add(new TimeClaim("[exp] Expired", expDate, expUT, expValid, true)); 120 | } catch (Exception e) { 121 | Output.output("Could not parse claim (exp) - " + e.getMessage() + " - " + e.getCause()); 122 | } 123 | } 124 | 125 | JsonValue nbf = object.get("nbf"); 126 | if (nbf != null) { 127 | try { 128 | long nbfUT = getDateJSONValue(nbf); 129 | java.util.Date time = new java.util.Date(nbfUT * 1000); 130 | String nbfDate = time.toString(); 131 | boolean nbfValid = nbfUT <= curUT; 132 | timeClaimList.add(new TimeClaim("[nbf] Not before", nbfDate, nbfUT, nbfValid, true)); 133 | } catch (Exception e) { 134 | Output.output("Could not parse claim (nbf) - " + e.getMessage() + " - " + e.getCause()); 135 | } 136 | } 137 | 138 | JsonValue iat = object.get("iat"); 139 | if (iat != null) { 140 | try { 141 | long iatUT = getDateJSONValue(iat); 142 | java.util.Date time = new java.util.Date(iatUT * 1000); 143 | String iatDate = time.toString(); 144 | timeClaimList.add(new TimeClaim("[iat] Issued at ", iatDate, iatUT, true, false)); 145 | } catch (Exception e) { 146 | Output.output("Could not parse claim (iat) - " + e.getMessage() + " - " + e.getCause()); 147 | } 148 | } 149 | } 150 | 151 | private long getDateJSONValue(JsonValue jv) { 152 | long utL; 153 | try { 154 | utL = jv.asLong(); 155 | } catch (Exception e) { 156 | Double utD = jv.asDouble(); 157 | utL = utD.longValue(); 158 | } 159 | return utL; 160 | } 161 | 162 | public CustomJWToken(String headerJson, String payloadJson, String signatureB64) { 163 | this.headerJson = headerJson; 164 | this.payloadJson = payloadJson; 165 | this.signature = Base64.decodeBase64(signatureB64); 166 | } 167 | 168 | public String getHeaderJson() { 169 | return headerJson; 170 | } 171 | 172 | public String getPayloadJson() { 173 | return payloadJson; 174 | } 175 | 176 | public JsonNode getHeaderJsonNode() { 177 | ObjectMapper objectMapper = new ObjectMapper(); 178 | try { 179 | return objectMapper.readTree(getHeaderJson()); 180 | } catch (IOException e) { 181 | return null; 182 | } 183 | } 184 | 185 | public void calculateAndSetSignature(Algorithm algorithm) { 186 | if (jsonMinify(getHeaderJson()) != null && jsonMinify(getPayloadJson()) != null) { 187 | byte[] payloadBytes = b64(Objects.requireNonNull(jsonMinify(getPayloadJson()))).getBytes(StandardCharsets.UTF_8); 188 | byte[] headerBytes = b64(Objects.requireNonNull(jsonMinify(getHeaderJson()))).getBytes(StandardCharsets.UTF_8); 189 | signature = algorithm.sign(headerBytes, payloadBytes); 190 | } 191 | } 192 | 193 | private String jsonMinify(String json) { 194 | try { 195 | return new Minify().minify(json); 196 | } catch (Exception e) { 197 | Output.outputError("Could not minify json: " + e.getMessage()); 198 | return null; 199 | } 200 | } 201 | 202 | public String getToken() { 203 | if (jsonMinify(getHeaderJson()) != null && jsonMinify(getPayloadJson()) != null) { 204 | String content = String.format("%s.%s", b64(jsonMinify(getHeaderJson())), b64(jsonMinify((getPayloadJson())))); 205 | String signatureEncoded = Base64.encodeBase64URLSafeString(this.signature); 206 | return String.format("%s.%s", content, signatureEncoded); 207 | } 208 | Output.outputError("Could not get token as some parts are to be null"); 209 | return null; 210 | } 211 | 212 | private String b64(String input) { 213 | return Base64.encodeBase64URLSafeString(input.getBytes(StandardCharsets.UTF_8)); 214 | } 215 | 216 | public static boolean isValidJWT(String token, boolean log) { 217 | if (org.apache.commons.lang.StringUtils.countMatches(token, ".") != 2) { 218 | return false; 219 | } 220 | try { 221 | CustomJWToken cjwt = new CustomJWToken(token,log); 222 | if (!cjwt.isBuiltSuccessful()) { 223 | return false; 224 | } 225 | String tok = cjwt.getToken(); 226 | if (tok == null) { 227 | return false; 228 | } 229 | JWT.decode(tok); 230 | return true; 231 | } catch (Exception ignored) { 232 | // ignored 233 | } 234 | return false; 235 | } 236 | 237 | // Method copied from: 238 | // https://github.com/auth0/java-jwt/blob/9148ca20adf679721591e1d012b7c6b8c4913d75/lib/src/main/java/com/auth0/jwt/TokenUtils.java#L14 239 | // Cannot be reused, it's visibility is protected. 240 | static String[] splitToken(String token, boolean log) throws JWTDecodeException { 241 | String[] parts = token.split("\\."); 242 | if (parts.length == 2 && token.endsWith(".")) { 243 | // Tokens with alg='none' have empty String as Signature. 244 | parts = new String[] { parts[0], parts[1], "" }; 245 | } 246 | if (parts.length != 3) { 247 | if(log) { 248 | throw new JWTDecodeException(String.format("The token was expected to have 3 parts, but got %s.", parts.length)); 249 | } 250 | return new String[] {}; 251 | } 252 | return parts; 253 | } 254 | 255 | public CustomJWToken setHeaderJson(String headerJson) { 256 | this.headerJson = headerJson; 257 | return this; 258 | } 259 | 260 | public boolean isMinified() { 261 | return isMinified; 262 | } 263 | 264 | public String getAlgorithm() { 265 | String algorithm = ""; 266 | try { 267 | algorithm = getHeaderJsonNode().get("alg").asText(); 268 | } catch (Exception ignored) { 269 | // ignored 270 | } 271 | return algorithm; 272 | } 273 | 274 | public String getSignature() { 275 | return Base64.encodeBase64URLSafeString(this.signature); 276 | } 277 | 278 | public CustomJWToken setSignature(String signature) { 279 | this.signature = Base64.decodeBase64(signature); 280 | return this; 281 | } 282 | 283 | public boolean isBuiltSuccessful() { 284 | return builtSuccessfully; 285 | } 286 | 287 | } 288 | -------------------------------------------------------------------------------- /src/main/java/gui/JWTSuiteTab.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import java.awt.Color; 4 | import java.awt.Desktop; 5 | import java.awt.Dimension; 6 | import java.awt.Font; 7 | import java.awt.GridBagConstraints; 8 | import java.awt.GridBagLayout; 9 | import java.awt.Insets; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.Serial; 13 | 14 | import javax.swing.JButton; 15 | import javax.swing.JLabel; 16 | import javax.swing.JPanel; 17 | import javax.swing.JPopupMenu; 18 | import javax.swing.JTextArea; 19 | import javax.swing.SwingUtilities; 20 | import javax.swing.UIManager; 21 | import javax.swing.event.DocumentListener; 22 | import javax.swing.text.JTextComponent; 23 | 24 | import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; 25 | import org.fife.ui.rsyntaxtextarea.Style; 26 | import org.fife.ui.rsyntaxtextarea.SyntaxConstants; 27 | import org.fife.ui.rsyntaxtextarea.SyntaxScheme; 28 | import org.fife.ui.rsyntaxtextarea.Token; 29 | import org.fife.ui.rtextarea.RTextScrollPane; 30 | 31 | import app.helpers.Config; 32 | import burp.api.montoya.MontoyaApi; 33 | import model.JWTSuiteTabModel; 34 | import model.Strings; 35 | 36 | public class JWTSuiteTab extends JPanel { 37 | 38 | @Serial 39 | private static final long serialVersionUID = 1L; 40 | 41 | private JTextArea jwtInputField; 42 | private RSyntaxTextArea jwtOuputField; 43 | private JButton jwtSignatureButton; 44 | private JTextArea jwtKeyArea; 45 | private final JWTSuiteTabModel jwtSTM; 46 | private final RSyntaxTextAreaFactory rSyntaxTextAreaFactory; 47 | private JLabel lbRegisteredClaims; 48 | private JLabel lblExtendedVerificationInfo; 49 | private MontoyaApi api; 50 | private JLabel lblPasteJwtToken; 51 | private JLabel lblEnterSecret; 52 | private JLabel lblDecodedJwt; 53 | 54 | public JWTSuiteTab(JWTSuiteTabModel jwtSTM, RSyntaxTextAreaFactory rSyntaxTextAreaFactory, MontoyaApi api) { 55 | this.rSyntaxTextAreaFactory = rSyntaxTextAreaFactory; 56 | this.api = api; 57 | drawGui(); 58 | this.jwtSTM = jwtSTM; 59 | } 60 | 61 | public void updateSetView() { 62 | SwingUtilities.invokeLater(() -> { 63 | if (!jwtInputField.getText().equals(jwtSTM.getJwtInput())) { 64 | jwtInputField.setText(jwtSTM.getJwtInput()); 65 | } 66 | if (!jwtSignatureButton.getText().equals(jwtSTM.getVerificationLabel())) { 67 | jwtSignatureButton.setText(jwtSTM.getVerificationLabel()); 68 | } 69 | if (!jwtOuputField.getText().equals(jwtSTM.getJwtJSON())) { 70 | jwtOuputField.setText(jwtSTM.getJwtJSON()); 71 | } 72 | if (!jwtKeyArea.getText().equals(jwtSTM.getJwtKey())) { 73 | jwtKeyArea.setText(jwtSTM.getJwtKey()); 74 | } 75 | if (!jwtSignatureButton.getBackground().equals(jwtSTM.getJwtSignatureColor())) { 76 | jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); 77 | } 78 | if (jwtKeyArea.getText().equals("")) { 79 | jwtSTM.setJwtSignatureColor(new JButton().getBackground()); 80 | jwtSignatureButton.setBackground(jwtSTM.getJwtSignatureColor()); 81 | } 82 | lblExtendedVerificationInfo.setText(jwtSTM.getVerificationResult()); 83 | lbRegisteredClaims.setText(jwtSTM.getTimeClaimsAsText()); 84 | jwtOuputField.setCaretPosition(0); 85 | }); 86 | } 87 | 88 | public void registerDocumentListener(DocumentListener jwtInputListener, DocumentListener jwtKeyListener) { 89 | jwtInputField.getDocument().addDocumentListener(jwtInputListener); 90 | jwtKeyArea.getDocument().addDocumentListener(jwtKeyListener); 91 | } 92 | 93 | @Override 94 | public void updateUI() { 95 | if(api!=null) { 96 | SwingUtilities.invokeLater(() -> { 97 | Font currentFont = api.userInterface().currentDisplayFont(); 98 | lblPasteJwtToken.setFont(currentFont); 99 | lblEnterSecret.setFont(currentFont); 100 | lblDecodedJwt.setFont(currentFont); 101 | String lbRegClaimText = lbRegisteredClaims.getText(); 102 | lbRegisteredClaims.putClientProperty("html.disable", false); 103 | lbRegisteredClaims.setText("reinitializing needed for proper html display"); 104 | lbRegisteredClaims.setText(lbRegClaimText); 105 | jwtOuputField.setFont(currentFont); 106 | }); 107 | } 108 | } 109 | 110 | private void drawGui() { 111 | Font currentFont = api.userInterface().currentDisplayFont(); 112 | 113 | GridBagLayout gridBagLayout = new GridBagLayout(); 114 | gridBagLayout.columnWidths = new int[] { 10, 0, 0, 0 }; 115 | gridBagLayout.rowHeights = new int[] { 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 116 | gridBagLayout.columnWeights = new double[] { 0.0, 1.0, 0.0, Double.MIN_VALUE }; 117 | gridBagLayout.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, Double.MIN_VALUE }; 118 | setLayout(gridBagLayout); 119 | 120 | lblPasteJwtToken = new JLabel(Strings.ENTER_JWT); 121 | lblPasteJwtToken.setFont(currentFont); 122 | GridBagConstraints gbc_lblPasteJwtToken = new GridBagConstraints(); 123 | gbc_lblPasteJwtToken.anchor = GridBagConstraints.SOUTHWEST; 124 | gbc_lblPasteJwtToken.insets = new Insets(0, 0, 5, 5); 125 | gbc_lblPasteJwtToken.gridx = 1; 126 | gbc_lblPasteJwtToken.gridy = 1; 127 | add(lblPasteJwtToken, gbc_lblPasteJwtToken); 128 | 129 | JButton creditButton = new JButton("About"); 130 | creditButton.addActionListener(arg0 -> { 131 | JLabelLink jLabelLink = new JLabelLink(Strings.CREDIT_TITLE, 530, 640); 132 | 133 | jLabelLink.addText("

About JWT4B

JSON Web Tokens (also known as JWT4B) is developed by Oussama Zgheb
"); 134 | jLabelLink.addURL("Mantainer Website", "zgheb.com"); 135 | jLabelLink.addURL("GitHub Repository", "github.com/ozzi/JWT4B"); 136 | jLabelLink.addText("
"); 137 | jLabelLink.addText("JWT4B, excluding the libraries mentioned below and the Burp extender classes, uses the GPL 3 license."); 138 | jLabelLink.addURL("· RSyntaxTextArea", 139 | "github.com/bobbylight/RSyntaxTextArea"); 140 | jLabelLink.addURL("· Auth0 -java-jwt", "github.com/auth0/java-jwt"); 141 | jLabelLink.addURL("· Apache Commons Lang", "apache.org"); 142 | jLabelLink.addText("
"); 143 | jLabelLink.addText("Thanks to:
· Compass for providing development time for the initial version
"); 144 | jLabelLink.addURL("  compass-security.com
", "compass-security.com"); 145 | jLabelLink.addText("· Swiss Re for providing time for me to maintain JWT4B
"); 146 | jLabelLink.addURL("  swissre.com
", "swissre.com"); 147 | jLabelLink.addText("· Brainloop for providing broader token support"); 148 | jLabelLink.addURL("  brainloop.com
", "brainloop.com"); 149 | jLabelLink.addText("· Cyrill for the help porting JWT4B to the Montaya API"); 150 | jLabelLink.addURL("  github.com/bcyrill
", "github.com/bcyrill"); 151 | jLabelLink.addText("· All the other great contributors on GitHub"); 152 | jLabelLink.addLogoImage(); 153 | }); 154 | GridBagConstraints gbc_creditButton = new GridBagConstraints(); 155 | gbc_creditButton.insets = new Insets(0, 0, 5, 0); 156 | gbc_creditButton.gridx = 2; 157 | gbc_creditButton.gridy = 1; 158 | gbc_creditButton.fill = GridBagConstraints.HORIZONTAL; 159 | add(creditButton, gbc_creditButton); 160 | 161 | JButton configButton = new JButton("Change Config"); 162 | configButton.addActionListener(arg0 -> { 163 | File file = new File(Config.configPath); 164 | Desktop desktop = Desktop.getDesktop(); 165 | try { 166 | desktop.open(file); 167 | } catch (IOException e) { 168 | System.err.println("Error using Desktop API - " + e.getMessage() + " - " + e.getCause()); 169 | } 170 | }); 171 | 172 | GridBagConstraints gbc_configButton = new GridBagConstraints(); 173 | gbc_configButton.insets = new Insets(0, 0, 5, 0); 174 | gbc_configButton.gridx = 2; 175 | gbc_configButton.gridy = 2; 176 | gbc_configButton.fill = GridBagConstraints.HORIZONTAL; 177 | add(configButton, gbc_configButton); 178 | 179 | jwtInputField = new JTextArea(); 180 | jwtInputField.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); 181 | jwtInputField.setRows(2); 182 | jwtInputField.setLineWrap(true); 183 | jwtInputField.setWrapStyleWord(true); 184 | 185 | GridBagConstraints gbc_jwtInputField = new GridBagConstraints(); 186 | gbc_jwtInputField.insets = new Insets(0, 0, 5, 5); 187 | gbc_jwtInputField.fill = GridBagConstraints.BOTH; 188 | gbc_jwtInputField.gridx = 1; 189 | gbc_jwtInputField.gridy = 2; 190 | add(jwtInputField, gbc_jwtInputField); 191 | 192 | lblEnterSecret = new JLabel(Strings.ENTER_SECRET_KEY); 193 | lblEnterSecret.setFont(currentFont); 194 | GridBagConstraints gbc_lblEnterSecret = new GridBagConstraints(); 195 | gbc_lblEnterSecret.anchor = GridBagConstraints.WEST; 196 | gbc_lblEnterSecret.insets = new Insets(0, 0, 5, 5); 197 | gbc_lblEnterSecret.gridx = 1; 198 | gbc_lblEnterSecret.gridy = 3; 199 | add(lblEnterSecret, gbc_lblEnterSecret); 200 | 201 | jwtKeyArea = new JTextArea(); 202 | jwtKeyArea.setBorder(UIManager.getLookAndFeel().getDefaults().getBorder("TextField.border")); 203 | GridBagConstraints gbc_jwtKeyField = new GridBagConstraints(); 204 | gbc_jwtKeyField.insets = new Insets(0, 0, 5, 5); 205 | gbc_jwtKeyField.fill = GridBagConstraints.HORIZONTAL; 206 | gbc_jwtKeyField.gridx = 1; 207 | gbc_jwtKeyField.gridy = 4; 208 | add(jwtKeyArea, gbc_jwtKeyField); 209 | jwtKeyArea.setColumns(10); 210 | 211 | jwtSignatureButton = new JButton(""); 212 | Dimension preferredSize = new Dimension(400, 30); 213 | jwtSignatureButton.setPreferredSize(preferredSize); 214 | 215 | GridBagConstraints gbc_jwtSignatureButton = new GridBagConstraints(); 216 | gbc_jwtSignatureButton.insets = new Insets(0, 0, 5, 5); 217 | gbc_jwtSignatureButton.gridx = 1; 218 | gbc_jwtSignatureButton.gridy = 6; 219 | add(jwtSignatureButton, gbc_jwtSignatureButton); 220 | 221 | GridBagConstraints gbc_jwtOuputField = new GridBagConstraints(); 222 | gbc_jwtOuputField.insets = new Insets(0, 0, 5, 5); 223 | gbc_jwtOuputField.fill = GridBagConstraints.BOTH; 224 | gbc_jwtOuputField.gridx = 1; 225 | gbc_jwtOuputField.gridy = 9; 226 | 227 | JTextComponent.removeKeymap("RTextAreaKeymap"); 228 | jwtOuputField = rSyntaxTextAreaFactory.rSyntaxTextArea(); 229 | UIManager.put("RSyntaxTextAreaUI.actionMap", null); 230 | UIManager.put("RSyntaxTextAreaUI.inputMap", null); 231 | UIManager.put("RTextAreaUI.actionMap", null); 232 | UIManager.put("RTextAreaUI.inputMap", null); 233 | jwtOuputField.setWhitespaceVisible(true); 234 | 235 | SyntaxScheme scheme = jwtOuputField.getSyntaxScheme(); 236 | Style style = new Style(); 237 | style.foreground = new Color(222, 133, 10); 238 | scheme.setStyle(Token.LITERAL_STRING_DOUBLE_QUOTE, style); 239 | jwtOuputField.revalidate(); 240 | jwtOuputField.setHighlightCurrentLine(false); 241 | jwtOuputField.setCurrentLineHighlightColor(Color.WHITE); 242 | jwtOuputField.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT); 243 | jwtOuputField.setEditable(false); 244 | // no context menu on right-click 245 | jwtOuputField.setPopupMenu(new JPopupMenu()); 246 | 247 | // hopefully fixing: 248 | // java.lang.ClassCastException: class 249 | // javax.swing.plaf.nimbus.DerivedColor$UIResource cannot be cast to class 250 | // java.lang.Boolean (javax.swing.plaf.nimbus.DerivedColor$UIResource is in 251 | // module java.desktop of loader 'bootstrap'; 252 | // java.lang.Boolean is in module java.base of loader 'bootstrap') 253 | SwingUtilities.invokeLater(() -> { 254 | RTextScrollPane sp = new RTextScrollPane(jwtOuputField); 255 | sp.setLineNumbersEnabled(false); 256 | add(sp, gbc_jwtOuputField); 257 | }); 258 | 259 | lblExtendedVerificationInfo = new JLabel(""); 260 | GridBagConstraints gbc_lblExtendedVerificationInfo = new GridBagConstraints(); 261 | gbc_lblExtendedVerificationInfo.insets = new Insets(0, 0, 5, 5); 262 | gbc_lblExtendedVerificationInfo.gridx = 1; 263 | gbc_lblExtendedVerificationInfo.gridy = 7; 264 | add(lblExtendedVerificationInfo, gbc_lblExtendedVerificationInfo); 265 | 266 | lblDecodedJwt = new JLabel(Strings.DECODED_JWT); 267 | lblDecodedJwt.setFont(currentFont); 268 | GridBagConstraints gbc_lblDecodedJwt = new GridBagConstraints(); 269 | gbc_lblDecodedJwt.anchor = GridBagConstraints.WEST; 270 | gbc_lblDecodedJwt.insets = new Insets(0, 0, 5, 5); 271 | gbc_lblDecodedJwt.gridx = 1; 272 | gbc_lblDecodedJwt.gridy = 8; 273 | add(lblDecodedJwt, gbc_lblDecodedJwt); 274 | 275 | 276 | lbRegisteredClaims = new JLabel(); 277 | lbRegisteredClaims.putClientProperty("html.disable", false); 278 | lbRegisteredClaims.setBackground(new Color(238, 238, 238)); 279 | GridBagConstraints gbc_lbRegisteredClaims = new GridBagConstraints(); 280 | gbc_lbRegisteredClaims.fill = GridBagConstraints.BOTH; 281 | gbc_lbRegisteredClaims.insets = new Insets(0, 0, 5, 5); 282 | gbc_lbRegisteredClaims.gridx = 1; 283 | gbc_lbRegisteredClaims.gridy = 11; 284 | add(lbRegisteredClaims, gbc_lbRegisteredClaims); 285 | } 286 | 287 | 288 | public String getJWTInput() { 289 | return jwtInputField.getText(); 290 | } 291 | 292 | public String getKeyInput() { 293 | return jwtKeyArea.getText(); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/main/java/gui/JLabelLink.java: -------------------------------------------------------------------------------- 1 | package gui; 2 | 3 | import java.awt.Cursor; 4 | import java.awt.Desktop; 5 | import java.awt.event.MouseAdapter; 6 | import java.awt.event.MouseEvent; 7 | import java.awt.image.BufferedImage; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.IOException; 10 | import java.io.Serial; 11 | import java.net.URI; 12 | import java.util.Base64; 13 | import java.util.Objects; 14 | 15 | import javax.imageio.ImageIO; 16 | import javax.swing.BoxLayout; 17 | import javax.swing.ImageIcon; 18 | import javax.swing.JFrame; 19 | import javax.swing.JLabel; 20 | import javax.swing.JPanel; 21 | import javax.swing.WindowConstants; 22 | import javax.swing.border.EmptyBorder; 23 | 24 | import app.helpers.Output; 25 | 26 | public class JLabelLink extends JFrame { 27 | @Serial 28 | private static final long serialVersionUID = 1L; 29 | 30 | private static final String LOGO_DATA = "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"; 31 | private static final Base64.Decoder DECODER = Base64.getDecoder(); 32 | private final JPanel pan; 33 | 34 | public JLabelLink(String title, int x, int y) { 35 | this.setTitle(title); 36 | this.setSize(x, y); 37 | this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 38 | this.setBounds(0, 0, x, y); 39 | this.setLocationRelativeTo(null); 40 | this.setLocationRelativeTo(null); 41 | 42 | pan = new JPanel(); 43 | pan.setBorder(new EmptyBorder(10, 10, 10, 10)); 44 | BoxLayout boxLayout = new BoxLayout(pan, BoxLayout.Y_AXIS); 45 | pan.setLayout(boxLayout); 46 | 47 | this.setContentPane(pan); 48 | this.setVisible(true); 49 | } 50 | 51 | public void addURL(String content, String tooltip) { 52 | JLabel label = new JLabel("" + content + ""); 53 | label.putClientProperty("html.disable", null); 54 | label.setCursor(new Cursor(Cursor.HAND_CURSOR)); 55 | label.setToolTipText(tooltip); 56 | addMouseHandler(label); 57 | pan.add(label); 58 | } 59 | 60 | public void addText(String content) { 61 | JLabel label = new JLabel("" + content + ""); 62 | label.putClientProperty("html.disable", null); 63 | pan.add(label); 64 | } 65 | 66 | public void addLogoImage() { 67 | byte[] imageBytes = DECODER.decode(LOGO_DATA); 68 | try { 69 | BufferedImage img = ImageIO.read(new ByteArrayInputStream(imageBytes)); 70 | ImageIcon icon = new ImageIcon(img); 71 | JLabel label = new JLabel(); 72 | label.setIcon(icon); 73 | pan.add(label); 74 | } catch (IOException e) { 75 | Output.output("Could not load logo img - " + e.getMessage()); 76 | } 77 | } 78 | 79 | private void addMouseHandler(final JLabel website) { 80 | website.addMouseListener(new MouseAdapter() { 81 | 82 | @Override 83 | public void mouseClicked(MouseEvent e) { 84 | try { 85 | String href = parseHREF(website.getText()); 86 | Desktop.getDesktop().browse(new URI(Objects.requireNonNull(href))); 87 | } catch (Exception ex) { 88 | Output.outputError("Exception trying to browser from jlabel href - " + ex.getMessage()); 89 | } 90 | } 91 | }); 92 | } 93 | 94 | private static String parseHREF(String html) { 95 | String hrefMarker = "href=\""; 96 | int hrefLoc = html.indexOf(hrefMarker); 97 | if (hrefLoc > 1) { 98 | int hrefEndLoc = html.indexOf("\">"); 99 | if (hrefEndLoc > hrefLoc + 4) { 100 | return html.substring(hrefLoc + hrefMarker.length(), hrefEndLoc); 101 | } 102 | } 103 | return null; 104 | } 105 | 106 | } 107 | --------------------------------------------------------------------------------