Ejemplo n.º 1
0
class BurpExtender(IBurpExtender, ITab, IHttpListener,
                   IMessageEditorController, AbstractTableModel,
                   ActionListener, DocumentListener):

    #
    # implement IBurpExtender
    #

    def registerExtenderCallbacks(self, callbacks):

        # keep a reference to our callbacks object
        self._callbacks = callbacks

        # obtain an extension helpers object
        self._helpers = callbacks.getHelpers()

        # set our extension name
        callbacks.setExtensionName("Response Clusterer")

        # create the log and a lock on which to synchronize when adding log entries
        self._log = ArrayList()
        self._lock = Lock()

        # main split pane
        self._main_jtabedpane = JTabbedPane()

        # The split pane with the log and request/respponse details
        self._splitpane = JSplitPane(JSplitPane.VERTICAL_SPLIT)

        # table of log entries
        logTable = Table(self)
        scrollPane = JScrollPane(logTable)
        self._splitpane.setLeftComponent(scrollPane)

        # List of log entries
        self._log_entries = []

        # tabs with request/response viewers
        tabs = JTabbedPane()
        self._requestViewer = callbacks.createMessageEditor(self, False)
        self._responseViewer = callbacks.createMessageEditor(self, False)
        tabs.addTab("Request", self._requestViewer.getComponent())
        tabs.addTab("Response", self._responseViewer.getComponent())
        self._splitpane.setRightComponent(tabs)

        #Setup the options
        self._optionsJPanel = JPanel()
        gridBagLayout = GridBagLayout()
        gbc = GridBagConstraints()
        self._optionsJPanel.setLayout(gridBagLayout)

        self.max_clusters = 500
        self.JLabel_max_clusters = JLabel("Maximum amount of clusters: ")
        gbc.gridy = 0
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_max_clusters, gbc)
        self.JTextField_max_clusters = JTextField(str(self.max_clusters), 5)
        self.JTextField_max_clusters.getDocument().addDocumentListener(self)
        gbc.gridx = 1
        self._optionsJPanel.add(self.JTextField_max_clusters, gbc)
        callbacks.customizeUiComponent(self.JLabel_max_clusters)
        callbacks.customizeUiComponent(self.JTextField_max_clusters)

        self.similarity = 0.95
        self.JLabel_similarity = JLabel("Similarity (between 0 and 1)")
        gbc.gridy = 1
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_similarity, gbc)
        self.JTextField_similarity = JTextField(str(self.similarity), 5)
        self.JTextField_similarity.getDocument().addDocumentListener(self)
        gbc.gridx = 1
        self._optionsJPanel.add(self.JTextField_similarity, gbc)
        callbacks.customizeUiComponent(self.JLabel_similarity)
        callbacks.customizeUiComponent(self.JTextField_similarity)

        self.use_quick_similar = False
        self.JLabel_use_quick_similar = JLabel(
            "Use set intersection of space splitted tokens for similarity (default: optimized difflib.SequenceMatcher.quick_ratio)"
        )
        gbc.gridy = 2
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_use_quick_similar, gbc)
        self.JCheckBox_use_quick_similar = JCheckBox("")
        self.JCheckBox_use_quick_similar.addActionListener(self)
        gbc.gridx = 1
        self._optionsJPanel.add(self.JCheckBox_use_quick_similar, gbc)
        callbacks.customizeUiComponent(self.JCheckBox_use_quick_similar)

        self.response_max_size = 10 * 1024  #10kb
        self.JLabel_response_max_size = JLabel("Response max size (bytes)")
        gbc.gridy = 3
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_response_max_size, gbc)
        self.JTextField_response_max_size = JTextField(
            str(self.response_max_size), 5)
        self.JTextField_response_max_size.getDocument().addDocumentListener(
            self)
        gbc.gridx = 1
        self._optionsJPanel.add(self.JTextField_response_max_size, gbc)
        callbacks.customizeUiComponent(self.JLabel_response_max_size)
        callbacks.customizeUiComponent(self.JTextField_response_max_size)

        self.uninteresting_mime_types = ('JPEG', 'CSS', 'GIF', 'script', 'GIF',
                                         'PNG', 'image')
        self.uninteresting_status_codes = ()
        self.uninteresting_url_file_extensions = ('js', 'css', 'zip', 'war',
                                                  'jar', 'doc', 'docx', 'xls',
                                                  'xlsx', 'pdf', 'exe', 'dll',
                                                  'png', 'jpeg', 'jpg', 'bmp',
                                                  'tif', 'tiff', 'gif', 'webp',
                                                  'm3u', 'mp4', 'm4a', 'ogg',
                                                  'aac', 'flac', 'mp3', 'wav',
                                                  'avi', 'mov', 'mpeg', 'wmv',
                                                  'swf', 'woff', 'woff2')

        about = "<html>"
        about += "Author: floyd, @floyd_ch, http://www.floyd.ch<br>"
        about += "modzero AG, http://www.modzero.ch<br>"
        about += "<br>"
        about += "<h3>Getting an overview of the tested website</h3>"
        about += "<p style=\"width:500px\">"
        about += "This plugin clusters all response bodies by similarity and shows a summary, one request/response per cluster. "
        about += 'Adjust similarity in the options if you get too few or too many entries in the "One member of each cluster" '
        about += "tab. The plugin will allow a tester to get an overview of the tested website's responses from all tools (scanner, proxy, etc.). "
        about += "As similarity comparison "
        about += "can use a lot of ressources, only small, in-scope responses that have interesting response codes, "
        about += "file extensions and mime types are processed. "
        about += "</p>"
        about += "</html>"
        self.JLabel_about = JLabel(about)
        self.JLabel_about.setLayout(GridBagLayout())
        self._aboutJPanel = JScrollPane(self.JLabel_about)

        # customize our UI components
        callbacks.customizeUiComponent(self._splitpane)
        callbacks.customizeUiComponent(logTable)
        callbacks.customizeUiComponent(scrollPane)
        callbacks.customizeUiComponent(tabs)

        # add the splitpane and options to the main jtabedpane
        self._main_jtabedpane.addTab("One member of each cluster", None,
                                     self._splitpane, None)
        self._main_jtabedpane.addTab("Options", None, self._optionsJPanel,
                                     None)
        self._main_jtabedpane.addTab("About & README", None, self._aboutJPanel,
                                     None)

        # clusters will grow up to self.max_clusters response bodies...
        self._clusters = set()
        self.Similarity = Similarity()

        # Now load the already stored
        with self._lock:
            log_entries_from_storage = self.load_project_setting("log_entries")
            if log_entries_from_storage:
                for toolFlag, req, resp, url in log_entries_from_storage:
                    try:
                        self.add_new_log_entry(toolFlag, req, resp, url)
                    except Exception as e:
                        print "Exception when deserializing a stored log entry", toolFlag, url
                        print e

        # Important: Do this at the very end (otherwise we could run into troubles locking up entire threads)
        # add the custom tab to Burp's UI
        callbacks.addSuiteTab(self)

        # register ourselves as an HTTP listener
        callbacks.registerHttpListener(self)

    #
    # implement what happens when options are changed
    #

    def changedUpdate(self, document):
        pass

    def removeUpdate(self, document):
        self.actionPerformed(None)

    def insertUpdate(self, document):
        self.actionPerformed(None)

    def actionPerformed(self, actionEvent):
        self.use_quick_similar = self.JCheckBox_use_quick_similar.isSelected()
        try:
            self.max_clusters = int(self.JTextField_max_clusters.getText())
        except:
            self.JTextField_max_clusters.setText("200")

        try:
            self.similarity = float(self.JTextField_similarity.getText())
            if self.similarity > 1.0 or self.similarity < 0.0:
                self.JTextField_similarity.setText("0.9")
        except:
            self.JTextField_similarity.setText("0.9")

        try:
            self.response_max_size = float(
                self.JTextField_response_max_size.getText())
            if self.response_max_size < 0.0:
                self.JTextField_response_max_size.setText(str(10 * 1024))
        except:
            self.JTextField_response_max_size.setText(str(10 * 1024))

        print self.JCheckBox_use_quick_similar.isSelected(
        ), self.JTextField_max_clusters.getText(
        ), self.JTextField_similarity.getText(
        ), self.JTextField_response_max_size.getText()

    #
    # implement ITab
    #

    def getTabCaption(self):
        return "Response Clusterer"

    def getUiComponent(self):
        return self._main_jtabedpane

    #
    # implement IHttpListener
    #

    def processHttpMessage(self, toolFlag, messageIsRequest, messageInfo):
        if not messageIsRequest:
            if len(self._clusters) >= self.max_clusters:
                return
            resp = messageInfo.getResponse()
            if len(resp) >= self.response_max_size:
                print "Message was too long"
                return
            iResponseInfo = self._helpers.analyzeResponse(resp)
            mime_type = iResponseInfo.getStatedMimeType()
            if mime_type in self.uninteresting_mime_types:
                print "Mime type", mime_type, "is ignored"
                return
            if iResponseInfo.getStatusCode(
            ) in self.uninteresting_status_codes:
                print "Status code", iResponseInfo.getStatusCode(
                ), "is ignored"
                return
            req = messageInfo.getRequest()
            iRequestInfo = self._helpers.analyzeRequest(messageInfo)
            if not iRequestInfo.getUrl():
                print "iRequestInfo.getUrl() returned None, so bailing out of analyzing this request"
                return
            if '.' in iRequestInfo.getUrl().getFile() and iRequestInfo.getUrl(
            ).getFile().split(
                    '.')[-1] in self.uninteresting_url_file_extensions:
                print iRequestInfo.getUrl().getFile().split(
                    '.')[-1], "is an ignored file extension"
                return
            if not self._callbacks.isInScope(iRequestInfo.getUrl()):
                print iRequestInfo.getUrl(), "is not in scope"
                return
            body = resp[iResponseInfo.getBodyOffset():]
            with self._lock:
                similarity_func = self.Similarity.similar
                if self.use_quick_similar:
                    similarity_func = self.Similarity.quick_similar
                start_time = time.time()
                for response_code, item in self._clusters:
                    if not response_code == iResponseInfo.getStatusCode():
                        #Different response codes -> different clusters
                        continue
                    if similarity_func(str(body), str(item), self.similarity):
                        return  #break
                else:  #when no break/return occures in the for loop
                    self.add_new_log_entry(toolFlag, req, resp,
                                           iRequestInfo.getUrl().toString())
                    self.save_project_setting("log_entries", self._log_entries)
                taken_time = time.time() - start_time
                if taken_time > 0.5:
                    print "Plugin took", taken_time, "seconds to process request... body length:", len(
                        body), "current cluster length:", len(self._clusters)
                    print "URL:", str(iRequestInfo.getUrl()),

    def add_new_log_entry(self, toolFlag, request, response, service_url):
        self._log_entries.append((toolFlag, request, response, service_url))
        iResponseInfo = self._helpers.analyzeResponse(response)
        body = response[iResponseInfo.getBodyOffset():]
        self._clusters.add((iResponseInfo.getStatusCode(), str(body)))
        row = self._log.size()
        service = CustomHttpService(service_url)
        r = CustomRequestResponse(None, None, service, request, response)
        iRequestInfo = self._helpers.analyzeRequest(r)
        self._log.add(
            LogEntry(toolFlag, self._callbacks.saveBuffersToTempFiles(r),
                     iRequestInfo.getUrl()))
        self.fireTableRowsInserted(row, row)

    #
    # extend AbstractTableModel
    #

    def getRowCount(self):
        try:
            return self._log.size()
        except:
            return 0

    def getColumnCount(self):
        return 2

    def getColumnName(self, columnIndex):
        if columnIndex == 0:
            return "Tool"
        if columnIndex == 1:
            return "URL"
        return ""

    def getValueAt(self, rowIndex, columnIndex):
        logEntry = self._log.get(rowIndex)
        if columnIndex == 0:
            return self._callbacks.getToolName(logEntry._tool)
        if columnIndex == 1:
            return logEntry._url.toString()
        return ""

    #
    # 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()

    def save_project_setting(self, name, value):
        value = pickle.dumps(value).encode("base64")
        request = "GET /"+name+" HTTP/1.0\r\n\r\n" \
                  "You can ignore this item in the site map. It was created by the ResponseClusterer extension. The \n" \
                  "reason is that the Burp API is missing a certain functionality to save settings. \n" \
                  "TODO Burp API limitation: This is a hackish way to be able to store project-scope settings.\n" \
                  "We don't want to restore requests/responses of tabs in a totally different Burp project.\n" \
                  "However, unfortunately there is no saveExtensionProjectSetting in the Burp API :(\n" \
                  "So we have to abuse the addToSiteMap API to store project-specific things\n" \
                  "Even when using this hack we currently cannot persist Collaborator interaction checks\n" \
                  "(IBurpCollaboratorClientContext is not serializable and Threads loose their Python class\n" \
                  "functionality when unloaded) due to Burp API limitations."
        response = None
        if value:
            response = "HTTP/1.1 200 OK\r\n" + value
        rr = CustomRequestResponse(
            name, '',
            CustomHttpService('http://responseclustererextension.local/'),
            request, response)
        self._callbacks.addToSiteMap(rr)

    def load_project_setting(self, name):
        rrs = self._callbacks.getSiteMap(
            'http://responseclustererextension.local/' + name)
        if rrs:
            rr = rrs[0]
            if rr.getResponse():
                val = "\r\n".join(
                    FloydsHelpers.jb2ps(rr.getResponse()).split("\r\n")[1:])
                return pickle.loads(val.decode("base64"))
            else:
                return None
        else:
            return None
