New Burp Suite Extension: BlazorTrafficProcessor

New Burp Suite Extension: BlazorTrafficProcessor
Cyber Labs

16 of 20

This insight is part 16 of 20 in this Collection.

July 20, 2023 16 mins

New Burp Suite Extension: BlazorTrafficProcessor

New Burp Suite Extension: BlazorTrafficProcessor

Pentesting web applications that use Blazor server comes with unique challenges, especially without tooling. In this post, we discuss why such challenges exist and provide a Burp Suite Extension to address them.

Introduction

During a web application assessment, we encountered ASP.NET’s “Blazor” server for the first time. After attempting a few basic test cases, we realized this was not like any app we’ve tested before. For starters, all the messages transmitted by the application included seemingly random binary characters. You can make out some strings within these messages, but most of the data is not easily readable (Figure 1).

Figure 1 – Example Blazor Message

Additionally, any attempts we made to tamper with or replay requests resulted in an error and the connection being renegotiated (Figure 2).

Figure 2 – Error After Replaying Message

These initial observations proved to be major hindrances for testing a Blazor server application: limited readability and the inability to tamper with data.

Blazor Basics

Before we dive into addressing these obstacles, we first need to cover some of the basics. Blazor is a framework that comes with a variety of hosting options: WebAssembly (WASM), Server-Side ASP.NET, and Native Client. For the purposes of this blog post, we’ll focus on server-side Blazor, which integrates with SignalR to send browser events and receive pre-rendered page updates. By default, Blazor server applications communicate via WebSockets, though other transports such as Long Polling over HTTP are available as well. Since the end goal here is a Burp Suite extension, we’ll need to use HTTP as the Burp Suite extender APIs previously had little to no WebSockets support.

Note: Since this project began, Portswigger has released newer versions of the Montoya APIs which offer better extension support for WS. Handling Blazor messages over WS is currently under development for the BlazorTrafficProcessor extension.

Forcing a Blazor application to use Long Polling is possible within the negotiation process. When a web browser connects to a Blazor server, the first request is a version negotiation. This specifies the version and transports that will be used by the application.

An example request would look like:

POST /_blazor/negotiate?negotiateVersion=1 HTTP/1.1
Content-Length: 0
[...]

The response is as follows:

HTTP/1.1 200 OK
Content-Length: 316
Content-Type: application/json
[...]

{"negotiateVersion":1,"connectionId":"******","connectionToken"
:"******","availableTransports":[{"transport":"WebSockets","transferFormats":["Text","Binary"]},{"transport":"ServerSentEvents","transferFormats":["Text"]},{"transport":"LongPolling","transferFormats":["Text","Binary"]}]}

Using a Burp match and replace rule, you can remove the WebSockets transport from the response, forcing the browser to fall back to use Long Polling over HTTP. The BlazorTrafficProcessor extension will automatically downgrade any Blazor connections from WS to Long Polling.

After modifying the response to exclude WebSockets the browser console will show a warning to indicate a WebSocket connection failed and Long Polling will be used instead.

Warning: Failed to connect via WebSockets, using the Long Polling fallback transport. This may be due to a VPN or proxy blocking the connection. To trouble shoot this, visit https://aka.ms/blazor-server-using-fallback-long-polling.

By forcing the application to use Long Polling, all Blazor traffic will now occur over HTTP which makes Blazor data more accessible to prospective Burp Suite extensions. However, we still don’t know how the Blazor messages are formatted.

We know that Blazor applications expect messages to be in a particular format and order, with any deviations resulting in an error. If we turn to the documentation, Microsoft identifies this format as MessagePack and outlines how it can be used in ASP.NET applications.

MessagePack

MessagePack is another serialization format used to package structured data, like JSON, XML, etc. The key difference with MessagePack is that it is binary in nature, meaning specific bytes are used to indicate the types and length of serialized data.

While Blazor server uses MessagePack, the traffic is specifically formatted according to Blazor’s own Hub Protocol specification. Therefore, generic MessagePack parsers like the Burp Suite MessagePack extension available from the BApp Store will not work with Blazor traffic. Take the following BlazorPack message for example.

Figure 3 – Example BlazorPack Message Bytes

If we use the MessagePack extension on this message, the result is just 25 and the rest of the message is ignored. This is because \x00-\x7f represent positive integers in the MessagePack specification. The extension sees the first \x19, converts it to the decimal value of 25, and fails to parse the rest of the message. We’ll need a customized MessagePack parser to properly read these messages.

