- Setting up the environment + Hello World
- Inspecting and tampering HTTP requests and responses
- Inspecting and tampering WebSocket messages
- Creating new tabs for processing HTTP requests and responses
- -> Adding new functionalities to the context menu (accessible by right-clicking)
- Adding new checks to Burp Suite Active and Passive Scanner – TBD
- Using the Collaborator in Burp Suite plugins – TBD
- … and much more!
Hi there!
Today we will see how to develop an extension that will add items to the context menu that opens with a right-click of the mouse. This type of plugin can be useful in many cases. One example is selecting part of a request or a response and using a plugin to perform some operation on it, such as encryption or decryption.
For this tutorial, we will use the same demo application as the previous article (see part 4 of this series) but we will solve our scenario in a different way, using a different type of plugin. Let’s have a quick recap. We are analyzing a mobile application that adds an encryption layer to the HTTP request and response bodies. The mobile application encrypts the body using AES before sending the request and decrypts the response body in the same way, as it is encrypted by the backend application. A full description of the scenario that includes the source code of the backend can be found in part 4.
Let’s start with the same encrypted request as the previous example:
In the previous example, we created a plugin that added a tab to the HTTP requests and responses containing a decrypted version of the HTTP message body. Now, instead, we will create a plugin that will add some functionalities accessible from the context menu, which can be invoked as follows:
First, let’s think about what we want our plugin to do. We want to highlight encrypted content, right-click, and ask our extension to decrypt it. Once decrypted, we can analyze the clear-text value and maybe modify it, and then before sending our request, we will highlight the decrypted value and ask our extension to encrypt it again.
So we will add two items to the context menu: “Encrypt” and “Decrypt”.
We also need to consider an important aspect, which is that these functions can be invoked by the user on both editable HTTP messages (e.g., requests in the Repeater or request/response intercepted by the Proxy) and non-editable messages (e.g., Proxy History or responses in the Repeater). In the first case, we can directly modify the highlighted content in-place; in the second case, the content is not modifiable. We will handle the latter case by creating a small popup with the result of our encryption/decryption routine, allowing its use even on non-editable HTTP messages.
As usual, we start from the Hello World plugin skeleton we wrote in the part 1 of the series.
package org.fd.montoyatutorial; import burp.api.montoya.BurpExtension; import burp.api.montoya.MontoyaApi; import burp.api.montoya.logging.Logging; public class ContextMenuExample implements BurpExtension { MontoyaApi api; Logging logging; @Override public void initialize(MontoyaApi api) { // Save a reference to the MontoyaApi object this.api = api; // api.logging() returns an object that we can use to print messages to stdout and stderr this.logging = api.logging(); // Set the name of the extension api.extension().setName("Montoya API tutorial - ContextMenuExample"); // Print a message to the stdout this.logging.logToOutput("*** Montoya API tutorial - ContextMenuExample loaded ***"); // TODO - Register our listeners } }
The plugin we need is of type ContextMenuItem and can be registered from the UserInterface object that we can get from the usual MontoyaApi (the object supplied as argument to the initialize function of the plugin).
To register our plugin we need to code an object that implements the interface ContextMenuItemsProvider and that will contain our encryption and decryption routines. The interface ContextMenuItemsProvider is documented as follows:
As we can see from the documentation, the ContextMenuItemsProvider interface has three different methods, each of which populates the context menu at different times. The first is called when the right mouse button is clicked on an audit issue, the second on an HTTP message, and the third on a WebSocket message. The three methods are defined as default: this means that a default implementation for the three methods is already provided in the interface definition. The purpose of this default implementation is to allow the user to redefine only the method(s) they are interested in, while without a default implementation they would have to implement all of them in order to compile the plugin. In this example we will give an implementation only to the second one, the one related to HTTP messages.
The skeleton of our object implementing the ContextMenuItemsProvider interface is the following one:
public class CustomContextMenuItemProvider implements ContextMenuItemsProvider { MontoyaApi api; Logging logging; public CustomContextMenuItemProvider(MontoyaApi api) { // Save a reference to the MontoyaApi object this.api = api; // Save a reference to the logging object of the MontoyaApi this.logging = api.logging(); // Save a reference to the Base64 utilities offere by the MontoyaApi this.base64Utils = api.utilities().base64Utils(); } @Override public List<Component> provideMenuItems(ContextMenuEvent event) { // Initialize an empty list that will contains our context menu entries List<Component> menuItems = new ArrayList<Component>(); // Add our entries return menuItems; } }
First, let’s add a method that will handle encryption and decryption (we can define it as static since it does not require instance fields of the object):
public static byte[] encryptDecrypt(int encryptionOrDecryption, byte[] data, Logging logging) { try { // Create a specific object containing the IV for encryption byte[] iv = HexFormat.of().parseHex(ivHex); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); // Create a specific object containing the key for encryption byte[] key = HexFormat.of().parseHex(keyHex); SecretKey SecKey = new SecretKeySpec(key, 0, key.length, "AES"); // Initialize our AER cipher Cipher aesCipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); aesCipher.init(encryptionOrDecryption, SecKey, ivParameterSpec); // Encrypt or decrypt the input data byte[] processedMessage = aesCipher.doFinal(data); return processedMessage; } catch (Exception e) { logging.logToError(e.toString()); return null; } }
Now let’s modify the provideMenuItems method, which is responsible for populating the context menu entries.
@Override public List<Component> provideMenuItems(ContextMenuEvent event) { // Initialize an empty list that will contains our context menu entries List<Component> menuItems = new ArrayList<Component>(); // Create the menu only if the menu has been created on a request/response object event.messageEditorRequestResponse().ifPresent(messageEditorReqRes -> { // Create the menu only if the request/response has a selected portion messageEditorReqRes.selectionOffsets().ifPresent(selectionOffset -> { // Get the HTTP message HttpRequestResponse reqRes = messageEditorReqRes.requestResponse(); // Necessary to understand if the context menu has been created on a request or on a response MessageEditorHttpRequestResponse.SelectionContext selectionContext = messageEditorReqRes.selectionContext(); // Create the "Decrypt" entry of the context menu JMenuItem decryptItem = new JMenuItem("Decrypt"); decryptItem.addActionListener(al -> { // Logic of our plugin (decryption) }); // Add the new items to the list we will return menuItems.add(decryptItem); }); }); return menuItems; }
The provideMenuItems method is provided with a ContextMenuEvent argument that contains the HTTP message, the necessary information to determine if any parts of the message are highlighted, and other contextual information.
Lines 8 and 11 may be difficult to read for those unfamiliar with Java lambda expressions, but they simply extract the HTTP message and the selection, and proceed in the method only if both are present (we are interested in encrypting/decrypting a part of the message, and fundamental requirements are that the user has clicked on an HTTP request or response and has selected a part of the content before clicking).
Next, we save the actual HTTP message and a piece of information called selectionContext, which will essentially help us understand if we are in the context of an HTTP request or response. After this, we create our graphical menu entries using the Java JMenuItem object and add an action listener on that, that is a block of code that will be executed when our entry is clicked (again defined using Java lambda expressions, which, once learned, make the code much more readable but can be quite tedious for those unfamiliar with them 🙂 ).
After this, we just need to write the code that will be executed when our entry is clicked, which is the code implementing the logic of our extension (the code includes only the button that decrypts, but the final extension will also have a very similar one responsible for encrypting):
// Create the "Decrypt" entry of the context menu JMenuItem decryptItem = new JMenuItem("Decrypt"); decryptItem.addActionListener(al -> { ByteArray requestResponseBytes; // Request if(selectionContext == MessageEditorHttpRequestResponse.SelectionContext.REQUEST) { requestResponseBytes = reqRes.request().toByteArray(); // Response } else { requestResponseBytes = reqRes.response().toByteArray(); } // Get the selected portion of the request/response in ByteArray ByteArray selectedBytes = requestResponseBytes.subArray(selectionOffset.startIndexInclusive(), selectionOffset.endIndexExclusive()); // Base64 decode the selected portion ByteArray decodedSelectedBytes = this.api.utilities().base64Utils().decode(selectedBytes); // Decrypt the selected portion byte[] decryptedMessage = encryptDecrypt(Cipher.DECRYPT_MODE,decodedSelectedBytes.getBytes(),logging); String decryptedMessageString = new String(decryptedMessage); // Create a new HTTP message that contains the decrypted value instead of the // selected portion of the message ByteArray editedRequestResponseBytes = requestResponseBytes.subArray(0,selectionOffset.startIndexInclusive()); editedRequestResponseBytes = editedRequestResponseBytes.withAppended(byteArray(decryptedMessage)); if(selectionOffset.endIndexExclusive()<requestResponseBytes.length()) editedRequestResponseBytes = editedRequestResponseBytes.withAppended(requestResponseBytes.subArray(selectionOffset.endIndexExclusive(),requestResponseBytes.length())); String editedRequestResponseString = editedRequestResponseBytes.toString(); // Try to replace the original HTTP message with the new one. This operation may fail if the // request/response is not editable (es. in the History of the Proxy) try { // Request if(selectionContext == MessageEditorHttpRequestResponse.SelectionContext.REQUEST) { messageEditorReqRes.setRequest(HttpRequest.httpRequest(editedRequestResponseBytes)); // Response } else { messageEditorReqRes.setResponse(HttpResponse.httpResponse(editedRequestResponseBytes)); } } catch (UnsupportedOperationException ex) { // If the request/response is not editable, an UnsupportedOperationException arises and // we print our edited message in a popup. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JTextArea ta = new JTextArea(20, 60); ta.setLineWrap(true); ta.setText(decryptedMessageString); JOptionPane.showMessageDialog(null, new JScrollPane(ta), "Edited message", JOptionPane.INFORMATION_MESSAGE); } }); } });
In lines 5 to 16, we extract the bytes of the HTTP request or response and the portion selected by the user, that is the portion we will decrypt with our extension. Next, from lines 18 to 23, we decode from Base64 (remember that our content is encrypted and encoded in Base64) using utilities provided by the Burp Suite API, and then we decrypt it using the previously defined encryptDecrypt method.
From lines 27 to 31, we build a new HTTP message that will contain the decrypted version of the content selected by the user instead of the encrypted one. To do this, we use the ByteArray class offered by the Burp Suite API, which provides numerous methods for operations on byte arrays, which are usually quite cumbersome in Java. With this class, we extract the portion of the original request before the start of the user-selected portion, add the decrypted value, and then add the portion of the original request that follows the selection.
From lines 35 to 45, we replace the request or response in the Burp object with our newly created version of the request/response. We need the try/catch block because this operation can fail if the user is working on a non-modifiable HTTP message (for example, one from the Proxy History or from a response in the Repeater). In this case, we cannot modify the original HTTP message, so instead, we create a popup with a Java object (JTextArea) that contains the result of our decryption operation (lines 49-61).
Before compiling our plugin we need to register our class as a “context menu item provider” in the initialize method of our extension, in order to have it called once the user click with the right button on Burp Suite:
@Override public void initialize(MontoyaApi api) { // Save a reference to the MontoyaApi object this.api = api; // api.logging() returns an object that we can use to print messages to stdout and stderr this.logging = api.logging(); // Set the name of the extension api.extension().setName("Montoya API tutorial - ContextMenuExample"); // Print a message to the stdout this.logging.logToOutput("*** Montoya API tutorial - ContextMenuExample loaded ***"); // Register our Context Menu Item Provider CustomContextMenuItemProvider customContextMenuItemProvider = new CustomContextMenuItemProvider(api); api.userInterface().registerContextMenuItemsProvider(customContextMenuItemProvider); }
After compiling our plugin (for detailed instructions, see part 1) and loading it into Burp Suite, we have our entry in the context menu that performs decryption of the content selected by the user:
We can do the same to decrypt the content of the response as well. However, since the response in the Repeater tool is non-editable, if everything works correctly, we should see the decrypted content in a popup.
We can now add a new entry to the context menu that will handle the encryption operation. The code will be completely analogous, but it will call the encryptDecrypt method in encryption mode instead of decryption. The code is the following (step-by-step explanation will be omitted as the method is almost identical to the previous one):
JMenuItem encryptItem = new JMenuItem("Encrypt"); encryptItem.addActionListener(al -> { ByteArray requestResponseBytes; // Request if(selectionContext == MessageEditorHttpRequestResponse.SelectionContext.REQUEST) { requestResponseBytes = reqRes.request().toByteArray(); // Response } else { requestResponseBytes = reqRes.response().toByteArray(); } // Get the selected portion of the request/response in ByteArray ByteArray selectedBytes = requestResponseBytes.subArray(selectionOffset.startIndexInclusive(), selectionOffset.endIndexExclusive()); // Encrypt the selected portion byte[] encryptedMessage = encryptDecrypt(Cipher.ENCRYPT_MODE,selectedBytes.getBytes(),logging); // Encode the encrypted value in Base64 ByteArray encodedMessage = this.api.utilities().base64Utils().encode(ByteArray.byteArray(encryptedMessage)); String encodedMessageString = encodedMessage.toString(); // Create a new HTTP message that contains the encrypted value instead of the // selected portion of the message ByteArray editedRequestResponseBytes = requestResponseBytes.subArray(0,selectionOffset.startIndexInclusive()); editedRequestResponseBytes = editedRequestResponseBytes.withAppended(encodedMessage); if(selectionOffset.endIndexExclusive()<requestResponseBytes.length()) editedRequestResponseBytes = editedRequestResponseBytes.withAppended(requestResponseBytes.subArray(selectionOffset.endIndexExclusive(),requestResponseBytes.length())); // Try to replace the original HTTP message with the new one. This operation may fail if the // request/response is not editable (es. in the History of the Proxy) try { // Request if (selectionContext == MessageEditorHttpRequestResponse.SelectionContext.REQUEST) { messageEditorReqRes.setRequest(HttpRequest.httpRequest(editedRequestResponseBytes)); // Response } else { messageEditorReqRes.setResponse(HttpResponse.httpResponse(editedRequestResponseBytes)); } } catch (UnsupportedOperationException ex) { // If the request/response is not editable, an UnsupportedOperationException arises and // we print our edited message in a popup. SwingUtilities.invokeLater(new Runnable() { @Override public void run() { JTextArea ta = new JTextArea(20, 60); ta.setLineWrap(true); ta.setText(encodedMessageString); JOptionPane.showMessageDialog(null, new JScrollPane(ta), "Edited message", JOptionPane.INFORMATION_MESSAGE); } }); } }); // Add the new items to the list we will return menuItems.add(encryptItem);
After a quick recompilation, we can also try the encryption operation. Let’s decrypt the content of the request as we did before, modify it, and try to encrypt it again with our new context menu entry.
We can then verify that the operation was successful by decrypting the content of the response with our “Decrypt” menu entry, as done previously:
And that’s all for today. In the next part, we will see how to extend the Burp Scanner to integrate our checks into both active and passive scanner.
As always, the complete code of the backend and of the plugins can be downloaded from my GitHub repository.
Cheers!