Hackerman's Hacking Tutorials

The knowledge of anything, since all things have causes, is not acquired or complete unless it is known by its causes. - Avicenna

Nov 26, 2019 - 10 minute read - Comments - Burp Burp extension

Swing in Python Burp Extensions - Part 3 - Tips and Tricks

In the two previous parts, we learned about Jython Swing. Those blogs take a lot of time to write. I think each of them took around 10 hours. I do not want to spend that kind of time but I still want to document what I have learned.

In this blog I will write tips and tricks with a small code snippet instead of creating a complete extension.

Did I tell you I release Bug Diaries, it's a Python Burp extension that aims to bring Burp issues to the community version. It's pretty neat.

See Error Messages in Spawned Frames/Dialogs

When creating new JFrames or JDialogs, you might not get the crash or error message in Burp. I am not sure why (probably has something to do with either Swing threads [swing is not thread safe] or because stdout/stderr is not defined).

For example, I would create a new dialog/frame and spawning it would not do anything or print any error messages to console. It was pretty hard to figure out what had gone wrong.

For example, in Java:

public void display() {
    try {
        this.frm.setVisible(true);
    } catch (Exception e) {
        MainDiary.printOutput(e.toString());
    }
}

Solution: Put the .setVisible(True) statement (or other statements) in try/except (Python) and try/catch blocks.

Save and Load Extension Configuration

You can save extension specific configuration items in Burp and later load them for reuse. You can use it to persist extension options between Burp sessions. We can use two IBurpExtenderCallbacks methods:

For example, to save the last used directory:

try:
    # get the last used directory from extension settings.
    # it might not exist so we are doing it in a try block.
    lastDir = callbacks.loadExtensionSetting("lastuseddir")
catch:
    pass

# open a dialog at "lastDir".
openFileDialog(lastDir)

# assume newDir is the directory the last opened file.

# save the last used directory
callbacks.saveExtensionSetting("lastuseddir", newDir)

Create a Context Menu

Step 1: BurpExtener should inherit IContextMenuFactory

IContextMenuFactory is the interface to create a context menu.

class BurpExtender(IBurpExtender, IContextMenuFactory):

Step 2: Register the Context Menu

Use registerContextMenuFactory inside registerExtenderCallbacks.

class BurpExtender(IBurpExtender, ITab, IContextMenuFactory):

    def registerExtenderCallbacks(self, callbacks):
        # ...

        callbacks.registerContextMenuFactory(self)

Step 3: Implement the IContextMenuFactory Interface

Create the createMenuItems method inside the BurpExtender class. It is called when the context menu is selected in Burp.

class BurpExtender(IBurpExtender, IContextMenuFactory):

    def registerExtenderCallbacks(self, callbacks):
        # ...

        callbacks.registerContextMenuFactory(self)
    
    def createMenuItems(self, invocation):
        # do something

Step 4: Create The Menu Items

Inside createMenuItems, make one or more menu items of type JMenuItem. Add them to a java.util.ArrayList. Return the ArrayList.

def createMenuItems(self, invocation):
    from javax.swing import JMenuItem
    menuItem1 = JMenuItem("Label for menu item 1")
    menuItem2 = JMenuItem("Label for menu item 2")
    # add action listeners to menuitems (see below)
    
    from java.util import ArrayList
    menuArray = ArrayList()
    menuArray.add(menuItem1)
    menuArray.add(menuItem2)

    return menuArray

Step 5: Configure Item Click ActionListeners

These are the actions that are executed after clicking each menu item. It can be done in two ways:

Method 1: Directly assign a method to the actionPerformed field of JMenuItem. The method takes one parameter which is an event of type ActionEvent.

This has the disadvantage of not having direct access to the invocation (more on it later), but it's simple and quick.

  • Note 1: Directly assigning to actionPerformed is not possible in Java.
  • Note 2: This can be done with other Swing elements in Jython. For example, a button's action can be assigned:

    • btn1 = JButton("Button Label", actionPerformed=buttonAction)

      class BurpExtender(IBurpExtender, IContextMenuFactory):
          def registerExtenderCallbacks(self, callbacks):
              # ...
      
          def actionMenu1(self, event):
              # do something when menu item 1 is clicked
              jitem = event.getSource()
                  
          def createMenuItems(self, invocation):
              from javax.swing import JMenuItem
              menuItem1 = JMenuItem("Label for menu item 1",
                  actionPerformed=actionMenu1)
              menuItem2 = JMenuItem("Label for menu item 2")
              # add action listeners to menuitems (see below)
                      
              from java.util import ArrayList
              menuArray = ArrayList()
              menuArray.add(menuItem1)
              menuArray.add(menuItem2)
      
              return menuArray

