In part 1 we discussed handcrafting Swing GUI items in a form. In this part, we will design a GUI using NetBeans and then convert it to Jython. Then use it in a Burp tab. Next, we will create a custom table model based on objects to handle our issues.
Code is at:
Open NetBeans and follow any tutorial to design a form. In this case, I am going to design something that looks like an issue tab.
Designing in NetBeans
- Open NetBeans.
File > Open Projectand point to the
- If the form is not opened, select it from the sidebar at
Source Packages > sample > SampleJFrame.java.
- Click on the
Designtab to view the form.
This is not a GUI design tutorial. You can use any tutorial on the web but Swing GUI design in NetBeans is pretty much the same as any other IDE (e.g., Visual C#). It's mostly drag and drop.
After you are satisfied with your design, click on the
Source tab beside
Design. You will see a Java file that has the generated code for the GUI. This
must be converted to Jython. The important part is the section after
@SuppressWarnings which is already folded in the editor with the label
initComponents sets up the GUI. Copy the contents of the
function into a separate file in your favorite editor and get converting.
For extensions in Java, the following guide by Paul Ritchie A.K.A. Corner Pirate on the Secarma Labs blog from October 2017 discusses designing Burp extension GUIs in NetBeans and then hooking them up in Burp. I have not tried it so I don't know if it works, but it's a good idea:
Converting to a JFrame
This is a simple process in this context. Because of our knowledge from part 1, we understand some of these words. This is good, because if something goes wrong, we can troubleshoot.
The conversion process was simple:
- Search for
javax.swing.and import everything.
- Then remove
- Add everything to the constructor of a new class.
The result is in
02-JFrame. If we add
extension.py to Burp, we will get a
getUiComponent we create an instance of the frame, display it and
NBFrame (stands for NetBeans frame) is a JFrame. Some items have
been commented out. I will discuss the important parts.
from javax.swing import (JScrollPane, JTable, JPanel, JTextField, JLabel, JTabbedPane, JComboBox, table, BorderFactory, GroupLayout, LayoutStyle, JFrame) class NBFrame(JFrame): """Represents the converted frame from NetBeans."""
JTable uses a model to populate the data and cells. We are using the DefaultTableModel. One of its constructors creates a model from a 2D array containing the data and a string array with the column names (or headers).
tableData = [ [None, None, None, None, None], [None, None, None, None, None], [None, None, None, None, None] ] tableColumns = ["#", "Issue Type/Name", "Severity", "Host", "Path"] # create the table model tableModel = table.DefaultTableModel(tableData, tableColumns)
The TableModel interface has methods for returning the column
getColumnClass) or if cells are editable (
isCellEditable) among other
things. The designer has created these because I
added types to columns. By default, they are all
are commented out in this version of the extension but we will use them later.
We are also setting up an auto sorter, this lets users sort the table by clicking on the columns. This is a neat idea but it will cause some headaches later.
# set the table model # if this fails, we have to use self.jTable1.setModel(tableModel) self.jTable1.setAutoCreateRowSorter(True) # wrap the table in a scrollpane self.jScrollPane1.setViewportView(self.jTable1)
The table might contain more rows than can be displayed in the GUI, so the designer has created a JScrollPane and assigned the table to it. This allows us to scroll to see all rows.
The rest of the auto-generated GUI code looked complex. The best I could do was convert it, run the extension and fix errors. The only interesting part was these lines:
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); getContentPane().setLayout(layout);
Because we are inside a JFrame, we can replace it with:
layout = GroupLayout(self.getContentPane()) self.getContentPane().setLayout(layout)
Note: In Python we can also pass
self instead of
in Java, passing
this will result in a "GroupLayout can only be used with one
container at a time" error. Instead we must to
Using a JPanel
We do not want a detached frame. We want our extension to be in the Burp tab. To
display our creation in a Burp tab we need to convert it to a panel. I tried
NBPanel class that inherits JPanel but it did not work out. It
messed up and was displayed on top of every tab.
The fix (might not be the correct fix but works in this case) is to create the
panel and assign it to a field of NBPanel (and not inherit anything). The result
# NBPanel is not inheriting anything. class NBPanel(): """Represents the converted frame from NetBeans."""
Now the part above with
# create the main panel self.panel = JPanel() layout = GroupLayout(self.panel) self.panel.setLayout(layout)
Inside our extension, we create an object and then return the panel.
The panel works:
Customizing the Table
The table is editable. This is useful but not here. I want to edit the rows in a different pop-up because data will have more rows than displayed in the table.
We need to create our own TableModel named
source resides in
__init__: We are calling the parent constructor.
isCellEditableallows us to decide which cell is editable. In this case, we are using a list with an element for each column. More detailed control is possible because we have both row and column. It's easier to just return
Falseto make everything uneditable.
getColumnClassreturns the class of each column.
Handling Mouse Events
We can detect when the table is clicked and act accordingly. This is done by
implementing the MouseListener interface and adding it to
the table. Our custom mouse listener is in the
It has two helper functions.
getClickedIndex returns the index number of the
item. Index is the first column so it detects which row is selected and then
returns the value of the first column. Note: This will be unreliable if we do
not disable column reordering. See below in the
def getClickedIndex(self, event): """Returns the value of the first column of the table row that was clicked. This is not the same as the row index because the table can be sorted.""" # get the event source, the table in this case. tbl = event.getSource() # get the clicked row row = tbl.getSelectedRow() # get the first value of clicked row return tbl.getValueAt(row, 0) # return event.getSource.getValueAt(event.getSource().getSelectedRow(), 0)
event.getSource() returns the object/component that sourced the event (the
table in this case). We can get the selected row (and also column with
getSelectedColumn). Then we return the first value of the row which is the
getClickedRow returns the data in the clicked row as a
def getClickedRow(self, event): """Returns the complete clicked row.""" tbl = event.getSource() return tbl.getModel().getDataVector().elementAt(tbl.getSelectedRow())
The interface has a few methods but we are only interested in
detect single and double-clicks. The following code prints the data in the row
after each click.
# event.getClickCount() returns the number of clicks. def mouseClicked(self, event): if event.getClickCount() == 1: # print "single-click. clicked index:", self.getClickedRow(event) # modify the items in the panel print "single-click: ", self.getClickedRow(event) if event.getClickCount() == 2: # open the dialog to edit print "double-click: ", self.getClickedRow(event)
For more information, please see the Java tutorial How to Write a Mouse Listener.
Finally, we have the actual table.
class IssueTable(JTable): """Issue table.""" def __init__(self, data, headers): # set the table model model = IssueTableModel(data, headers) self.setModel(model) self.setAutoCreateRowSorter(True) # disable the reordering of columns self.getTableHeader().setReorderingAllowed(False) # assign panel to a field self.addMouseListener(IssueTableMouseListener())
NBPanel we set the table:
# setting up the table # initial data in the table tableData = [ [3, "Issue3", "Severity3", "Host3", "Path3"], [1, "Issue1", "Severity1", "Host1", "Path1"], [2, "Issue2", "Severity2", "Host2", "Path2"], ] tableHeadings = ["#", "Issue Type/Name", "Severity", "Host", "Path"] from IssueTable import IssueTable self.jTable1 = IssueTable(tableData, tableHeadings) # wrap the table in a scrollpane self.jScrollPane1.setViewportView(self.jTable1)
The data is printed to console after clicking on each row.
Updating the Panel with The Selected Row
In this step, we want to update the read-only items in the panel such as the
name, host, and path with the data from the selected row in the table. You
probably have noticed that somewhere in between I changed the severity combobox
to a text box. This works better for read-only data. The code for this section is in
Updating the panel should be done inside
mouseClicked. But while we can get
the table through
event.getSource(), we do not have access to the main panel.
I guess with some trickery we could traverse the hierarchy and get it, but it
looks like too much work. Instead, I choose the simple way of passing a "global"
panel object between modules. This is not pythonic but does the job.
The panel class has been renamed to
MainPanel and resides in
(doh). At the end of the file, there is an instance of
# create "global" panel mainPanel = MainPanel()
This is then loaded in
extension.py and used.
IssueTable.py we do the same and use
This mostly works. After clicking each item, it's updated in the rest of the panel.
However, there is a problem. If we sort the table, the view changes and if we
click on any row, it will update it to the data that was there before sorting.
For example, in the gif, issue2 is the 3rd item. After we sort the table by the
#) column, it will be the second item (which was issue1 before). After
clicking on it, the panel information does not change to issue2. If we click on
the 3rd item (now issue3), the panel will be updated with issue2.
We can read about this behavior in the JTable Documentation:
All of JTables row based methods are in terms of the RowSorter, which is not necessarily the same as that of the underlying TableModel. For example, the selection is always in terms of JTable so that when using RowSorter you will need to convert using convertRowIndexToView or convertRowIndexToModel.
We need to pass the row from the table (
This fixes the issue.
Populating the Table from Outside
Hardcoding the table data in
MainPanel is not practical. My initial way to do
this was manually assigning something to the
jTable1 from inside
getUiComponent. See the code for this section in
# 06-UpdateTable/MainPanel.py self.jTable1 = IssueTable(None, None) # 06-UpdateTable/extension.py tableData = [ [3, "Issue3", "Severity3", "Host3", "Path3"], [1, "Issue1", "Severity1", "Host1", "Path1"], [2, "Issue2", "Severity2", "Host2", "Path2"], ] tableHeadings = ["#", "Issue Type/Name", "Severity", "Host", "Path"] from IssueTable import IssueTable mainPanel.jTable1 = IssueTable(tableData, tableHeadings) return mainPanel.panel
But it resulted in an empty table (no headings or data). The panel is already
created, so after adding the table I needed to do something else (maybe
redraw?). I decided I need to change the structure a bit. Hence, instead of
MainPanel.py, I would create the variable
and then create and assign it in
extension.py. I also decided to change the
burpPanel (less confusing).
The constructor for
MainPanel has also been modified to add an optional table
parameter. We are using this new table parameter to pass the IssueTable.
Let's also experiment with adding data to the table in real-time after the form
has been created. The
IssueTable class gets a new method:
Note that we are not checking the data for the correct length. Later, we need to create objects to pass to the table to display and add. After creating the table, we add a new row:
It works but to be sure, we want to add another row after we interact with the
tab in Burp. To do so, I am going to modify the double click section in the
mouseClicked method in
IssueTableMouseListener. When we double-click any
cell, a new row should be added to the table.
Works in real-time. In this gif, double-click on row 4 added a new row.
Revamping the TableModel
Up until now, our table is manual. It's about time we created an Issue object and used it in the table.
We now have a new object called
Issue (I would have preferred to call it a
finding but Burp calls them issues and do we). Issue has a bunch of fields and
not all fields are shown in the table.
You can ignore the type hints but they are useful when coding in IntelliJ IDEA (VS Code does not support Jython).
Requests and responses are generally byte arrays so we will store them in base64. Hence, we will have these extra methods to do it.
Which converts our constructor to:
IMessageEditors cannot be constructed normally. We have to use IBurpExtenderCallbacks.htmlcreateMessageEditor to get an instance. As a result, we need to pass an instance of callbacks to MainPanel via the constructor.
A small gotcha when adding the IMessageEditors to the tab. You need to call
getComponent on them to pass them as seen in the
# request tab self.panelRequest.setMessage("", True) self.tabIssue.addTab("Request", self.panelRequest.getComponent()) # response tab self.panelResponse.setMessage("", False) self.tabIssue.addTab("Response", self.panelResponse.getComponent())
The bulk of the change happens in the
is now extending the AbstractTableModel class. Looking
at the description, we only need to implement three methods. I followed this
guide from 2008 and
CustomLogger does the same:
In both examples, there is an underlying data structure which is an array of objects. The custom tablemodel manages adding and removing items. The table just displays them.
We can see the usual column names/classes. Then we have the
issues list which
will hold all of them and is populated in the constructor.
Next comes the three methods we need to implement after inheriting from
We are not displaying every
Issue field so
getValueAt only returns some of
them. This is where we decide which column displays what field and can do any
transformation that we want.
I added some extra utility methods like
removeIssue. There is a
setValueAt which is not needed here because our table is
The MouseListener is the same as before with some minor modifications mostly in
After every mouse click, we want to display everything in the bottom panel. Note
the difference in assignments to
panelResponse. We are
storing the requests in base64 inside the Issue object so we must use the helper
methods (that handle the encoding/decoding) to get them.
Double-click creates a new issue and adds it to the table.
What Did We Learn Here Today
We learned a lot of stuff and I think this is a good place to stop. In the next session, I will start by adding the edit functionality to the table. We will modify the double-click to create a new frame to edit the clicked issue and a new button to add new issues.