Blazor messages are formatted according to the Hub Protocol specification.

([Length] [Body])([Length] [Body])...

Length is a variable-size integer representing the size of Body, which is the actual message. A variable-size integer is one that can occupy a varying number of bytes, depending on the integer’s value. For example, a small integer may occupy a single byte whereas a larger integer may occupy up to five bytes. This value is crucial for parsing messages accurately and is one of the reasons that our tampering attempts failed. If you modify a string to be a different length than the original message, you’d need to update the Length variable-size integer as well. Manually performing these calculations for every request you want to tamper with is tedious and inefficient.

For the Body field, there are different types of messages supported by Blazor (i.e., Invocation, StreamInvocation, Ping, etc.). However, while proxying traffic and testing a sample Blazor application, we rarely saw any other types of messages being used other than Invocation. This is where we’ll focus for an example message breakdown.

InvocationMessage Analysis

The predominant message type observed while testing Blazor applications is an InvocationMessage, used to render page updates and submit data.

Taking a look at the specification, we see that there is the following structure:

[1, Headers, InvocationId, Target, [Arguments], [StreamIds]]

  • 1 – the message type, InvocationMessage types are 1.
  • Headers – a map containing string key-value pairs. During testing of a sample Blazor app, this was observed to only be equal to a null/empty map.
  • InvocationId – this can either be NIL to indicate a lack of an invocation ID or a string that holds the value. Again, this was always NIL during testing.
  • Target – a string representing the backend function to call.
  • Arguments – an array of arguments to pass to that backend function.
  • StreamIds – an array of strings representing unique stream identifiers. Again, this was always NIL or non-existent in the messages observed whilst testing.

InvocationMessage Example and Byte Breakdown

The following request body was sent after modifying a text input field to be foobar:

®•€À·BeginInvokeDotNetFromJS•¡1À²DispatchEventAsyncÙx[{
"eventHandlerId":4,"eventName":"change","eventFieldInfo":{"componentId":20,"fieldValue":"foobar"}},{"value":"foobar"}]

Now, taking a look at the bytes:

