diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..767531c --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java' +} + +sourceCompatibility = '1.8' +targetCompatibility = '1.8' + +version = '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + // Add Burp Suite API dependency + compileOnly 'net.portswigger.burp.extender:burp-extender-api:2.3' +} + +jar { + // Ensure META-INF/MANIFEST.MF is created with the correct Main-Class + manifest { + attributes( + 'Implementation-Title': 'Timeinator', + 'Implementation-Version': version, + 'Main-Class': 'burp.BurpExtender' + ) + } + + // Include all dependencies in the JAR + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + // Exclude signature files from dependencies + exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA' +} + +// Create a task to copy the built JAR to a specific directory (optional) +task copyJarToBurp(type: Copy, dependsOn: jar) { + from jar.outputs.files.singleFile + // Change this path to match your Burp Suite extensions directory + into "${System.getProperty('user.home')}/BurpSuite/extensions" +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5a16e13 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'timeinator' \ No newline at end of file diff --git a/src/main/java/burp/BurpExtender.java b/src/main/java/burp/BurpExtender.java new file mode 100644 index 0000000..0d9311d --- /dev/null +++ b/src/main/java/burp/BurpExtender.java @@ -0,0 +1,544 @@ +package burp; + +import javax.swing.*; +import javax.swing.table.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.util.*; +import java.util.List; + +public class BurpExtender implements IBurpExtender, ITab, IContextMenuFactory, IMessageEditorController { + private IBurpExtenderCallbacks callbacks; + private IExtensionHelpers helpers; + private JTabbedPane tabbedPane; + private IMessageEditor messageEditor; + private JTextField hostTextField; + private JTextField portTextField; + private JCheckBox protocolCheckBox; + private JTextArea payloadTextArea; + private JTextField requestsNumTextField; + private JProgressBar progressBar; + private ResultsTableModel resultsTableModel; + private IHttpService httpService; + private byte[] request; + private IHttpRequestResponse[] contextMenuData; + + private static final String EXTENSION_NAME = "Timeinator"; + private static final String[] COLUMNS = { + "Payload", "Number of Requests", "Status Code", "Length (B)", "Body (B)", + "Minimum (ms)", "Maximum (ms)", "Mean (ms)", "Median (ms)", "StdDev (ms)" + }; + + @Override + public void registerExtenderCallbacks(IBurpExtenderCallbacks callbacks) { + this.callbacks = callbacks; + this.helpers = callbacks.getHelpers(); + + callbacks.setExtensionName(EXTENSION_NAME); + callbacks.registerContextMenuFactory(this); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + messageEditor = callbacks.createMessageEditor(BurpExtender.this, true); + + Insets insets = new Insets(3, 3, 3, 3); + JPanel attackPanel = constructAttackPanel(insets, messageEditor.getComponent()); + JPanel resultsPanel = constructResultsPanel(insets); + JPanel aboutPanel = constructAboutPanel(insets); + + tabbedPane = new JTabbedPane(); + tabbedPane.addTab("Attack", attackPanel); + tabbedPane.addTab("Results", resultsPanel); + tabbedPane.addTab("About", aboutPanel); + + callbacks.addSuiteTab(BurpExtender.this); + } + }); + } + + @Override + public String getTabCaption() { + return EXTENSION_NAME; + } + + @Override + public Component getUiComponent() { + return tabbedPane; + } + + @Override + public IHttpService getHttpService() { + updateClassFromUI(); + return httpService; + } + + @Override + public byte[] getRequest() { + updateClassFromUI(); + return request; + } + + @Override + public byte[] getResponse() { + return null; + } + + @Override + public List createMenuItems(IContextMenuInvocation invocation) { + IHttpRequestResponse[] messages = invocation.getSelectedMessages(); + + if (messages != null && messages.length == 1) { + contextMenuData = messages; + JMenuItem menuItem = new JMenuItem("Send to " + EXTENSION_NAME); + menuItem.addActionListener(e -> contextMenuItemClicked()); + return Collections.singletonList(menuItem); + } + return null; + } + + private void contextMenuItemClicked() { + IHttpRequestResponse httpRequestResponse = contextMenuData[0]; + + httpService = httpRequestResponse.getHttpService(); + request = httpRequestResponse.getRequest(); + + // Update fields in tab + hostTextField.setText(httpService.getHost()); + portTextField.setText(String.valueOf(httpService.getPort())); + protocolCheckBox.setSelected(httpService.getProtocol().equals("https")); + messageEditor.setMessage(request, true); + } + + private double mean(List values) { + double sum = 0.0; + for (double value : values) { + sum += value; + } + return sum / values.size(); + } + + private double median(List values) { + ArrayList sortedValues = new ArrayList<>(values); + Collections.sort(sortedValues); + int length = sortedValues.size(); + + if (length % 2 != 0) { + // Odd number of values, return middle one + return sortedValues.get(length / 2); + } else { + // Even number of values, return mean of middle two + return (sortedValues.get(length / 2 - 1) + sortedValues.get(length / 2)) / 2.0; + } + } + + private double stdDev(List values) { + double meanValue = mean(values); + double sum = 0.0; + + for (double value : values) { + sum += Math.pow(value - meanValue, 2); + } + + double variance = sum / values.size(); + return Math.sqrt(variance); + } + + private void startAttack(ActionEvent e) { + // Switch to results tab + tabbedPane.setSelectedIndex(1); + + // Clear results table + while (resultsTableModel.getRowCount() > 0) { + resultsTableModel.removeRow(0); + } + + // Set progress bar to 0% + progressBar.setValue(0); + + // Start attack in new thread + new Thread(() -> makeHttpRequests()).start(); + } + + private void makeHttpRequests() { + // Set class variables from values in UI + updateClassFromUI(); + + Map> responses = new HashMap<>(); + Set payloads = new HashSet<>(Arrays.asList(payloadTextArea.getText().split("\n"))); + int numReq = Integer.parseInt(requestsNumTextField.getText()); + + // Set progress bar max to number of requests + progressBar.setMaximum(payloads.size() * numReq); + + for (String payload : payloads) { + responses.put(payload, new ArrayList<>()); + + // Replace payload markers in request + byte[] modifiedRequest = replacePayloadMarkers(request, payload); + modifiedRequest = updateContentLength(modifiedRequest); + + for (int i = 0; i < numReq; i++) { + // Make request and measure time + long startTime = System.currentTimeMillis(); + IHttpRequestResponse response = callbacks.makeHttpRequest(httpService, modifiedRequest); + long endTime = System.currentTimeMillis(); + double duration = endTime - startTime; + + progressBar.setValue(progressBar.getValue() + 1); + responses.get(payload).add(duration); + + // If this was the last request for this payload, add results to table + if (i == numReq - 1) { + addResultToTable(payload, numReq, response, responses.get(payload)); + } + } + } + } + + private byte[] replacePayloadMarkers(byte[] request, String payload) { + String requestString = helpers.bytesToString(request); + requestString = requestString.replaceAll("\u00a7[^\u00a7]*\u00a7", payload); + return helpers.stringToBytes(requestString); + } + + private byte[] updateContentLength(byte[] request) { + IRequestInfo analyzedRequest = helpers.analyzeRequest(request); + int bodyOffset = analyzedRequest.getBodyOffset(); + int contentLength = request.length - bodyOffset; + + List headers = analyzedRequest.getHeaders(); + List newHeaders = new ArrayList<>(); + + for (String header : headers) { + if (!header.toLowerCase().startsWith("content-length:")) { + newHeaders.add(header); + } + } + newHeaders.add("Content-Length: " + contentLength); + + byte[] body = Arrays.copyOfRange(request, bodyOffset, request.length); + return helpers.buildHttpMessage(newHeaders, body); + } + + private void addResultToTable(final String payload, final int numReqs, final IHttpRequestResponse response, final List timings) { + final short statusCode = response.getResponse() != null ? + helpers.analyzeResponse(response.getResponse()).getStatusCode() : 0; + + final int contentLength; + if (response.getResponse() != null) { + IResponseInfo responseInfo = helpers.analyzeResponse(response.getResponse()); + int tempLength = 0; + for (String header : responseInfo.getHeaders()) { + if (header.toLowerCase().startsWith("content-length:")) { + tempLength = Integer.parseInt(header.split(": ")[1].trim()); + break; + } + } + contentLength = tempLength; + } else { + contentLength = 0; + } + + SwingUtilities.invokeLater(() -> { + // Calculate statistics inside the lambda to avoid effectively final issues + double meanTime = Math.round(mean(timings) * 1000.0) / 1000.0; + double medianTime = Math.round(median(timings) * 1000.0) / 1000.0; + double stdDevTime = Math.round(stdDev(timings) * 1000.0) / 1000.0; + int minTime = (int) Collections.min(timings).doubleValue(); + int maxTime = (int) Collections.max(timings).doubleValue(); + + resultsTableModel.addRow(new Object[]{ + payload, numReqs, statusCode, + response.getResponse() != null ? response.getResponse().length : 0, + contentLength, minTime, maxTime, meanTime, medianTime, stdDevTime + }); + }); + } + + private void updateClassFromUI() { + String host = hostTextField.getText(); + int port = Integer.parseInt(portTextField.getText()); + String protocol = protocolCheckBox.isSelected() ? "https" : "http"; + + // Attempt DNS resolution to cache the result + try { + java.net.InetAddress.getByName(host); + } catch (Exception e) { + // Ignore resolution failures + } + + httpService = helpers.buildHttpService(host, port, protocol); + request = messageEditor.getMessage(); + } + + private void addPayload(ActionEvent e) { + byte[] currentMessage = messageEditor.getMessage(); + int[] selection = messageEditor.getSelectionBounds(); + + if (selection[0] == selection[1]) { + // No text selected, insert markers at cursor + byte[] newMessage = new byte[currentMessage.length + 2]; + System.arraycopy(currentMessage, 0, newMessage, 0, selection[0]); + newMessage[selection[0]] = (byte)0xa7; + newMessage[selection[0] + 1] = (byte)0xa7; + System.arraycopy(currentMessage, selection[0], newMessage, selection[0] + 2, currentMessage.length - selection[0]); + messageEditor.setMessage(newMessage, true); + } else { + // Text selected, wrap with markers + byte[] newMessage = new byte[currentMessage.length + 2]; + System.arraycopy(currentMessage, 0, newMessage, 0, selection[0]); + newMessage[selection[0]] = (byte)0xa7; + System.arraycopy(currentMessage, selection[0], newMessage, selection[0] + 1, selection[1] - selection[0]); + newMessage[selection[1] + 1] = (byte)0xa7; + System.arraycopy(currentMessage, selection[1], newMessage, selection[1] + 2, currentMessage.length - selection[1]); + messageEditor.setMessage(newMessage, true); + } + } + + private void clearPayloads(ActionEvent e) { + byte[] currentMessage = messageEditor.getMessage(); + String messageString = helpers.bytesToString(currentMessage).replace("\u00a7", ""); + messageEditor.setMessage(helpers.stringToBytes(messageString), true); + } + + private JPanel constructAttackPanel(Insets insets, Component messageEditorComponent) { + JPanel attackPanel = new JPanel(new GridBagLayout()); + GridBagConstraints c; + + // Target heading + JLabel targetHeadingLabel = new JLabel("Target"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.gridwidth = 4; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(targetHeadingLabel, c); + + // Start Attack button + JButton startAttackButton = new JButton("Start Attack"); + startAttackButton.addActionListener(this::startAttack); + c = new GridBagConstraints(); + c.gridx = 4; + c.gridy = 0; + c.insets = insets; + attackPanel.add(startAttackButton, c); + + // Host field + JLabel hostLabel = new JLabel("Host:"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 1; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(hostLabel, c); + + hostTextField = new JTextField(25); + hostTextField.setMinimumSize(hostTextField.getPreferredSize()); + c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = 1; + c.weightx = 1; + c.gridwidth = 2; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(hostTextField, c); + + // Port field + JLabel portLabel = new JLabel("Port:"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 2; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(portLabel, c); + + portTextField = new JTextField(5); + portTextField.setMinimumSize(portTextField.getPreferredSize()); + c = new GridBagConstraints(); + c.gridx = 1; + c.gridy = 2; + c.gridwidth = 2; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(portTextField, c); + + // HTTPS checkbox + protocolCheckBox = new JCheckBox("Use HTTPS"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 3; + c.gridwidth = 3; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(protocolCheckBox, c); + + // Request heading + JLabel requestHeadingLabel = new JLabel("Request"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 4; + c.gridwidth = 4; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(requestHeadingLabel, c); + + // Message editor + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 5; + c.weightx = 1; + c.weighty = 0.75; + c.gridwidth = 4; + c.gridheight = 2; + c.fill = GridBagConstraints.BOTH; + c.insets = insets; + attackPanel.add(messageEditorComponent, c); + + // Add/Clear payload marker buttons + JButton addPayloadButton = new JButton("Add §"); + addPayloadButton.addActionListener(this::addPayload); + c = new GridBagConstraints(); + c.gridx = 4; + c.gridy = 5; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = insets; + attackPanel.add(addPayloadButton, c); + + JButton clearPayloadButton = new JButton("Clear §"); + clearPayloadButton.addActionListener(this::clearPayloads); + c = new GridBagConstraints(); + c.gridx = 4; + c.gridy = 6; + c.anchor = GridBagConstraints.PAGE_START; + c.fill = GridBagConstraints.HORIZONTAL; + c.insets = insets; + attackPanel.add(clearPayloadButton, c); + + // Payloads section + JLabel payloadHeadingLabel = new JLabel("Payloads"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 7; + c.gridwidth = 4; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(payloadHeadingLabel, c); + + payloadTextArea = new JTextArea(); + JScrollPane payloadScrollPane = new JScrollPane(payloadTextArea); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 8; + c.weighty = 0.25; + c.gridwidth = 3; + c.fill = GridBagConstraints.BOTH; + c.insets = insets; + attackPanel.add(payloadScrollPane, c); + + // Number of requests field + JLabel requestsNumLabel = new JLabel("Number of requests for each payload:"); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 9; + c.gridwidth = 2; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(requestsNumLabel, c); + + requestsNumTextField = new JTextField("100", 4); + requestsNumTextField.setMinimumSize(requestsNumTextField.getPreferredSize()); + c = new GridBagConstraints(); + c.gridx = 2; + c.gridy = 9; + c.anchor = GridBagConstraints.LINE_START; + c.insets = insets; + attackPanel.add(requestsNumTextField, c); + + return attackPanel; + } + + private JPanel constructResultsPanel(Insets insets) { + JPanel resultsPanel = new JPanel(new GridBagLayout()); + GridBagConstraints c; + + // Progress bar + progressBar = new JProgressBar(); + progressBar.setStringPainted(true); + progressBar.setMinimum(0); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 0; + c.fill = GridBagConstraints.HORIZONTAL; + resultsPanel.add(progressBar, c); + + // Results table + resultsTableModel = new ResultsTableModel(); + JTable resultsTable = new JTable(resultsTableModel); + resultsTable.setAutoCreateRowSorter(true); + + // Set up cell renderer for timing columns + ColoredTableCellRenderer cellRenderer = new ColoredTableCellRenderer(); + for (int i = 5; i <= 9; i++) { + resultsTable.getColumnModel().getColumn(i).setCellRenderer(cellRenderer); + } + + // Set column widths + resultsTable.getColumnModel().getColumn(0).setPreferredWidth(99999999); + resultsTable.getColumnModel().getColumn(1).setMinWidth(160); + resultsTable.getColumnModel().getColumn(2).setMinWidth(100); + resultsTable.getColumnModel().getColumn(3).setMinWidth(80); + resultsTable.getColumnModel().getColumn(4).setMinWidth(80); + resultsTable.getColumnModel().getColumn(5).setMinWidth(110); + resultsTable.getColumnModel().getColumn(6).setMinWidth(110); + resultsTable.getColumnModel().getColumn(7).setMinWidth(90); + resultsTable.getColumnModel().getColumn(8).setMinWidth(110); + resultsTable.getColumnModel().getColumn(9).setMinWidth(110); + + resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); + + JScrollPane resultsScrollPane = new JScrollPane(resultsTable); + c = new GridBagConstraints(); + c.gridx = 0; + c.gridy = 1; + c.weightx = 1; + c.weighty = 1; + c.fill = GridBagConstraints.BOTH; + resultsPanel.add(resultsScrollPane, c); + + return resultsPanel; + } + + private JPanel constructAboutPanel(Insets insets) { + JPanel aboutPanel = new JPanel(new GridBagLayout()); + + String aboutBody = + EXTENSION_NAME + "\n\n" + + "A Burp Suite extension for timing attacks.\n\n" + + "To use this extension:\n\n" + + "1. Send a request to this extension using the context menu\n" + + "2. Mark the position(s) for payload insertion using the § buttons\n" + + "3. Enter payloads (one per line)\n" + + "4. Click 'Start Attack'\n\n" + + "The extension will make the specified number of requests for each payload " + + "and display timing statistics in the Results tab."; + + JTextArea aboutTextArea = new JTextArea(aboutBody); + aboutTextArea.setEditable(false); + aboutTextArea.setWrapStyleWord(true); + aboutTextArea.setLineWrap(true); + aboutTextArea.setBackground(new Color(238, 238, 238)); + aboutTextArea.setMargin(new Insets(10, 10, 10, 10)); + + GridBagConstraints c = new GridBagConstraints(); + c.weightx = 1; + c.weighty = 1; + c.insets = insets; + c.fill = GridBagConstraints.BOTH; + c.anchor = GridBagConstraints.PAGE_START; + aboutPanel.add(aboutTextArea, c); + + return aboutPanel; + } +} \ No newline at end of file diff --git a/src/main/java/burp/ColoredTableCellRenderer.java b/src/main/java/burp/ColoredTableCellRenderer.java new file mode 100644 index 0000000..04903e5 --- /dev/null +++ b/src/main/java/burp/ColoredTableCellRenderer.java @@ -0,0 +1,63 @@ +package burp; + +import javax.swing.*; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; + +public class ColoredTableCellRenderer extends DefaultTableCellRenderer { + @Override + public Component getTableCellRendererComponent(JTable table, Object value, + boolean isSelected, boolean hasFocus, int row, int column) { + + Component renderer = super.getTableCellRendererComponent( + table, value, isSelected, hasFocus, row, column); + + if (table.getRowCount() == 1) { + renderer.setBackground(table.getBackground()); + renderer.setForeground(table.getForeground()); + return renderer; + } + + // Get all values in this column + double[] colValues = new double[table.getRowCount()]; + for (int i = 0; i < table.getRowCount(); i++) { + Object val = table.getValueAt(i, column); + colValues[i] = val instanceof Integer ? (Integer)val : (Double)val; + } + + // Find min and max + double minValue = colValues[0]; + double maxValue = colValues[0]; + for (double val : colValues) { + minValue = Math.min(minValue, val); + maxValue = Math.max(maxValue, val); + } + + if (minValue != maxValue) { + double currentValue = value instanceof Integer ? (Integer)value : (Double)value; + float fraction = (float)((currentValue - minValue) / (maxValue - minValue)); + + // Set text color + if (fraction > 0.75) { + renderer.setForeground(Color.WHITE); + } else { + renderer.setForeground(Color.BLACK); + } + + // Calculate color components + float red = fraction > 0.5f ? 1.0f : fraction * 2.0f; + float green = fraction < 0.5f ? 1.0f : 2.0f - (fraction * 2.0f); + float blue = 111f/256f; + + if (isSelected) { + red = Math.max(0.0f, red - 0.25f); + green = Math.max(0.0f, green - 0.25f); + blue = Math.max(0.0f, blue - 0.25f); + } + + renderer.setBackground(new Color(red, green, blue)); + } + + return renderer; + } +} \ No newline at end of file diff --git a/src/main/java/burp/ResultsTableModel.java b/src/main/java/burp/ResultsTableModel.java new file mode 100644 index 0000000..4d2f8be --- /dev/null +++ b/src/main/java/burp/ResultsTableModel.java @@ -0,0 +1,37 @@ +package burp; + +import javax.swing.table.DefaultTableModel; + +public class ResultsTableModel extends DefaultTableModel { + private static final String[] COLUMN_NAMES = { + "Payload", "Number of Requests", "Status Code", "Length (B)", "Body (B)", + "Minimum (ms)", "Maximum (ms)", "Mean (ms)", "Median (ms)", "StdDev (ms)" + }; + + private static final Class[] COLUMN_TYPES = { + String.class, + Integer.class, + Integer.class, + Integer.class, + Integer.class, + Integer.class, + Integer.class, + Double.class, + Double.class, + Double.class + }; + + public ResultsTableModel() { + super(COLUMN_NAMES, 0); + } + + @Override + public Class getColumnClass(int column) { + return COLUMN_TYPES[column]; + } + + @Override + public boolean isCellEditable(int row, int column) { + return false; + } +} \ No newline at end of file diff --git a/timeinator.py b/timeinator.py deleted file mode 100644 index 5cbd1cf..0000000 --- a/timeinator.py +++ /dev/null @@ -1,495 +0,0 @@ -from re import sub -from socket import gethostbyname -from threading import Thread -from time import time - -from javax.swing import (JTabbedPane, JPanel, JLabel, JTextField, - JTextArea, JCheckBox, JMenuItem, JButton, JTable, - JScrollPane, JProgressBar) -from javax.swing.table import DefaultTableModel, DefaultTableCellRenderer -from java.awt import Color, GridBagLayout, GridBagConstraints, Insets -import java.lang - -from burp import ( - IBurpExtender, ITab, IContextMenuFactory, IMessageEditorController) - -EXTENSION_NAME = "Timeinator" -COLUMNS = [ - "Payload", "Number of Requests", "Status Code", "Length (B)", "Body (B)", - "Minimum (ms)", "Maximum (ms)", "Mean (ms)", "Median (ms)", "StdDev (ms)"] - - -def mean(values): - return sum(values) / len(values) - - -def median(values): - length = len(values) - values.sort() - if length % 2 != 0: - # Odd number of values, so chose middle one - return values[length/2] - else: - # Even number of values, so mean of middle two - return mean([values[length/2], values[(length/2)-1]]) - -def stdDev(values): - ss = sum((x - mean(values))**2 for x in values) - pvar = ss/len(values) - return pvar**0.5 - -class BurpExtender( - IBurpExtender, ITab, IContextMenuFactory, IMessageEditorController): - - # Implement IBurpExtender - def registerExtenderCallbacks(self, callbacks): - - callbacks.registerContextMenuFactory(self) - - self._callbacks = callbacks - self._helpers = callbacks.getHelpers() - - callbacks.setExtensionName(EXTENSION_NAME) - - # Construct UI - insets = Insets(3, 3, 3, 3) - self._messageEditor = callbacks.createMessageEditor(self, True) - attackPanel = self._constructAttackPanel( - insets, self._messageEditor.getComponent()) - resultsPanel = self._constructResultsPanel(insets) - aboutPanel = self._constructAboutPanel(insets) - self._tabbedPane = JTabbedPane() - self._tabbedPane.addTab("Attack", attackPanel) - self._tabbedPane.addTab("Results", resultsPanel) - self._tabbedPane.addTab("About", aboutPanel) - callbacks.addSuiteTab(self) - - # Implement ITab - def getTabCaption(self): - return EXTENSION_NAME - - def getUiComponent(self): - return self._tabbedPane - - # Implement IMessageEditorController - def getHttpService(self): - self._updateClassFromUI() - return self._httpService - - def getRequest(self): - self._updateClassFromUI() - return self._request - - def getResponse(self): - return None - - # Implement IContextMenuFactory - def createMenuItems(self, contextMenuInvocation): - messages = contextMenuInvocation.getSelectedMessages() - - # Only add menu item if a single request is selected - if len(messages) == 1: - self._contextMenuData = messages - menu_item = JMenuItem( - "Send to {}".format(EXTENSION_NAME), - actionPerformed=self._contextMenuItemClicked - ) - return [menu_item] - - def _contextMenuItemClicked(self, _): - httpRequestResponse = self._contextMenuData[0] - - # Update instance variables with request data - self._httpService = httpRequestResponse.getHttpService() - self._request = httpRequestResponse.getRequest() - - # Update fields in tab - self._hostTextField.setText(self._httpService.getHost()) - self._portTextField.setText(str(self._httpService.getPort())) - self._protocolCheckBox.setSelected( - True if self._httpService.getProtocol() == "https" else False) - self._messageEditor.setMessage(self._request, True) - - def _startAttack(self, _): - - # Switch to results tab - self._tabbedPane.setSelectedIndex(1) - - # Clear results table - self._resultsTableModel.setRowCount(0) - - # Set progress bar to 0% - self._progressBar.setValue(0) - - Thread(target=self._makeHttpRequests).start() - - def _makeHttpRequests(self): - - # Set class variables from values in UI - self._updateClassFromUI() - - self._responses = {} - - # Set progress bar max to number of requests - self._progressBar.setMaximum(len(self._payloads) * self._numReq) - - for payload in self._payloads: - self._responses[payload] = [] - # Stick payload into request at specified position - # Use lambda function for replacement string to stop slashes being - # escaped - request = sub("\xa7[^\xa7]*\xa7", lambda x: payload, self._request) - request = self._updateContentLength(request) - for _ in xrange(self._numReq): - # Make request and work out how long it took in ms. This method - # is crude, but it's as good as we can get with current Burp - # APIs. - # See https://support.portswigger.net/customer/portal/questions/16227838-request-response-timing # noqa: E501 - startTime = time() - response = self._callbacks.makeHttpRequest( - self._httpService, request) - endTime = time() - duration = (endTime - startTime) * 1000 - - self._progressBar.setValue(self._progressBar.getValue() + 1) - - self._responses[payload].append(duration) - - # If all responses for this payload have - # been added to array, add to results table. - results = self._responses[payload] - numReqs = self._numReq - statusCode = response.getStatusCode() - analysis = self._helpers.analyzeResponse( - response.getResponse()) - for header in analysis.getHeaders(): - if header.lower().startswith("content-length"): - content_length = int(header.split(": ")[1]) - meanTime = round(mean(results), 3) - medianTime = round(median(results), 3) - stdDevTime = round(stdDev(results), 3) - minTime = int(min(results)) - maxTime = int(max(results)) - rowData = [ - payload, numReqs, statusCode, - len(response.getResponse()), content_length, minTime, - maxTime, meanTime, medianTime, stdDevTime] - self._resultsTableModel.addRow(rowData) - - def _updateClassFromUI(self): - host = self._hostTextField.text - port = int(self._portTextField.text) - protocol = "https" if self._protocolCheckBox.isSelected() else "http" - # In an effort to prevent DNS queries introducing a delay, an attempt - # was made to use the IP address of the destination web server instead - # of the hostname when building the HttpService. Unfortunately it - # caused issues with HTTPS requests, probably because of SNIs. As an - # alternative, the hostname is resolved in the next line and hopefully - # it will be cached at that point. - gethostbyname(host) - - self._httpService = self._helpers.buildHttpService( - host, port, protocol) - self._request = self._messageEditor.getMessage() - self._numReq = int(self._requestsNumTextField.text) - self._payloads = set(self._payloadTextArea.text.split("\n")) - - def _addPayload(self, _): - request = self._messageEditor.getMessage() - selection = self._messageEditor.getSelectionBounds() - if selection[0] == selection[1]: - # No text selected so in/out points are same - request.insert(selection[0], 0xa7) - request.insert(selection[1], 0xa7) - else: - request.insert(selection[0], 0xa7) - request.insert(selection[1]+1, 0xa7) - self._messageEditor.setMessage(request, True) - - def _clearPayloads(self, _): - request = self._messageEditor.getMessage() - request = self._helpers.bytesToString(request).replace("\xa7", "") - self._messageEditor.setMessage(request, True) - - def _updateContentLength(self, request): - messageSize = len(request) - bodyOffset = self._helpers.analyzeRequest(request).getBodyOffset() - contentLength = messageSize - bodyOffset - contentLengthHeader = "Content-Length: {}".format(contentLength) - request = sub("Content-Length: \\d+", contentLengthHeader, request) - return request - - def _constructAttackPanel(self, insets, messageEditorComponent): - attackPanel = JPanel(GridBagLayout()) - - targetHeadingLabel = JLabel("Target") - targetHeadingLabelConstraints = GridBagConstraints() - targetHeadingLabelConstraints.gridx = 0 - targetHeadingLabelConstraints.gridy = 0 - targetHeadingLabelConstraints.gridwidth = 4 - targetHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START - targetHeadingLabelConstraints.insets = insets - attackPanel.add(targetHeadingLabel, targetHeadingLabelConstraints) - - startAttackButton = JButton("Start Attack", - actionPerformed=self._startAttack) - startAttackButtonConstraints = GridBagConstraints() - startAttackButtonConstraints.gridx = 4 - startAttackButtonConstraints.gridy = 0 - startAttackButtonConstraints.insets = insets - attackPanel.add(startAttackButton, startAttackButtonConstraints) - - hostLabel = JLabel("Host:") - hostLabelConstraints = GridBagConstraints() - hostLabelConstraints.gridx = 0 - hostLabelConstraints.gridy = 1 - hostLabelConstraints.anchor = GridBagConstraints.LINE_START - hostLabelConstraints.insets = insets - attackPanel.add(hostLabel, hostLabelConstraints) - - self._hostTextField = JTextField(25) - self._hostTextField.setMinimumSize( - self._hostTextField.getPreferredSize()) - hostTextFieldConstraints = GridBagConstraints() - hostTextFieldConstraints.gridx = 1 - hostTextFieldConstraints.gridy = 1 - hostTextFieldConstraints.weightx = 1 - hostTextFieldConstraints.gridwidth = 2 - hostTextFieldConstraints.anchor = GridBagConstraints.LINE_START - hostTextFieldConstraints.insets = insets - attackPanel.add(self._hostTextField, hostTextFieldConstraints) - - portLabel = JLabel("Port:") - portLabelConstraints = GridBagConstraints() - portLabelConstraints.gridx = 0 - portLabelConstraints.gridy = 2 - portLabelConstraints.anchor = GridBagConstraints.LINE_START - portLabelConstraints.insets = insets - attackPanel.add(portLabel, portLabelConstraints) - - self._portTextField = JTextField(5) - self._portTextField.setMinimumSize( - self._portTextField.getPreferredSize()) - portTextFieldConstraints = GridBagConstraints() - portTextFieldConstraints.gridx = 1 - portTextFieldConstraints.gridy = 2 - portTextFieldConstraints.gridwidth = 2 - portTextFieldConstraints.anchor = GridBagConstraints.LINE_START - portTextFieldConstraints.insets = insets - attackPanel.add(self._portTextField, portTextFieldConstraints) - - self._protocolCheckBox = JCheckBox("Use HTTPS") - protocolCheckBoxConstraints = GridBagConstraints() - protocolCheckBoxConstraints.gridx = 0 - protocolCheckBoxConstraints.gridy = 3 - protocolCheckBoxConstraints.gridwidth = 3 - protocolCheckBoxConstraints.anchor = GridBagConstraints.LINE_START - protocolCheckBoxConstraints.insets = insets - attackPanel.add(self._protocolCheckBox, protocolCheckBoxConstraints) - - requestHeadingLabel = JLabel("Request") - requestHeadingLabelConstraints = GridBagConstraints() - requestHeadingLabelConstraints.gridx = 0 - requestHeadingLabelConstraints.gridy = 4 - requestHeadingLabelConstraints.gridwidth = 4 - requestHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START - requestHeadingLabelConstraints.insets = insets - attackPanel.add(requestHeadingLabel, requestHeadingLabelConstraints) - - messageEditorComponentConstraints = GridBagConstraints() - messageEditorComponentConstraints.gridx = 0 - messageEditorComponentConstraints.gridy = 5 - messageEditorComponentConstraints.weightx = 1 - messageEditorComponentConstraints.weighty = .75 - messageEditorComponentConstraints.gridwidth = 4 - messageEditorComponentConstraints.gridheight = 2 - messageEditorComponentConstraints.fill = GridBagConstraints.BOTH - messageEditorComponentConstraints.insets = insets - attackPanel.add( - messageEditorComponent, messageEditorComponentConstraints) - - addPayloadButton = JButton( - "Add \xa7", actionPerformed=self._addPayload) - addPayloadButtonConstraints = GridBagConstraints() - addPayloadButtonConstraints.gridx = 4 - addPayloadButtonConstraints.gridy = 5 - addPayloadButtonConstraints.fill = GridBagConstraints.HORIZONTAL - addPayloadButtonConstraints.insets = insets - attackPanel.add(addPayloadButton, addPayloadButtonConstraints) - - clearPayloadButton = JButton( - "Clear \xa7", actionPerformed=self._clearPayloads) - clearPayloadButtonConstraints = GridBagConstraints() - clearPayloadButtonConstraints.gridx = 4 - clearPayloadButtonConstraints.gridy = 6 - clearPayloadButtonConstraints.anchor = GridBagConstraints.PAGE_START - clearPayloadButtonConstraints.fill = GridBagConstraints.HORIZONTAL - clearPayloadButtonConstraints.insets = insets - attackPanel.add(clearPayloadButton, clearPayloadButtonConstraints) - - payloadHeadingLabel = JLabel("Payloads") - payloadHeadingLabelConstraints = GridBagConstraints() - payloadHeadingLabelConstraints.gridx = 0 - payloadHeadingLabelConstraints.gridy = 7 - payloadHeadingLabelConstraints.gridwidth = 4 - payloadHeadingLabelConstraints.anchor = GridBagConstraints.LINE_START - payloadHeadingLabelConstraints.insets = insets - attackPanel.add(payloadHeadingLabel, payloadHeadingLabelConstraints) - - self._payloadTextArea = JTextArea() - payloadScrollPane = JScrollPane(self._payloadTextArea) - payloadScrollPaneConstraints = GridBagConstraints() - payloadScrollPaneConstraints.gridx = 0 - payloadScrollPaneConstraints.gridy = 8 - payloadScrollPaneConstraints.weighty = .25 - payloadScrollPaneConstraints.gridwidth = 3 - payloadScrollPaneConstraints.fill = GridBagConstraints.BOTH - payloadScrollPaneConstraints.insets = insets - attackPanel.add(payloadScrollPane, payloadScrollPaneConstraints) - - requestsNumLabel = JLabel("Number of requests for each payload:") - requestsNumLabelConstraints = GridBagConstraints() - requestsNumLabelConstraints.gridx = 0 - requestsNumLabelConstraints.gridy = 9 - requestsNumLabelConstraints.gridwidth = 2 - requestsNumLabelConstraints.anchor = GridBagConstraints.LINE_START - requestsNumLabelConstraints.insets = insets - attackPanel.add(requestsNumLabel, requestsNumLabelConstraints) - - self._requestsNumTextField = JTextField("100", 4) - self._requestsNumTextField.setMinimumSize( - self._requestsNumTextField.getPreferredSize()) - requestsNumTextFieldConstraints = GridBagConstraints() - requestsNumTextFieldConstraints.gridx = 2 - requestsNumTextFieldConstraints.gridy = 9 - requestsNumTextFieldConstraints.anchor = GridBagConstraints.LINE_START - requestsNumTextFieldConstraints.insets = insets - attackPanel.add( - self._requestsNumTextField, requestsNumTextFieldConstraints) - - return attackPanel - - def _constructResultsPanel(self, insets): - resultsPanel = JPanel(GridBagLayout()) - - self._progressBar = JProgressBar() - self._progressBar.setStringPainted(True) - self._progressBar.setMinimum(0) - progressBarContraints = GridBagConstraints() - progressBarContraints.gridx = 0 - progressBarContraints.gridy = 0 - progressBarContraints.fill = GridBagConstraints.HORIZONTAL - - resultsPanel.add(self._progressBar, progressBarContraints) - - self._resultsTableModel = ResultsTableModel(COLUMNS, 0) - resultsTable = JTable(self._resultsTableModel) - resultsTable.setAutoCreateRowSorter(True) - cellRenderer = ColoredTableCellRenderer() - for index in [5, 6, 7, 8, 9]: - column = resultsTable.columnModel.getColumn(index) - column.cellRenderer = cellRenderer - resultsTable.getColumnModel().getColumn(0).setPreferredWidth(99999999) - resultsTable.getColumnModel().getColumn(1).setMinWidth(160) - resultsTable.getColumnModel().getColumn(2).setMinWidth(100) - resultsTable.getColumnModel().getColumn(3).setMinWidth(80) - resultsTable.getColumnModel().getColumn(4).setMinWidth(80) - resultsTable.getColumnModel().getColumn(5).setMinWidth(110) - resultsTable.getColumnModel().getColumn(6).setMinWidth(110) - resultsTable.getColumnModel().getColumn(7).setMinWidth(90) - resultsTable.getColumnModel().getColumn(8).setMinWidth(110) - resultsTable.getColumnModel().getColumn(9).setMinWidth(110) - resultsTable.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS) - resultsScrollPane = JScrollPane(resultsTable) - resultsScrollPaneConstraints = GridBagConstraints() - resultsScrollPaneConstraints.gridx = 0 - resultsScrollPaneConstraints.gridy = 1 - resultsScrollPaneConstraints.weightx = 1 - resultsScrollPaneConstraints.weighty = 1 - resultsScrollPaneConstraints.fill = GridBagConstraints.BOTH - resultsPanel.add(resultsScrollPane, resultsScrollPaneConstraints) - - return resultsPanel - - def _constructAboutPanel(self, insets): - aboutPanel = JPanel(GridBagLayout()) - with open("about.html") as file: - aboutBody = file.read() - aboutLabel = JLabel( - aboutBody.format(extension_name=EXTENSION_NAME)) - aboutLabelConstraints = GridBagConstraints() - aboutLabelConstraints.weightx = 1 - aboutLabelConstraints.weighty = 1 - aboutLabelConstraints.insets = insets - aboutLabelConstraints.fill = GridBagConstraints.HORIZONTAL - aboutLabelConstraints.anchor = GridBagConstraints.PAGE_START - aboutPanel.add(aboutLabel, aboutLabelConstraints) - - return aboutPanel - - -# Required for coloured cells -class ColoredTableCellRenderer(DefaultTableCellRenderer): - def getTableCellRendererComponent( - self, table, value, isSelected, hasFocus, row, column): - - renderer = DefaultTableCellRenderer.getTableCellRendererComponent( - self, table, value, isSelected, hasFocus, row, column) - - value = table.getValueAt(row, column) - model = table.getModel() - rowsCount = model.getRowCount() - if rowsCount == 1: - renderer.setBackground(table.getBackground()) - renderer.setForeground(table.getForeground()) - else: - colValues = [] - for index in xrange(rowsCount): - valueAtIndex = model.getValueAt(index, column) - colValues.append(valueAtIndex) - minBound = min(colValues) - maxBound = max(colValues) - if minBound != maxBound: - valueAsFraction = ( - float(value - minBound) / (maxBound - minBound)) - if valueAsFraction > 0.75: - renderer.setForeground(Color.WHITE) - else: - renderer.setForeground(Color.BLACK) - if valueAsFraction > 0.5: - red = 1.0 - else: - red = (valueAsFraction * 2.0) - if valueAsFraction < 0.5: - green = 1.0 - else: - green = 2 - (valueAsFraction * 2.0) - blue = 111/256.0 - - if isSelected: - red = max(0.0, red-0.25) - green = max(0.0, green-0.25) - blue = max(0.0, blue-0.25) - - renderer.setBackground(Color(red, green, blue)) - return renderer - - -# Required for proper sorting -class ResultsTableModel(DefaultTableModel): - - # Native java types are required here for proper sorting - _types = [ - java.lang.String, - java.lang.Integer, - java.lang.Integer, - java.lang.Integer, - java.lang.Integer, - java.lang.Integer, - java.lang.Integer, - java.lang.Float, - java.lang.Float, - java.lang.Float] - - def getColumnClass(self, column): - return self._types[column]