The most useful method inside the function is event.getSource(). getSource returns the Swing element that was the source of the event. In this case, we get the menu item that was clicked.

Method 2: Create a class that inherits java.awt.event.ActionListener. Inside the class we put the action inside an actionPerformed method similar to the above.

Then we add this class with addActionListener to the menu item.

This method has the advantage of being able to get the invocation through a constructor. See this example:

class BurpExtender(IBurpExtender, ITab, IContextMenuFactory):
    # ...

    def createMenuItems(self, invocation):
        """Called when a context menu is invoked in Burp."""
        from javax.swing import JMenuItem
        menuItem1 = JMenuItem("Label for menu item 1")
        menuItem2 = JMenuItem("Label for menu item 2")

        # create an instance of the custom ActionListener and pass the
        # invocation as a parameter to the constructor
        contextMenuListener = ContextMenuListener(invocation)
        # add the instance as an actionlistener
        customMenuItem.addActionListener(contextMenuListener)

        from java.util import ArrayList
        menuArray = ArrayList()
        menuArray.add(menuItem1)
        menuArray.add(menuItem2)
        return menuArray

    # this class is defined before BurpExtender but showing it before helps

class ContextMenuListener(ActionListener):
    """ActionListener for the Burp context menu."""
    def __init__(self, invocation):
        # storing the invocation
        self.invocation = invocation

    def actionPerformed(self, event):
        """Invoked when the context menu item is selected."""
        # now we can use both the event and the invocation

Step 6: Invocation

The invocation is of type IContextMenuInvocation and contains a lot of useful methods for figuring out what was clicked and getting information.

  • getInvocationContext returns a byte that can be used to figure out the menu item was clicked in which Burp context. For example, CONTEXT_PROXY_HISTORY is 6. See all of them at burp.IContextMenuInvocation enums.
  • getSelectedMessages() is useful for getting the message(s) that were selected when the item was clicked. This useful when the extension wants to do something with the message(s). For example, sending them to a different tab.
    • Note: This method returns an array of IHttpRequestResponse.

      def actionPerformed(self, event):
          """Invoked when the context menu item is selected."""
          # now we can use both the event and the invocation
          requestResponses = self.invocation.getSelectedMessages()
          for reqResp in requestResponses:
              request = reqResp.getRequest()
              # note: not every message has a response
              response = reqResp.getResponse()
              # https://portswigger.net/burp/extender/api/burp/IHttpService.html#getHost()
              host = reqResp.getHttpService().getHost()
              # do somethings with these
  • getToolFlag() returns the tool flag. For example, TOOL_PROXY is 4.

JTextArea Sizing

JTextArea by default does not have word wrap and can take over all of the GUI if it gets too big.

textArea = JTextArea("some initial text")
# set to readonly
textArea.editable = False
# textArea.setEditable(false) in Java

# enable linewrap
textArea.setLineWrap(True)
# set linewrap to word
textArea.setWrapStyleWord(True)

To enable scrollbars and have a fixed size textarea, put it in a JScrollPane.

js = JScrollPane(textArea)

"GroupLayout can only be used with one container at a time" Error

When spawning a new JFrame or JPanel, I would get an error. Before putting the error in a try/catch block, it would not spawn. After that I would get this error "GroupLayout can only be used with one container at a time."

When using classes that had inherited JFrames or JPanels, it would work but when I would assign it to a field. And this would mostly happen in Java.

Solution: GroupLayout should be assigned to the frame or panel's "content pane." Content pane is the layer that holds all the objects together.

// Inside the class
private JFrame frm;

// Inside initializeComponent() [which is called by the constructor]
frm = new JFrame();

// Later when assigning the GroupLayout
GroupLayout layout = new GroupLayout(this.frm.getContentPane());
this.frm.getContentPane().setLayout(layout);

The following code compiles but will result in the "GroupLayout can only be used ..." error.

GroupLayout layout = new GroupLayout(this.frm);
this.frm.setLayout(layout);

IMessageEditor

  • Create it:
    • callbacks.createMessageEditor([controller], [editable True/False])
  • Add it to the form with getComponent():
    • jTabbedPane1.addTab("Request", panelRequest.getComponent());
  • Set the message
    • self.panelRequest.setMessage([message in a byte array], [True if request, False if response])
  • docs:

Context Menu in IMessageEditor

To make this happen, you need to create an IMessageEditorController and assign it to the IMessageEditor during creation.

class MyController(IMessageEditorController):
    # needs to implement three methods
    def getHttpService():
        # ...
    def getRequest():
        # ...
    def getResponse():
        # ...

# ...
# creating the IMessageEditor in gui
callbacks.createMessageEditor(MyController, [editable True/False])

