diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..3a5851a --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,84 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle +name: Java CI with Gradle + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up JDK 19 + uses: actions/setup-java@v3 + with: + distribution: 'oracle' + java-version: '19' + cache: 'gradle' + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build + - name: Creating the jar file + run: ./gradlew jar + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + path: ./releases/*.jar + name: Downloadable Extension File + - name: Get previous tag for the branch + id: get_previous_tag + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + PREFIX="MainBranch_" + else + PREFIX="NotMainBranch_" + fi + echo "PREFIX=$PREFIX" >> $GITHUB_ENV + PREVIOUS_TAG=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/git/refs/tags \ + | jq -r ".[] | select(.ref | startswith(\"refs/tags/${PREFIX}\")) | .ref" \ + | sort -V | tail -n 1 | sed "s|refs/tags/||") + + # Check if we found a tag + if [[ -z "$PREVIOUS_TAG" ]]; then + echo "No previous tag found for prefix ${PREFIX}." + echo "PREVIOUS_TAG=" >> $GITHUB_ENV + else + echo "PREVIOUS_TAG=$PREVIOUS_TAG" >> $GITHUB_ENV + fi + - name: Delete the previous tag + if: env.PREVIOUS_TAG != '' + run: | + gh release delete ${{ env.PREVIOUS_TAG }} -y + git push origin --delete ${{ env.PREVIOUS_TAG }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Extract new version + id: get_version + run: | + VERSION=$(grep "^version" build.gradle | awk -F\' '{print $2}' | tr -d \') + echo "VERSION=$VERSION" >> $GITHUB_ENV + - name: Set release tag based on branch + id: vars + run: | + if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then + echo "RELEASE_TAG=${PREFIX}${VERSION}" >> $GITHUB_ENV + else + echo "RELEASE_TAG=${PREFIX}${VERSION}" >> $GITHUB_ENV + fi + - name: Create Release using GitHub CLI + run: | + gh release create ${{ env.RELEASE_TAG }} ./releases/*.jar \ + --title "Release ${{ env.RELEASE_TAG }}" \ + --notes "This jar file has been built by GitHub automatically." \ + --repo ${{ github.repository }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 28c1259..d91958f 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,8 @@ gradle-app.setting # Intellij .idea + +/out/ +gradle-app.setting +.git +/releases/* \ No newline at end of file diff --git a/README.md b/README.md index 2dd9552..01d51c7 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,18 @@ A BurpSuite extension to aid pentesting web applications that use Blazor Server/ ## Build ### Prerequisites -- Install [Java 18](https://www.oracle.com/java/technologies/javase/jdk18-archive-downloads.html) on your building machine. +- Install [Java 19](https://www.oracle.com/java/technologies/javase/jdk19-archive-downloads.html) on your building machine. - Install [Gradle](https://gradle.org/install/) on your building machine. -- Ensure the `JAVA_HOME` environment variable is set to the JDK 18 path if you have multiple versions of Java installed. +- Ensure the `JAVA_HOME` environment variable is set to the JDK 19 path if you have multiple versions of Java installed. - _NOTE: This project requires Java 17+._ ### Build Steps 1. Clone the repository with `git clone https://github.com/AonCyberLabs/BlazorTrafficProcessor` 2. `cd BlazorTrafficProcessor` 3. `gradle build` -4. The built JAR file will be located at `BlazorTrafficProcessor/build/libs/BlazorTrafficProcessor-1.0.jar` +4. The built JAR file will be located at `./build/libs/` or `./releases/` + +Note: The latest build should be automatically compiled by GitHub workflows (Actions) ## Usage @@ -167,4 +169,14 @@ Deserialized: } ] ``` + +#### Contributors + +SignalR header support added by [@R4ML1N](https://github.com/R4ML1N) + +WebSocket support has been added by Soroush Dalili [@irsdl](https://github.com/irsdl) + + +#### Copyright + Copyright 2023 Aon plc diff --git a/build.gradle b/build.gradle index de31d94..3b1c114 100644 --- a/build.gradle +++ b/build.gradle @@ -4,33 +4,44 @@ plugins { } group 'com.gdssecurity' -version '1.0' -sourceCompatibility = 17 +version '1.1' +sourceCompatibility = 19 repositories { mavenCentral() } dependencies { - implementation 'org.msgpack:msgpack-core:0.9.3' - implementation 'net.portswigger.burp.extensions:montoya-api:2023.4' - implementation 'org.json:json:20220924' - implementation 'org.apache.parquet:parquet-common:1.12.3' + implementation 'org.msgpack:msgpack-core:0.9.6' + implementation 'net.portswigger.burp.extensions:montoya-api:2023.10.4' + implementation 'org.json:json:20230227' + implementation 'org.apache.parquet:parquet-common:1.13.1' implementation 'javax.xml.bind:jaxb-api:2.3.1' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' } test { - useJUnitPlatform() + //useJUnitPlatform() } -jar { +jar{ duplicatesStrategy = DuplicatesStrategy.EXCLUDE - manifest { - attributes "Main-Class": "com.gdssecurity.BlazorTrafficProcessor" - } + archivesBaseName = project.name from { - configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + (configurations.runtimeClasspath).collect { it.isDirectory() ? it : zipTree(it) } + }{ + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + exclude "META-INF/*.txt" } } + +tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" +} + +tasks.withType(Jar) { + destinationDirectory = file("$rootDir/releases/") +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..42defcc --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..79a61d4 100755 --- a/gradlew +++ b/gradlew @@ -55,7 +55,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -80,10 +80,10 @@ do esac done -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit - -APP_NAME="Gradle" +# This is normally unused +# shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' @@ -143,12 +143,16 @@ fi if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -205,6 +209,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..6689b85 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,8 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +41,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +76,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/com/gdssecurity/BlazorTrafficProcessor.java b/src/main/java/com/gdssecurity/BlazorTrafficProcessor.java index 2ead565..dc17f04 100644 --- a/src/main/java/com/gdssecurity/BlazorTrafficProcessor.java +++ b/src/main/java/com/gdssecurity/BlazorTrafficProcessor.java @@ -25,6 +25,7 @@ import com.gdssecurity.providers.BTPContextMenuItemsProvider; import com.gdssecurity.providers.BTPHttpRequestEditorProvider; import com.gdssecurity.providers.BTPHttpResponseEditorProvider; +import com.gdssecurity.providers.BTPWebSocketEditorProvider; import com.gdssecurity.views.BTPView; /** @@ -48,8 +49,11 @@ public void initialize(MontoyaApi api) { // Request/Response Editor Providers BTPHttpRequestEditorProvider requestEditorProvider = new BTPHttpRequestEditorProvider(this._montoya); BTPHttpResponseEditorProvider responseEditorProvider = new BTPHttpResponseEditorProvider(this._montoya); + BTPWebSocketEditorProvider webSocketEditorProvider = new BTPWebSocketEditorProvider(this._montoya); + this._montoya.userInterface().registerHttpRequestEditorProvider(requestEditorProvider); this._montoya.userInterface().registerHttpResponseEditorProvider(responseEditorProvider); + this._montoya.userInterface().registerWebSocketMessageEditorProvider(webSocketEditorProvider); // Request/Response Handlers (for Highlighting + Downgrade WS to HTTP) BTPHttpResponseHandler downgradeHandler = new BTPHttpResponseHandler(this._montoya); diff --git a/src/main/java/com/gdssecurity/MessageModel/DisplayErrorMessage.java b/src/main/java/com/gdssecurity/MessageModel/DisplayErrorMessage.java new file mode 100644 index 0000000..52b6e24 --- /dev/null +++ b/src/main/java/com/gdssecurity/MessageModel/DisplayErrorMessage.java @@ -0,0 +1,31 @@ +package com.gdssecurity.MessageModel; + +import burp.api.montoya.MontoyaApi; +import com.gdssecurity.helpers.BTPConstants; +import org.json.JSONObject; + +import java.io.IOException; + +public class DisplayErrorMessage extends GenericMessage { + public DisplayErrorMessage(String msg, MontoyaApi api) { + super(new JSONObject().put(BTPConstants.EXTENSION_NAME + " Error", msg), api); + } + public DisplayErrorMessage(JSONObject msg, MontoyaApi api) { + super(msg, api); + } + + @Override + boolean validateJson(JSONObject msg) { + return true; + } + + @Override + void initBlazorFromJson() throws IOException { + + } + + @Override + void initJsonFromMessage() throws IOException { + + } +} diff --git a/src/main/java/com/gdssecurity/MessageModel/GenericMessage.java b/src/main/java/com/gdssecurity/MessageModel/GenericMessage.java index ae01373..c6601c5 100644 --- a/src/main/java/com/gdssecurity/MessageModel/GenericMessage.java +++ b/src/main/java/com/gdssecurity/MessageModel/GenericMessage.java @@ -106,7 +106,10 @@ public String toJsonString() { * @return - a byte array containing the raw BlazorPack bytes */ public byte[] toBlazorBytes() { - return this.blazorMessage.toByteArray(); + if(this.blazorMessage != null) + return this.blazorMessage.toByteArray(); + else + return new byte[]{}; } } diff --git a/src/main/java/com/gdssecurity/editors/BTPHttpRequestEditor.java b/src/main/java/com/gdssecurity/editors/BTPHttpRequestEditor.java index 0c7e97f..9b25c31 100644 --- a/src/main/java/com/gdssecurity/editors/BTPHttpRequestEditor.java +++ b/src/main/java/com/gdssecurity/editors/BTPHttpRequestEditor.java @@ -132,24 +132,38 @@ public boolean isEnabledFor(HttpRequestResponse requestResponse) { if (requestResponse == null || requestResponse.request() == null) { return false; } + if (requestResponse.request().httpVersion() == null) { return false; } - if (requestResponse.url() == null) { + + if (requestResponse.request().url() == null) { return false; } - if (!this._montoya.scope().isInScope(requestResponse.url())) { + + if (!this._montoya.scope().isInScope(requestResponse.request().url())) { return false; } + if (requestResponse.request().contentType() == ContentType.JSON) { return false; } - if (!requestResponse.url().contains(BTPConstants.BLAZOR_URL)) { - return false; + + if (!requestResponse.request().url().contains(BTPConstants.BLAZOR_URL)) { + if (!requestResponse.request().hasHeader(BTPConstants.SIGNALR_HEADER)) { + return false; + } } + if (requestResponse.request().body() == null || requestResponse.request().body().length() == 0) { return false; } + + // Request during negotiation containing "{anything}\x1e", not valid blazor and BTP tab shouldn't be enabled + if (requestResponse.request().body().toString().startsWith("{") && requestResponse.request().body().toString().endsWith("}\u001E")) { + return false; + } + return true; } diff --git a/src/main/java/com/gdssecurity/editors/BTPHttpResponseEditor.java b/src/main/java/com/gdssecurity/editors/BTPHttpResponseEditor.java index ecd44e0..1e49e0a 100644 --- a/src/main/java/com/gdssecurity/editors/BTPHttpResponseEditor.java +++ b/src/main/java/com/gdssecurity/editors/BTPHttpResponseEditor.java @@ -102,28 +102,43 @@ public boolean isEnabledFor(HttpRequestResponse requestResponse) { if (requestResponse == null || requestResponse.response() == null) { return false; } + if (requestResponse.response().httpVersion() == null) { return false; } + if (this._montoya.scope() == null) { return false; } + if (requestResponse.response().httpVersion() == null) { return false; } - if (!requestResponse.url().contains(BTPConstants.BLAZOR_URL)) { - return false; + + if (!requestResponse.request().url().contains(BTPConstants.BLAZOR_URL)) { + if (!requestResponse.request().hasHeader(BTPConstants.SIGNALR_HEADER)) { + return false; + } } - if (!this._montoya.scope().isInScope(requestResponse.url())) { + + if (!this._montoya.scope().isInScope(requestResponse.request().url())) { return false; } + if (requestResponse.response().body() == null || requestResponse.response().body().length() == 0) { return false; } + // Response during negotiation containing "{}\x1e", not valid blazor and BTP tab shouldn't be enabled if ( requestResponse.response().body().length() == 3 && requestResponse.response().body().toString().startsWith("{}")) { return false; } + + // Response during negotiation containing "{anything}\x1e", not valid blazor and BTP tab shouldn't be enabled + if (requestResponse.response().body().toString().startsWith("{") && requestResponse.response().body().toString().endsWith("}\u001E")) { + return false; + } + return true; } diff --git a/src/main/java/com/gdssecurity/editors/BTPWebSocketEditor.java b/src/main/java/com/gdssecurity/editors/BTPWebSocketEditor.java new file mode 100644 index 0000000..352a56f --- /dev/null +++ b/src/main/java/com/gdssecurity/editors/BTPWebSocketEditor.java @@ -0,0 +1,172 @@ +package com.gdssecurity.editors; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.http.message.requests.HttpRequest; +import burp.api.montoya.logging.Logging; +import burp.api.montoya.ui.Selection; +import burp.api.montoya.ui.contextmenu.WebSocketMessage; +import burp.api.montoya.ui.editor.RawEditor; +import burp.api.montoya.ui.editor.extension.EditorMode; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedWebSocketMessageEditor; +import com.gdssecurity.MessageModel.GenericMessage; +import com.gdssecurity.helpers.BTPConstants; +import com.gdssecurity.helpers.BlazorHelper; +import org.json.JSONArray; +import org.json.JSONException; + +import java.awt.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +public class BTPWebSocketEditor implements ExtensionProvidedWebSocketMessageEditor { + + private MontoyaApi _montoya; + private WebSocketMessage webSocketMessage; + private RawEditor editor; + private BlazorHelper blazorHelper; + private Logging logging; + + public BTPWebSocketEditor(MontoyaApi api, EditorMode editorMode) { + this._montoya = api; + this.editor = this._montoya.userInterface().createRawEditor(); + this.blazorHelper = new BlazorHelper(this._montoya); + this.logging = this._montoya.logging(); + } + + /** + * @return The current message set in the editor as an instance of {@link ByteArray} + */ + @Override + public ByteArray getMessage() { + byte[] body; + if (this.editor.isModified()) { + body = this.editor.getContents().getBytes(); + } else { + body = this.webSocketMessage.payload().getBytes(); + } + if (body == null | body.length == 0) { + this.logging.logToError("[-] getMessage: The selected editor body is empty/null."); + return null; + } + JSONArray messages; + byte[] newBody; + try { + messages = new JSONArray(new String(body,StandardCharsets.UTF_8)); + newBody = this.blazorHelper.blazorPack(messages); + } catch (JSONException e) { + this.logging.logToError("[-] getMessage - JSONExcpetion while parsing JSON array: " + e.getMessage()); + return null; + } catch (Exception e) { + this.logging.logToError("[-] getMessage - Unexpected exception while getting the request: " + e.getMessage()); + return null; + } + return ByteArray.byteArray(newBody); + } + + /** + * Sets the provided {@link WebSocketMessage} within the editor component. + * + * @param message The message to set in the editor. + */ + @Override + public void setMessage(WebSocketMessage message) { + this.webSocketMessage = message; + byte[] body = webSocketMessage.payload().getBytes(); + ArrayList messages = this.blazorHelper.blazorUnpack(body); + ByteArrayOutputStream outstream = new ByteArrayOutputStream(); + try { + String jsonStrMessages = this.blazorHelper.messageArrayToString(messages); + outstream.write(jsonStrMessages.getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + this.logging.logToError("[-] setRequestResponse - IOException while writing bytes to buffer: " + e.getMessage()); + return; + } catch (JSONException e) { + this.logging.logToError("[-] setRequestResponse - JSONException while parsing JSON array: " + e.getMessage()); + return; + } catch (Exception e) { + this.logging.logToError("[-] setRequestResponse - Unexpected exception: " + e.getMessage()); + return; + } + this.editor.setContents(ByteArray.byteArray(outstream.toByteArray())); + } + + /** + * A check to determine if the Web Socket editor is enabled for a specific {@link WebSocketMessage} message + * + * @param message The {@link WebSocketMessage} to check. + * @return True if the Web Socket message editor is enabled for the provided message. + */ + @Override + public boolean isEnabledFor(WebSocketMessage message) { + if(message == null) + return false; + + HttpRequest upgradeRequest; + // Burp Suite bug workaround: + try{ + upgradeRequest = message.upgradeRequest(); + if(upgradeRequest.url()!=null){ + if (!upgradeRequest.isInScope()) { + return false; + } + + if (!upgradeRequest.url().contains(BTPConstants.BLAZOR_URL)) { + if (!upgradeRequest.hasHeader(BTPConstants.SIGNALR_HEADER)) { + return false; + } + } + } + }catch(Exception e){ + // let's pretend it is all nice and dandy! + this._montoya.logging().logToError("Ignored error in getting the upgradeRequest(): " + e.getMessage()); + } + + // Response during negotiation containing "{anything}\x1e", not valid blazor and BTP tab shouldn't be enabled + if (message.payload() != null && message.payload().toString().startsWith("{") && message.payload().toString().endsWith("}\u001E")) { + return false; + } + + if(message.payload() != null && !message.payload().toString().isEmpty()) + return true; + else + return false; + + } + + /** + * @return The caption located in the message editor tab header. + */ + @Override + public String caption() { + return BTPConstants.CAPTION; + } + + /** + * @return The component that is rendered within the message editor tab. + */ + @Override + public Component uiComponent() { + return this.editor.uiComponent(); + } + + /** + * The method should return {@code null} if no data has been selected. + * + * @return The data that is currently selected by the user. + */ + @Override + public Selection selectedData() { + return this.editor.selection().get(); + } + + /** + * @return True if the user has modified the current message within the editor. + */ + @Override + public boolean isModified() { + return this.editor.isModified(); + } +} diff --git a/src/main/java/com/gdssecurity/handlers/BTPHttpRequestHandler.java b/src/main/java/com/gdssecurity/handlers/BTPHttpRequestHandler.java index 6beff76..81ca022 100644 --- a/src/main/java/com/gdssecurity/handlers/BTPHttpRequestHandler.java +++ b/src/main/java/com/gdssecurity/handlers/BTPHttpRequestHandler.java @@ -22,6 +22,7 @@ import burp.api.montoya.proxy.http.ProxyRequestHandler; import burp.api.montoya.proxy.http.ProxyRequestReceivedAction; import burp.api.montoya.proxy.http.ProxyRequestToBeSentAction; +import com.gdssecurity.helpers.BTPConstants; /** * Class to handle highlighting requests that use BlazorPack @@ -51,6 +52,9 @@ public ProxyRequestReceivedAction handleRequestReceived(InterceptedRequest inter if (interceptedRequest.body().length() != 0 && interceptedRequest.path().contains("_blazor?id")) { interceptedRequest.annotations().setHighlightColor(HighlightColor.CYAN); } + if (interceptedRequest.hasHeader(BTPConstants.SIGNALR_HEADER)) { + interceptedRequest.annotations().setHighlightColor(HighlightColor.CYAN); + } return ProxyRequestReceivedAction.continueWith(interceptedRequest); } diff --git a/src/main/java/com/gdssecurity/handlers/BTPHttpResponseHandler.java b/src/main/java/com/gdssecurity/handlers/BTPHttpResponseHandler.java index 37d3fc3..25d5001 100644 --- a/src/main/java/com/gdssecurity/handlers/BTPHttpResponseHandler.java +++ b/src/main/java/com/gdssecurity/handlers/BTPHttpResponseHandler.java @@ -63,6 +63,16 @@ public ProxyResponseReceivedAction handleResponseReceived(InterceptedResponse in if (!interceptedResponse.initiatingRequest().url().contains(BTPConstants.NEGOTIATE_URL) || interceptedResponse.statedMimeType() != MimeType.JSON) { return ProxyResponseReceivedAction.continueWith(interceptedResponse); } + + + Boolean useWebSocket = this._montoya.persistence().preferences().getBoolean("use_websocket"); + if(useWebSocket == null) + useWebSocket = false; // default value + if(useWebSocket){ + // Do not downgrade if the user has selected to use WebSockets + return ProxyResponseReceivedAction.continueWith(interceptedResponse); + } + try { JSONObject body = new JSONObject(interceptedResponse.bodyToString()); if (body.has("availableTransports")) { diff --git a/src/main/java/com/gdssecurity/helpers/ArraySliceHelper.java b/src/main/java/com/gdssecurity/helpers/ArraySliceHelper.java index 46eb93a..de7e270 100644 --- a/src/main/java/com/gdssecurity/helpers/ArraySliceHelper.java +++ b/src/main/java/com/gdssecurity/helpers/ArraySliceHelper.java @@ -31,6 +31,11 @@ public static byte[] getArraySlice(byte[] array, int start, int end) { if (start >= end) { throw new IllegalArgumentException("Start index for array slice must be < end index."); } + + if (start < 0 || end > array.length) { + throw new IllegalArgumentException("Invalid indices for array slice: start=" + start + ", end=" + end); + } + byte[] slicedArray = new byte[end - start]; for (int i = 0; i < slicedArray.length; i++) { slicedArray[i] = array[start + i]; diff --git a/src/main/java/com/gdssecurity/helpers/BTPConstants.java b/src/main/java/com/gdssecurity/helpers/BTPConstants.java index f44b75d..2336225 100644 --- a/src/main/java/com/gdssecurity/helpers/BTPConstants.java +++ b/src/main/java/com/gdssecurity/helpers/BTPConstants.java @@ -28,14 +28,15 @@ public final class BTPConstants { public static final String CAPTION = "BTP"; public static final String SEND_TO_BTP_CAPTION = "Send body to BTP tab"; public static final String SEND_TO_INT_CAPTION = "Send to Intruder"; - public static final String LOADED_LOG_MSG = "[+] BTP v1.0 Extension loaded."; - public static final String UNLOADED_LOG_MSG = "[*] BTP v1.0 Extension unloaded."; + public static final String LOADED_LOG_MSG = "[+] BTP v1.1 Extension loaded."; + public static final String UNLOADED_LOG_MSG = "[*] BTP v1.1 Extension unloaded."; // Patterns and Regexes public static final String BLAZOR_URL = "_blazor?id="; public static final String NEGOTIATE_URL = "negotiate?negotiateVersion="; public static final Pattern BODY_OFFSET = Pattern.compile("(\r\n\r\n)"); public static final String HEX_FORMAT = "%02X"; + public static final String SIGNALR_HEADER = "X-Signalr-User-Agent"; // Downgrade Constants (WS -> HTTP) public static final String TRANSPORT_STR = "[{'transport':'ServerSentEvents','transferFormats':['Text']},{'transport':'LongPolling','transferFormats':['Text','Binary']}]"; diff --git a/src/main/java/com/gdssecurity/helpers/BlazorHelper.java b/src/main/java/com/gdssecurity/helpers/BlazorHelper.java index efd086b..a8fa1cc 100644 --- a/src/main/java/com/gdssecurity/helpers/BlazorHelper.java +++ b/src/main/java/com/gdssecurity/helpers/BlazorHelper.java @@ -97,27 +97,32 @@ public byte[] blazorPack(JSONArray blazorMessages) { */ public ArrayList blazorUnpack(byte[] blob) { ArrayList messages = new ArrayList<>(); - int blobIdx = 0; - int blobLength = blob.length; - while (blobIdx < blobLength) { - byte[] blobSlice = ArraySliceHelper.getArraySlice(blob, blobIdx, blobLength); - JSONObject varInt = null; - try { - varInt = VarIntHelper.extractVarInt(blobSlice); - } catch (IOException e) { - this.logging.logToError("[-] blazorUnpack - An IOException occurred while unpacking the provided blob: " + e.getMessage()); - return null; - } catch (Exception e) { - this.logging.logToError("[-] blazorUnpack - An unexpected exception occurred while unpacking the blob: " + e.getMessage()); - return null; + try{ + int blobIdx = 0; + int blobLength = blob.length; + while (blobIdx < blobLength) { + byte[] blobSlice = ArraySliceHelper.getArraySlice(blob, blobIdx, blobLength); + JSONObject varInt = null; + try { + varInt = VarIntHelper.extractVarInt(blobSlice); + } catch (IOException e) { + this.logging.logToError("[-] blazorUnpack - An IOException occurred while unpacking the provided blob: " + e.getMessage()); + return null; + } catch (Exception e) { + this.logging.logToError("[-] blazorUnpack - An unexpected exception occurred while unpacking the blob: " + e.getMessage()); + return null; + } + int bytesRead = varInt.getInt("bytesRead"); + int msgSize = varInt.getInt("result"); + byte[] messageBytes = ArraySliceHelper.getArraySlice(blob, blobIdx + bytesRead, blobIdx + bytesRead + msgSize); + GenericMessage msg = initializeMessage(messageBytes); + messages.add(msg); + blobIdx += bytesRead + msgSize; } - int bytesRead = varInt.getInt("bytesRead"); - int msgSize = varInt.getInt("result"); - byte[] messageBytes = ArraySliceHelper.getArraySlice(blob, blobIdx + bytesRead, blobIdx + bytesRead + msgSize); - GenericMessage msg = initializeMessage(messageBytes); - messages.add(msg); - blobIdx += bytesRead + msgSize; + }catch(Exception e){ + messages.add(new DisplayErrorMessage("Message is incomplete or incompatible", _montoya)); } + return messages; } @@ -139,7 +144,7 @@ public int getBodyOffset(byte[] data) { * @param raw - a byte array containing the blazorpack bytes * @return an instantiated message object */ - public GenericMessage initializeMessage(byte[] raw) { + private GenericMessage initializeMessage(byte[] raw) { MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(raw); try { int arrayHeader = unpacker.unpackArrayHeader(); diff --git a/src/main/java/com/gdssecurity/helpers/VarIntHelper.java b/src/main/java/com/gdssecurity/helpers/VarIntHelper.java index cddaf0d..e64f717 100644 --- a/src/main/java/com/gdssecurity/helpers/VarIntHelper.java +++ b/src/main/java/com/gdssecurity/helpers/VarIntHelper.java @@ -18,7 +18,10 @@ import org.apache.parquet.bytes.BytesUtils; import org.json.JSONObject; -import java.io.*; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; /** * Helper class for Variable-Size Integer (VarInt) Parsing diff --git a/src/main/java/com/gdssecurity/providers/BTPContextMenuItemsProvider.java b/src/main/java/com/gdssecurity/providers/BTPContextMenuItemsProvider.java index 546b3e0..b4fd79b 100644 --- a/src/main/java/com/gdssecurity/providers/BTPContextMenuItemsProvider.java +++ b/src/main/java/com/gdssecurity/providers/BTPContextMenuItemsProvider.java @@ -20,9 +20,11 @@ import burp.api.montoya.logging.Logging; import burp.api.montoya.ui.contextmenu.ContextMenuEvent; import burp.api.montoya.ui.contextmenu.ContextMenuItemsProvider; +import burp.api.montoya.ui.contextmenu.WebSocketContextMenuEvent; +import burp.api.montoya.ui.contextmenu.WebSocketMessage; +import com.gdssecurity.helpers.BTPConstants; import com.gdssecurity.helpers.BlazorHelper; import com.gdssecurity.views.BTPView; -import com.gdssecurity.helpers.BTPConstants; import javax.swing.*; import java.awt.*; @@ -77,15 +79,43 @@ public List provideMenuItems(ContextMenuEvent event) { return menuItems; } + /** + * Gets called by Burpsuite since this is a registered menu items provider + * @param event This object can be queried to find out about HTTP websocket that are associated with the context menu invocation. + * + * @return - an arraylist of components to include in the right-click menu + */ + public List provideMenuItems(WebSocketContextMenuEvent event) { + ArrayList menuItems = new ArrayList<>(); + + // Send to BTP tab for ad-hoc serialization + JMenuItem sendToBTP = new JMenuItem(); + sendToBTP.setText(BTPConstants.SEND_TO_BTP_CAPTION); + sendToBTP.addActionListener(e -> { + WebSocketMessage selection; + + if(event.selectedWebSocketMessages().isEmpty() && event.messageEditorWebSocket().isPresent()){ + selection = event.messageEditorWebSocket().get().webSocketMessage(); + }else{ // Selected on WS history + selection = event.selectedWebSocketMessages().get(0); + } + + this.sendSelectionToBTP(selection); + }); + menuItems.add(sendToBTP); + return menuItems; + } /** * Handles the selection of "Send body to BTP tab" menu option * Sends the body from the selected request/response to editor of BTP tab * @param selection - the selected HttpRequestResponse object */ private void sendSelectionToBTP(HttpRequestResponse selection) { - if (selection.url() != null && !selection.url().contains(BTPConstants.BLAZOR_URL)) { - this._logging.logToError("[-] sendSelectionToBTP - Selected message is not BlazorPack."); - return; + if (selection.request().url() != null && !selection.request().url().contains(BTPConstants.BLAZOR_URL)) { + if (!selection.request().hasHeader(BTPConstants.SIGNALR_HEADER)) { + this._logging.logToError("[-] sendSelectionToBTP - Selected message is not BlazorPack."); + return; + } } if (selection.request() != null && selection.request().body() != null && selection.request().body().length() != 0) { this.btpTab.setEditorText(selection.request().body()); @@ -93,4 +123,16 @@ private void sendSelectionToBTP(HttpRequestResponse selection) { this.btpTab.setEditorText(selection.response().body()); } } + + private void sendSelectionToBTP(WebSocketMessage selection) { + if (selection.upgradeRequest().url() != null && !selection.upgradeRequest().url().contains(BTPConstants.BLAZOR_URL)) { + if (!selection.upgradeRequest().hasHeader(BTPConstants.SIGNALR_HEADER)) { + this._logging.logToError("[-] sendSelectionToBTP - Selected message is not BlazorPack."); + return; + } + } + + this.btpTab.setEditorText(selection.payload()); + } + } diff --git a/src/main/java/com/gdssecurity/providers/BTPHttpRequestEditorProvider.java b/src/main/java/com/gdssecurity/providers/BTPHttpRequestEditorProvider.java index ed5c9db..f3d4c30 100644 --- a/src/main/java/com/gdssecurity/providers/BTPHttpRequestEditorProvider.java +++ b/src/main/java/com/gdssecurity/providers/BTPHttpRequestEditorProvider.java @@ -17,8 +17,8 @@ import burp.api.montoya.MontoyaApi; import burp.api.montoya.ui.editor.extension.EditorCreationContext; -import burp.api.montoya.ui.editor.extension.HttpRequestEditorProvider; import burp.api.montoya.ui.editor.extension.ExtensionProvidedHttpRequestEditor; +import burp.api.montoya.ui.editor.extension.HttpRequestEditorProvider; import com.gdssecurity.editors.BTPHttpRequestEditor; /** diff --git a/src/main/java/com/gdssecurity/providers/BTPWebSocketEditorProvider.java b/src/main/java/com/gdssecurity/providers/BTPWebSocketEditorProvider.java new file mode 100644 index 0000000..0a1a675 --- /dev/null +++ b/src/main/java/com/gdssecurity/providers/BTPWebSocketEditorProvider.java @@ -0,0 +1,32 @@ +package com.gdssecurity.providers; + +import burp.api.montoya.MontoyaApi; +import burp.api.montoya.ui.editor.extension.EditorCreationContext; +import burp.api.montoya.ui.editor.extension.ExtensionProvidedWebSocketMessageEditor; +import burp.api.montoya.ui.editor.extension.WebSocketMessageEditorProvider; +import com.gdssecurity.editors.BTPWebSocketEditor; + +public class BTPWebSocketEditorProvider implements WebSocketMessageEditorProvider { + + private MontoyaApi _montoya; + + + /** + * Construct a BTPHttpResponseEditorProvider + * @param api - an instance of the Montoya API + */ + public BTPWebSocketEditorProvider(MontoyaApi api) { + this._montoya = api; + } + + /** + * Invoked by Burp when a new Web Socket message editor is required from the extension. + * + * @param creationContext details about the context that is requiring a message editor + * @return An instance of {@link ExtensionProvidedWebSocketMessageEditor} + */ + @Override + public ExtensionProvidedWebSocketMessageEditor provideMessageEditor(EditorCreationContext creationContext) { + return new BTPWebSocketEditor(this._montoya, creationContext.editorMode()); + } +} diff --git a/src/main/java/com/gdssecurity/views/BTPView.java b/src/main/java/com/gdssecurity/views/BTPView.java index 716b8be..4c9941e 100644 --- a/src/main/java/com/gdssecurity/views/BTPView.java +++ b/src/main/java/com/gdssecurity/views/BTPView.java @@ -45,6 +45,7 @@ public class BTPView extends JComponent { private String buttonText; private JComboBox dropDownMenu; private BlazorHelper blazorHelper; + private JCheckBox useWebSocketCheckBox; private final int DESERIALIZE_IDX = 0; private final int SERIALIZE_IDX = 1; @@ -97,6 +98,16 @@ public BTPView(MontoyaApi montoyaApi) { }); this.buttonView.add(this.clearButton); + this.useWebSocketCheckBox = new JCheckBox("Use WebSocket"); + Boolean useWebSocket = this._montoya.persistence().preferences().getBoolean("use_websocket"); + if(useWebSocket == null) + useWebSocket = false; // default value + this.useWebSocketCheckBox.setSelected(useWebSocket); + this.useWebSocketCheckBox.addActionListener(e -> { + this._montoya.persistence().preferences().setBoolean("use_websocket", this.useWebSocketCheckBox.isSelected()); + }); + this.buttonView.add(this.useWebSocketCheckBox); + // Add the button view to main UI component this.topLevel.add(this.buttonView, BorderLayout.NORTH);