Ejemplo n.º 2
0
class BurpExtender(IBurpExtender, ITab, DocumentListener, ActionListener, IScannerInsertionPointProvider):

    def	registerExtenderCallbacks(self, callbacks):
        print "Extension loaded!"
        self._callbacks = callbacks

        self._helpers = callbacks.getHelpers()

        callbacks.setExtensionName("HttpFuzzer")

        #Abusing functionality here :(
        #I would have prefered to have it implemented as an active scan module
        callbacks.registerScannerInsertionPointProvider(self)

        self._newline = "\r\n"

        #Options:
        self._random_mutations = 0
        self._known_fuzz_string_mutations = 0
        self._custom_fuzz_strings = None

        self._known_fuzz_strings = [
            "A" * 256,
            "A" * 1024,
            "A" * 4096,
            "A" * 20000,
            "A" * 65535,
            "%x" * 256,
            "%n" * 256 ,
            "%s" * 256,
            "%s%n%x%d" * 256,
            "%s" * 256,
            "%.1024d",
            "%.2048d",
            "%.4096d",
            "%.8200d",
            "%99999999999s",
            "%99999999999d",
            "%99999999999x",
            "%99999999999n",
            "%99999999999s" * 200,
            "%99999999999d" * 200,
            "%99999999999x" * 200,
            "%99999999999n" * 200,
            "%08x" * 100,
            "%%20s" * 200,
            "%%20x" * 200,
            "%%20n" * 200,
            "%%20d" * 200,
            "%#0123456x%08x%x%s%p%n%d%o%u%c%h%l%q%j%z%Z%t%i%e%g%f%a%C%S%08x%%#0123456x%%x%%s%%p%%n%%d%%o%%u%%c%%h%%l%%q%%j%%z%%Z%%t%%i%%e%%g%%f%%a%%C%%S%%08x",
            "'",
            "\\",
            "<",
            "+",
            "%",
            "$",
            "`"
        ]

        #End Options

        #UI START

        self._main_jtabedpane = JTabbedPane()

        #Setup the options
        self._optionsJPanel = JPanel()
        gridBagLayout = GridBagLayout();
        gbc = GridBagConstraints()
        self._optionsJPanel.setLayout(gridBagLayout)

        self.JLabel_random_mutations = JLabel("Number of random bit and byte mutations: ")
        gbc.gridy += 1
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_random_mutations, gbc)
        gbc.gridx = 1
        self.JTextField_random_mutations = JTextField(str(self._random_mutations), 6)
        self.JTextField_random_mutations.getDocument().addDocumentListener(self)
        self._optionsJPanel.add(self.JTextField_random_mutations, gbc)
        callbacks.customizeUiComponent(self.JLabel_random_mutations)
        callbacks.customizeUiComponent(self.JTextField_random_mutations)

        self.JLabel_known_fuzz_string_mutations = JLabel("Number of tests with known fuzzing strings: ")
        gbc.gridy += 1
        gbc.gridx = 0
        self._optionsJPanel.add(self.JLabel_known_fuzz_string_mutations, gbc)
        gbc.gridx = 1
        self.JTextField_known_fuzz_string_mutations = JTextField(str(self._known_fuzz_string_mutations), 6)
        self.JTextField_known_fuzz_string_mutations.getDocument().addDocumentListener(self)
        self._optionsJPanel.add(self.JTextField_known_fuzz_string_mutations, gbc)
        callbacks.customizeUiComponent(self.JLabel_known_fuzz_string_mutations)
        callbacks.customizeUiComponent(self.JTextField_known_fuzz_string_mutations)

        self._filepath = ''
        self.JLabel_filepath = JLabel("Replacement for known fuzzing strings (one per line): ")
        gbc.gridy += 1
        gbc.gridx = 0
        gbc.gridwidth = 1
        self._optionsJPanel.add(self.JLabel_filepath, gbc)
        gbc.gridx = 1
        self.JTextField_filepath = JTextField(self._filepath, 25)
        self.JTextField_filepath.getDocument().addDocumentListener(self)
        self._optionsJPanel.add(self.JTextField_filepath, gbc)
        gbc.gridx = 2
        self.FileChooserButton_filepath = FileChooserButton()
        self.FileChooserButton_filepath.setup(self.JTextField_filepath, "Choose")
        self._optionsJPanel.add(self.FileChooserButton_filepath, gbc)
        callbacks.customizeUiComponent(self.JLabel_filepath)
        callbacks.customizeUiComponent(self.JTextField_filepath)
        callbacks.customizeUiComponent(self.FileChooserButton_filepath)

        about = "<html>"
        about += "Author: floyd, @floyd_ch, http://www.floyd.ch<br>"
        about += "<br>"
        about += "<h3>A simple random fuzzer</h3>"
        about += "<p style=\"width:500px\">"
        about += "This plugin adds ActiveScan checks. "
        about += "Using this fuzzer with any standard HTTP server (Apache, Nginx, etc.) is usually useless, but can be fun. "
        about += "It can be used to see the different error conditions a server and the web application code can run into. "
        about += "However, if you are targeting an embedded device HTTP server or anything more exotic you might be more lucky. "
        about += "The plugin does not do any checks and doesn't add any issues. It is recommended to install the Collect500, "
        about += "ResponseClusterer, Logger++ and Error Message Checks plugin. Additionally it is recommended to attach a debugger to the "
        about += "target program on the server (or use strace or another tool of your choice). <br>"
        about += "In it's default configuration the plugin will not do anything, as it is not considered efficient to fuzz every actively scanned request. "
        about += "You need to specify a higher value for the number of tests in the options tab to enable fuzzing. "
        about += "</p>"
        about += "</html>"
        self.JLabel_about = JLabel(about)
        self.JLabel_about.setLayout(GridBagLayout())
        self._aboutJPanel = JScrollPane(self.JLabel_about)

        # customize our UI components
        callbacks.customizeUiComponent(self._main_jtabedpane)
        callbacks.customizeUiComponent(self._optionsJPanel)
        callbacks.customizeUiComponent(self._aboutJPanel)

        self._main_jtabedpane.addTab("Options", None, self._optionsJPanel, None)
        self._main_jtabedpane.addTab("About & README", None, self._aboutJPanel, None)

        # add the custom tab to Burp's UI
        callbacks.addSuiteTab(self)

        #UI END
        print "Extension registered!"

    #
    # UI: implement ITab
    #

    def getTabCaption(self):
        return "HttpFuzzer"

    def getUiComponent(self):
        return self._main_jtabedpane

    #
    # UI: implement what happens when options are changed
    #

    def changedUpdate(self, document):
        pass

    def removeUpdate(self, document):
        self.insertUpdate(document)

    def insertUpdate(self, document):
        filepath = self.JTextField_filepath.getText().encode("utf-8")
        if filepath:
            try:
                self._custom_fuzz_strings = file(filepath, "rb").readlines()
            except:
                print "ERROR: Couldn't read file"
                self._custom_fuzz_strings = None
        print filepath

        try:
            self._random_mutations = int(self.JTextField_random_mutations.getText())
        except:
            print "Exception,", self.JTextField_random_mutations.getText(), "is not numeric"
            self._random_mutations = 0
        try:
            self._known_fuzz_string_mutations = int(self.JTextField_known_fuzz_string_mutations.getText())
        except:
            print "Exception,", self.JTextField_known_fuzz_string_mutations.getText(), "is not numeric"
            self._known_fuzz_string_mutations = 0
        print self._random_mutations, self._known_fuzz_string_mutations

    def actionPerformed(self, actionEvent):
        self.insertUpdate(None)

    #TODO: Is there another way to simply say "each active scanned HTTP request once"?
    #it seems not: https://support.portswigger.net/customer/en/portal/questions/16776337-confusion-on-insertionpoints-active-scan-module?new=16776337
    #So we are going to abuse a functionality of Burp called IScannerInsertionPoint
    #which is by coincidence always called once per request for every actively scanned item (with baseRequestResponse)
    #this is an ugly hack as the percentage of active scan is simply stuck until this plugin is done
    def getInsertionPoints(self, baseRequestResponse):
        self.do_fuzzing(baseRequestResponse)

    def do_fuzzing(self, baseRequestResponse):
        req = FloydsHelpers.jb2ps(baseRequestResponse.getRequest())
        fuzz_strings = self._custom_fuzz_strings or self._known_fuzz_strings
        for _ in xrange(0, self._known_fuzz_string_mutations):
            index = random.choice(xrange(0, len(req)))
            print "Inserted known fuzz string at byte index", index
            new_req = req[:index]+random.choice(fuzz_strings)+req[index+1:]
            try:
                self._send(baseRequestResponse, new_req)
            except Exception, e:
                print "Error occured. Ignoring and simply going on."
                print e
        for _ in xrange(0, self._random_mutations):
            index = random.randint(0, len(req)-1)
            new_req = req
            if random.choice((True, False)):
                #byte change
                print "At byte index", index, "changed to new byte"
                new_req = req[:index]+chr(random.randint(0, 255))+req[index+1:]
            else:
                #bit change
                bit_index = random.randint(0, 7)
                print "At byte index", index, "changed bit", bit_index
                new_byte = chr(ord(req[index]) ^ (2**bit_index))
                new_req = req[:index]+new_byte+req[index+1:]
            try:
                self._send(baseRequestResponse, new_req)
            except Exception, e:
                print "Error occured. Ignoring and simply going on."
                print e