Now you can the context menu in your IMessageEditors. E.g., Send to Repeater.

Creating this interface is straightforward in simple extensions. For a good example, see the Custom Logger extension from Portswigger:

In this case, the extension implements this interface. The extension stores the selected IHttpRequestResponse in the log and returns its fields in these methods.

class BurpExtender(IBurpExtender, ITab, IHttpListener, IMessageEditorController, AbstractTableModel):
    # ...

    #
    # implement IMessageEditorController
    # this allows our request/response viewers to obtain details about the messages being displayed
    #
    
    def getHttpService(self):
        return self._currentlyDisplayedItem.getHttpService()

    def getRequest(self):
        return self._currentlyDisplayedItem.getRequest()

    def getResponse(self):
        return self._currentlyDisplayedItem.getResponse()

    # inside registerExtenderCallbacks
    def	registerExtenderCallbacks(self, callbacks):
        # ...
        # https://github.com/PortSwigger/custom-logger/blob/master/python/CustomLogger.py#L45
        # tabs with request/response viewers
        tabs = JTabbedPane()
        self._requestViewer = callbacks.createMessageEditor(self, False)
        self._responseViewer = callbacks.createMessageEditor(self, False)

In my extension Bug Diaries, it was a nightmare. I had to implement IHttpRequestResponse, IHttpService and this interface. It did not work out in the end. I will update this section if I figure it out.

Select the Current Text When Control is Selected

When tabbing between controls that have text (e.g., JTextField, JTextArea), we want the current selection. This allows us to replace the default text with new items without having to select it manually. This is done with setting the selection start index to 0.

  • In Python:
    • textField1.setSelectionStart(0)
    • textField.selectionstart = 0
  • In Java:
    • textField1.setSelectionStart(0)

Print to Console

Normal print statements do the job in the main thread. print in Python and System.out.println in Java. But when I was printing in other Swing components (e.g., a spawned JFrame), it did not work. callbacks.getStdout() returns a java.io.OutputStream which is a pain to print too.

Instead use callbacks.printOutput and callbacks.printError. Create functions like this and use them using callbacks.

/**
* Prints the String s to standard output.
* @param s The String to be printed.
*/
public static void printOutput(String s) {
    callbacks.printOutput(s);
}

/**
* Prints the String s to standard error.
* @param s The String to be printed.
*/
public static void printError(String s) {
    callbacks.printError(s);
}

Equivalent of Jython toString() is __repr()__ in Python

When creating a class, you might want to create a toString() method. The equivalent of it in Python is not __str()__ but instead __repr()__.

Source: https://stackoverflow.com/a/6950800

Adding Objects to a JComboBox

It's done with JComboBox.addItem(). To decide what is displayed in the combobox for each item, the class needs to have toString() method in Java and __repr()__ in Python

For example, if we want to add objects from the Issue class to the combobox and display the issue name.

class Issue():
    # fields
    self.name
    self.host

    def __repr__(self):
        return self.name

# later they are added to the combobox
cBox = JComboBox()
for issue in issues:
    cBox.addItem(issue)

Assign Fields to a Python Object

Python stores an object field in a dictionary. Let's say you have a dictionary of items and you want to assign them as fields of an object:

d = {"name": "name1", "path": "example.net"}

obj1 = SomeClass()
obj1.__dict__.update(d)

update keeps any existing attributes that are not in the new dictionary. To overwrite those, assign it like this obj1.__dict__ = d

Converting an Object with Object Fields to JSON

json.dump(s) do not work on objects that have other objects as fields. It works if the object only has primitive types.

class Obj1():
    # field
    self.name # string
    self.host # string

    def toJSON():
        return json.dumps(self.__dict, indent=2)

But if Obj1 has another class as a field. We get an error that the field is not serializable. We add a customJSON method to Obj1 and other classes that are fields. This method returns the fields as a dictionary.

class Obj():
    # field
    self.name # string
    self.host # string
    self.child # type Obj2

    def customJSON(self):
        return dict(
            name = self.name,
            host = self.host,
            child = self.child
        )

class Obj2():
    # fields
    self.name # string
    # ...

    def customJSON(self):
        return dict(
            name = self.name
            # ...
        )

Next, we create an encoder of type json.JSONEncoder.

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if hasattr(obj,'customJSON'):
            return obj.customJSON()
        else:
            return json.JSONEncoder.default(self, obj)

This encoder checks if the class has a method called customJSON, if so, it will use it to return a dictionary. Otherwise, it will use the normal encoder.

We can convert an array of Obj to JSON:

def objsToJSON(self):
    from ComplexEncoder import ComplexEncoder
    return json.dumps([obj.customJSON() for obj in objects],
                            cls=ComplexEncoder, indent=2)