\xae\x01 \x95 \x01 \x80 \xc0 \xb7 BeginInvokeDotNetFromJS \x95 \xa1 1 \xc0 \xb2 DispatchEventAsync \x01 \xd9\x78 [{"eventHandlerId":4,"eventName":"change","eventFieldInfo":{"componentId":20,"fieldValue":"foobar"}},{"value":"foobar"}]

  • \xae\x01 – 174   Variable-sized integer corresponding to the Length parameter for the Hub Protocol.
  • \x95 – 5   MessagePack array header representing the length of the whole InvocationMessage body (5).
  • \x01 – 1   First element in the InvocationMessage array corresponding to the message type (InvocationMessage = 1).
  • \x80 – 0   Second element in the InvocationMessage array indicating a map of length 0.
  • \xc0 – NIL   Third element in the InvocationMessage array corresponding to the InvocationID (NIL)
  • \xb7 – 23   MessagePack header corresponding to the length of the fourth element in the InvocationMessage (23 bytes).
  • BeginInvokeDotNetFromJS   Fourth element in the InvocationMessage array representing the Target value (BeginInvokeDotNetFromJS).
  • \x95 – 5   Fifth element in the InvocationMessage array corresponding to MessagePack array header length (5 bytes).
  • \xa1 – 1   MessagePack array header representing the size in bytes for the first element in the Arguments array (1 byte).
  • 1   Raw string representing the value of the first element in the Arguments array (1).
  • \xc0 – NIL   Second element in the Arguments array (NIL).
  • \xb2 – 18   MessagePack header corresponding to the length in bytes of the third element in the Arguments array (18 bytes).
  • DispatchEventAsync   Raw string representing the value of the third element in the Arguments array (DispatchEventAsync).
  • \x01 – 1   MessagePack integer representing the value of the fourth element in the Arguments array (1).
  • \xd9\x78 – 120   MessagePack header corresponding to the length in bytes of the fifth element in the Arguments array (120 bytes).
  • [{“eventHandlerId”:4,… MessagePack raw string corresponding to the value of the fifth element in the Arguments array ([{“eventHandlerId”:4,…)
  • There are no StreamIds in this message.

Each string in a Blazor message is preceded by a MessagePack string header to indicate the size of the string. This adds another layer of complexity to manually tampering with messages; not only do you have to update the Length variable-size integer, but you’d have to update the size headers for each tampered string as well.

BlazorTrafficProcessor (BTP) Burp Suite Extension

In summary, the two biggest obstacles when it comes to testing Blazor applications are:

  1. Readability: the serialized messages can be difficult to read by themselves. As shown above, this is due to bytes in MessagePack representing specific values or types, resulting in seemingly random data to the naked eye.
  2. While some data may be easily readable (i.e., the JSON array above containing user input), any attempt to modify these values will require modification to the preceding size bytes as well. This brings us to our next obstacle:

  3. Tampering Difficulties: because the messages are serialized according to the Hub Protocol specification, tampering with a value without accounting for the respective size bytes will result in a malformed MessagePack stream that the server can’t read. When this happens, the Blazor application will often error out and renegotiate your connection.

The BlazorTrafficProcessor (BTP) Burp Suite extension addresses both of these issues by providing testers with the ability to convert request bodies from BlazorPack to JSON and vice versa. JSON is more common in web applications and is easier to read due to being text-based rather than binary-based. By providing both deserialization and serialization functionality, testers can leverage this extension to capture and deserialize a request, modify the desired parameter(s), then serialize back to BlazorPack and submit the request.

Demo

The initial version of the extension has two primary features. The first of which is a BTP Request/Response tab that appears on every in-scope BlazorPack message with the ability to convert BlazorPack to JSON. The second is a BTP Burp Suite tab that provides a sandbox for users to serialize/deserialize BlazorPack messages at-will. Any requests/responses that contain BlazorPack serialized data will be highlighted in the HTTP History tab as Cyan.

Reading

The BTP tab will appear on every in-scope request and response in your Burp history that is serialized using BlazorPack.

Figure 4 – BTP Request Editor Tab

This tab simply displays the BlazorPack body converted into a JSON array of message objects.

Figure 5 – BTP Deserialization Example
Tampering

Within Blazor server applications, the JSInvokable attribute allows developers to expose DotNet functions to the client-side JavaScript. Take the following snippet for example:

[JSInvokable("CallMe")]
public static void hiddenFunc(String var)
{
    Console.WriteLine("Hidden function called!");
    Console.WriteLine(var);
}

This is a simple example that just logs some user input to the console, but there are no web pages that allow the user to call this function. Instead, the DotNet.invokeMethodAsync JavaScript function can be used (as outlined here).

Figure 6 – Invoke Hidden Function from Browser Console

After the function has been called from the browser, Burp captured the serialized message that was sent:

Figure 7 – Hidden Function Invocation Request

Sending the above payload to repeater and using the extension to deserialize yields the following:

Figure 8 – Deserialized Invocation Request

The input of foo is contained within a 1-element array, so let’s try tampering and replaying the request. For demo purposes, we’ll change the JSON payload in the BTP tab to include a newline to see if newlines get written to the console output:

[{"Target":"BeginInvokeDotNetFromJS","Headers":0,"Arguments":["3","BlazorServerTestApp","CallMe",0,["Line1\r\nLine2"]],"MessageType":1}]

Clicking on the Raw tab to serialize the data back into MessagePack yields:

Figure 9 – Reserialized Request with Custom Input

Upon sending the request, the response is a simple 200 OK:

HTTP/1.1 200 OK
Content-Length: 0
Connection: close
Content-Type: text/plain
Server: Kestrel

Important note about Blazor server Long Polling: sending any type of invocation POST request will not return data in the response. Instead, Long Polling keeps an open GET request that receives server updates as they become available. As such, if you send a request with malformed input that causes an error, you won’t see that error unless you look at subsequent GET requests.

Checking the console log and we see that the payload did in fact create two distinct lines with our input:

Figure 10 – Application Logs with Inserted Payload
Ad-Hoc Serialization & Deserialization

The BTP extension also includes a new tab added to Burp Suite, aptly named BTP. While you can copy and paste data into this tab, it is recommended to use the “Send body to BTP” right-click menu option (Figure 11) to ensure the whole message is copied appropriately. This feature applies to both requests and responses.

Figure 11 – Send to BTP Menu Item

The message will then appear in the BTP tab as shown below:

Figure 12 – BTP Tab Deserialization Example

This tab can be used for ad-hoc Blazor conversions with the following steps:

  1. Select the conversion type (JSON->Blazor or Blazor-JSON)
  2. Enter the message in the left-hand editor
  3. Click Serialize/Deserialize Button
  4. Results are shown in the right-hand editor

What are these “Target” values I keep seeing?

While testing Blazor server applications, you’re likely to run into various Target values such as: BeginInvokeDotNetFromJS, OnRenderCompleted, OnLocationChanged, etc. These functions themselves are not specific to the app that you’re testing. Rather, they are built-in to ASP.NET Core as part of ComponentHub and are used to facilitate communications between the frontend JavaScript and the backend .NET application. For example, OnLocationChanged is used to navigate between pages in the web application. The implementation of these functions can be found in the “aspnetcore” repository on GitHub here.

It is important to distinguish between what’s native and what’s custom in Blazor server applications, since your goal will likely be to find vulnerabilities in the app that you’re testing, not in Blazor itself. As such, focus your testing efforts on fields that contain your input (i.e., arguments to BeginInvokeDotNetFromJS) as opposed to normal Blazor operations (i.e., OnRenderCompleted or EndInvokeJSFromDotNet). With that said, security researchers can also utilize the BTP extension to test Blazor server itself since tampering inputs is now easier.

The following screenshot (Figure 13) displays a deserialized request that updates the value of a text box in the application to be asdf. The highlighted values are potential areas for tampering. The rest of the arguments are native to Blazor and tampering may cause a Blazor error, which would renegotiate your connection.

Figure 13 – Tamperable Values vs. Native Values

Consider a case where only client-side JavaScript is used to validate this input field. We could intercept the request, deserialize the message into JSON, enter a value that would otherwise fail validation, reserialize, and fire the request to bypass client-side input validation.

Further Research

Fuzzing

While this extension achieves the base proof-of-concept for a tool to process Blazor server traffic, a feature we believe would be valuable to testers is the ability to fuzz serialized inputs (similar to a Burp Suite intruder attack). However, application data is never returned in response to a POST request when using Long Polling. Instead, the client-side JavaScript polls the server with a GET request and passes an epoch timestamp. The response to these polling requests will contain app data and render updates, making it difficult to deduce which request caused an error or a specific response. For example, if you send a POST request that causes an error, the error would be returned to the next GET (“poll”) request. Sending multiple error-inducing requests could cause the application to return several errors in one response body or spread out across subsequent polling responses. With no clear way to map one input to one response/error, we decided it best to leave out fuzzing for the first iteration of this tool. We are still looking for effective ways to fuzz Blazor applications and a fuzzing feature is on the roadmap for the BlazorTrafficProcessor extension.

WebSockets

BTP is fully dependent on the ability to downgrade a Blazor server connection from WebSockets to Long Polling over HTTP. ASP.NET developers have considered removing Long Polling support altogether, which could render BTP unusable. Ultimately Long Polling was left in the framework due to “unforeseen effects” of removing it, though it remains possible that support will be discontinued in the future.

For BTP to be future proof, it will need WebSockets support eventually. However, Blazor messages can be distributed across different WebSocket frames with a max size of 4096 bytes per frame. In other words, a Blazor message could start in one WebSocket frame and end in another. With limited support for WebSockets in Burp Suite’s Montoya APIs, it was not plausible to include Blazor WebSocket parsing for the initial version of BTP. However, this feature is currently under development as there are new iterations of Burp Suite’s Montoya APIs being released frequently.

You can find the latest version of the extension, as well as setup and usage instructions at our GitHub Repository here: https://github.com/AonCyberLabs/BlazorTrafficProcessor

Thank you for reading and happy Blazor hunting!

Aon’s Thought Leader
  • Will Rabb
    Sr. Consultant, Security Testing, Cyber Solutions

About Cyber Solutions:

Aon’s Cyber Solutions offers holistic cyber risk management, unsurpassed investigative skills, and proprietary technologies to help clients uncover and quantify cyber risks, protect critical assets, and recover from cyber incidents.

General Disclaimer

This material has been prepared for informational purposes only and should not be relied on for any other purpose. You should consult with your own professional advisors or IT specialists before implementing any recommendation or following the guidance provided herein. Further, the information provided and the statements expressed are not intended to address the circumstances of any particular individual or entity. Although we endeavor to provide accurate and timely information and use sources that we consider reliable, there can be no guarantee that such information is accurate as of the date it is received or that it will continue to be accurate in the future.

Terms of Use

The contents herein may not be reproduced, reused, reprinted or redistributed without the expressed written consent of Aon, unless otherwise authorized by Aon. To use information contained herein, please write to our team.

More Like This

View All
Subscribe CTA Banner